목표
- 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
를 가져오는 부분부터 호출하는 부분까지만 사용하면 된다.
|
|
쉘코드 실행
메모리에 쉘코드를 복사하여 실행하면 계산기가 실행되는 것을 확인할 수 있다.
|
|