목표
- Windows 32비트 유니버셜 쉘코드 개념 이해와 제작 및 실행
유니버셜 쉘코드란?
먼저 쉘코드는 취약점을 이용하여 특정 명령을 실행하기 위한 기계어 코드를 말한다.
하지만 ASLR 기법의 등장으로 인해 이를 우회할 필요가 있었다.
이러한 배경으로 동적으로 함수의 주소를 가져와 실행하는 유니버셜 쉘코드가 탄생하였다.
ASLR기법이란?
스택, 힙, 라이브러리, 등의 주소를 랜덤한 영역에 배치하여, 주소를 예측하기 어렵게 하는 기법
함수 주소 동적으로 구하기
ASLR 기법을 우회하기 위해 함수의 주소를 동적으로 구해야 한다.
함수의 주소는 DLL의 PE 헤더를 파싱하여 Export Table을 통해 함수의 오프셋을 찾을 수 있다.
그렇기 때문에 먼저 DLL의 베이스 주소를 구해야 한다.
DLL Base Address 구하기
이번 글에서는 계산기를 실행하는 것이 목표이기 때문에 프로그램을 실행시키는 WinExec 함수가 저장되어 있는 kernel32.dll의 주소를 구하도록 하겠다.
PEB
먼저 Windows 운영체제에서 각 프로세스의 정보를 담고 있는 PEB(Process Environment Block) 구조체에 접근해야 한다.
PEB 구조체는 fs 레지스터의 0x30 오프셋에 위치한다.

LDR
다음으로 프로세스에 로드된 모듈(DLL)에 대한 정보를 포함하는 PEB_LDR_DATA 구조체의 포인터인 Ldr에 접근해야 한다.
Ldr은 PEB 구조체의 0x0C 오프셋에 위치한다.

ModuleList
PEB_LDR_DATA 구조체에는 모듈의 정보를 저장하는 3개의 연결 리스트가 있는데, 각각 정렬되는 방식이 다르다.
InLoadOrderModuleList: 메모리에 로드된 순서대로 정렬InMemoryOrderModuleList: 메모리 상에 배치된 주소 순서대로 정렬InInitializationOrderModuleList: 초기화된 순서대로 정렬

DllBase
보통의 경우에는 메모리에 배치되는 순서가 Process Image -> ntdll.dll -> kernel32.dll 이기 때문에 InMemoryOrderModuleList의 세 번째 모듈이 kernel32.dll에 해당한다.
windbg를 통해 직접 확인해볼 수 있다.

Export Table RVA 구하기
kernel32.dll의 주소를 구했으니 이제 프로그램에서 내보낸 함수의 목록이 저장된 Export Table을 찾아야 한다.
Export Table의 위치는 Data Directory에 저장된 Export Table의 RVA를 통해 구할 수 있다.
NT Header
먼저 PE 파일의 로딩 정보를 저장하는 NT Header에 접근해야 한다.
NT Header의 위치는 e_lfanew에 저장되어 있는 오프셋을 통해 구할 수 있는데, 이는 DllBase(DOS Header)의 0x3C 오프셋에 저장되어 있다.

Optional Header
다음으로 프로그램 실행에 필요한 정보를 담고 있는 Optional Header에 접근해야 한다.
Optional Header는 NT Header의 0x18 오프셋에 위치한다.

Data Directory
이제 Export Table의 RVA가 저장되어 있는 Data Directory에 접근해야 한다.
Data Directory는 Optional Header의 0x60 오프셋에 위치한다.

Export Table RVA
Export Table의 RVA는 Data Directory 배열의 첫번째 항목이다.

WinExec 함수 RVA 구하기
이제 Export Table의 필드들을 순회하며 필요한 값들을 이용하여 WinExec 함수의 RVA를 구해야 한다.
Export Table Fields
Export Table은 함수의 이름과 주소를 저장한 RVA를 포함하는데, 아래 필드들을 이용할 것이다.
AddressOfFunctions: 함수들의 주소 배열의RVA(오프셋 :0x1C)AddressOfNames: 함수 이름들의RVA배열의RVA(오프셋 :0x20)AddressOfNameOrdinals: 이름과 함수 주소를 매핑하는Ordinals배열의RVA(오프셋 :0x24)
구하는 방법
AddressOfNames 필드에 접근하여 WinExec라는 이름의 함수의 인덱스를 기억한 뒤에,
AddressOfNameOrdinals 배열에 접근해서 인덱스번째의 Ordinal 값을 저장하면,
AddressOfFunctions 배열의 Ordinal 번째의 값이 WinExec 함수의 RVA 값이 될 것이다.
따라서 WinExec 함수의 RVA 값을 kernel32.dll의 DllBase에 더하면 WinExec의 절대 주소가 된다.
쉘코드 구현
위 정보들을 바탕으로 C언어로 쉘코드를 작성하여 컴파일한 뒤 추출하여 쉘코드로써 실행해보도록 하겠다.
C 코드 작성
Inline Assembly를 이용하면 fs 레지스터를 통해 PEB에 접근할 수 있다.
앞서 설명한 방법들을 통해 Export Table의 필드들을 순회하여 WinExec 함수의 주소를 구하였다.
"WinExec"나 "calc.exe"와 같은 문자열 리터럴 값은 이후 쉘코드로 추출하였을 때 그 배열을 절대주소로 불러오기 때문에, 이를 방지하기 위해 ASCII 값으로 저장하였다.
| |
쉘코드 추출
objdump를 통해 바이트 코드로 추출하였다.
_main 함수에서 PEB를 가져오는 부분부터 호출하는 부분까지만 사용하면 된다.
| |
쉘코드 실행
메모리에 쉘코드를 복사하여 실행하면 계산기가 실행되는 것을 확인할 수 있다.
| |