security/리버싱핵심원리

Code Injection - using assembly

민사민서 2023. 3. 24. 11:20

CodeInjection.exe의 ThreadProc 함수

CodeInjection.exe를 디버깅하면서 생각했다. 어셈블리로 ThreadProc()을 짠 후 바이트 코드 배열을 넘겨주어도 되겠다

 

Assembly code of ThreadProc

1  PUSH EBP
2  MOV EBP, ESP
3  PUSH ESI
4  MOV ESI, DWORD PTR SS:[EBP+8]
5  CALL 7
6  ASCII "user32.dll\x00"
7  CALL DWORD PTR DS:[ESI]; LoadLibraryA("user32.dll")
8  CALL 10
9  ASCII "MessageBoxA\x00"
10 PUSH EAX
11 CALL DWORD PTR DS:[ESI+4]; GetProcAddress(hMod, "MessageBoxA")
12 PUSH 0
13 CALL 17
14 ASCII "MINSEO\x00"
15 CALL 19
16 ASCII "This is code injection 2!!\x00"
17 PUSH 0
18 CALL EAX
19 XOR EAX, EAX
20 MOV ESP, EBP
21 SUB ESP,4
22 POP ESI
23 POP EBP
24 RETN 4

- 문자열을 코드 중간중간에 넣고, CALL 명령어를 통해 문자열 시작 주소(이자 호출 후 RET address)를 스택에 넣음

- 함수 prologue에서 EBP&ESI 값 백업, 함수 epilogue에서 복원 필요

- 함수 body에서 수많은 call이 이루어지면서 ESP가 변했으므로 ESP=EBP-4 상태로 다시 만들어 준 후 ESI, EBP 값을 복원하고 스택 프레임 정리해야 한다

=> 21 ADD ESP, 4 잘못 입력했더니 (혹은 생략했더니) MessageBoxA 호출 종료 후 해당 process가 crash함!!!

=> stack cleanup 정확하게 하자

- RETN 4 / RETN 모두 정상 동작

 

How to Programming in Ollydbg

1) 아무 실행파일(ex. asmtest.exe) 실행 후 코드 섹션 맨 윗부분부터 입력하기 시작

2) Space 통해 어셈블리 입력 (Keep size / Fill with NOPs uncheck)

3) 코드 중간에 data 넣고 싶으면 우클릭 - Edit - Binary Edit 

* 코드 중간에 데이터 넣을 경우 디버거는 instruction으로 잘못 해석: 안티 디버깅 기법

* 데이터 이후 오는 코드가 깨지는 게 싫어서 데이터 끝에 약간의 padding 남겨둠

4) 우클릭 - Edit - Copy All modifications to executable - 우클릭 - Save file

5) 별도의 파일로 저장 후 HXD에 던져 Code section의 file offset을 찾아 함수의 바이너리 코드 확보

6) NotePad++에서 편집 - 열 편집기 - 글자 삽입 이용해 0x 붙이고 콤마로 연결

* 파이썬 코드 만드는게 훨 편하겠네

print("Give me bytes array: ", end='')
bArray = input().split(" ")

res = "{ "
for i in range(len(bArray)-1):
    res += "0x"+bArray[i]+", "
    if i%10==9: 
        res += "\n  "

print(res+"0x"+bArray[-1]+" }")

7) CodeInjection2.exe 구현하기

// CodeInjection2.exe
#include "stdio.h"
#include "windows.h"

typedef struct _THREAD_PARAM {
	FARPROC pFunc[2];
} THREAD_PARAM, *PTHREAD_PARAM;

BYTE ThreadProc[] = 
{0x55 ,0x8B ,0xEC ,0x56 ,0x8B ,0x75 ,0x08 ,0xE8 ,0x0C ,0x00 ,0x00 ,0x00 ,0x75 ,0x73 ,0x65, 
 0x72 ,0x33 ,0x32 ,0x2E ,0x64 ,0x6C ,0x6C ,0x00 ,0xFF ,0xFF ,0x16 ,0xE8 ,0x0E ,0x00 ,0x00,
 0x00 ,0x4D ,0x65 ,0x73 ,0x73 ,0x61 ,0x67 ,0x65 ,0x42 ,0x6F ,0x78 ,0x41 ,0x00 ,0x55 ,0x8B,
 0x50 ,0xFF ,0x56 ,0x04 ,0x6A ,0x00 ,0xE8 ,0x08 ,0x00 ,0x00 ,0x00 ,0x4D ,0x49 ,0x4E ,0x53,
 0x45 ,0x4F ,0x00 ,0xE8 ,0xE8 ,0x1D ,0x00 ,0x00 ,0x00 ,0x54 ,0x68 ,0x69 ,0x73 ,0x20 ,0x69,
 0x73 ,0x20 ,0x63 ,0x6F ,0x64 ,0x65 ,0x20 ,0x69 ,0x6E ,0x6A ,0x65 ,0x63 ,0x74 ,0x69 ,0x6F,
 0x6E ,0x20 ,0x32 ,0x21 ,0x21 ,0x00 ,0x5A ,0x00 ,0x6A ,0x00 ,0xFF ,0xD0 ,0x31 ,0xC0 ,0x89,
 0xEC ,0x83 ,0xEC ,0x04 ,0x5E ,0x5D ,0xC2 ,0x04 ,0x00};


BOOL InjectCode(DWORD dwPID) {
	HMODULE hMod = NULL;
	HANDLE hProcess = NULL, hThread = NULL;
	LPVOID pRemoteBuf[2] = {0,};
	THREAD_PARAM tParam = {0,};
	DWORD dwSize = 0;

	hMod = GetModuleHandleA("kernel32.dll");
	tParam.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
	tParam.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

	if( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) {
		printf("OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
	}

	// Thread parameter
	dwSize = sizeof(THREAD_PARAM);
	if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE)) ) {
		printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
	}

	if( !WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&tParam, dwSize, NULL) ) {
		printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
	}

	// Thread procedure code
	dwSize = sizeof(ThreadProc);
	if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) ) {
		printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
	}

	if( !WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL) ) {
		printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
	}

	hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL);

	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) {
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

    if( !OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken) ) {
        printf("OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

    if( !LookupPrivilegeValue(NULL, lpszPrivilege, &luid) )  {
        printf("LookupPrivilegeValue error: %u\n", GetLastError() ); 
        return FALSE; 
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if( bEnablePrivilege )
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    if( !AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(TOKEN_PRIVILEGES),(PTOKEN_PRIVILEGES)NULL,(PDWORD)NULL) ) { 
        printf("AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED ) {
        printf("The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}

int main(int argc, char* argv[]) {
	if(argc!=2) {
		printf("USAGE: %s <PID> \n", argv[0]);
		return 1;
	}
	if(!SetPrivilege(SE_DEBUG_NAME, TRUE)) {
		return 1;
	}
	DWORD dwPID = (DWORD)atol(argv[1]);
	InjectCode(dwPID);

	return 0;
}

* THREAD_PARAM으로 문자열 넘겨줄 필요 없음 (인젝션하는 코드에 포함시킴)

* ThreadProc으로 명령어 바이트 배열 사용

8) 디버깅 해보기

* Break on new thread 상태에서 code injection 하면 우리가 짠 ThreadProc 어셈 코드가 잘 보인다 ㅎㅎ

 

추가 메모

1. 인젝션하는 코드에 문자열을 포함시키는 방법

1-1) 문자열 데이터를 코드 중간에 포함시킨다

- CALL instruction 다음 코드 영역에 문자열을 입력하여 스택에 문자열 주소(RET address) 들어가게 함

- 위에서 구현한 방식

1-2) PUSH 명령어를 이용해 원하는 문자열을 스택에 입력한다

- 4바이트 단위로, Little Endian 고려하여 문자열을 스택에 push한다

- PUSH ESP를 통해 문자열 주소를 스택에 넣는다

- 함수의 파라미터가 1개일 때, 또 문자열 길이가 길지 않을 때 이용할 만 함

=> ThreadProc에서는 LoadLibrayA("user32.dll")과 GetProcAddress("MessageBoxA")에서 사용 가능하겠다

=> MessageBoxA()에서는 사용 불가

잘 동작한다!!

2. _THREAD_PARAM 구조체조차 필요 없게 ThreadProc 구현?

- injected code가 스스로 타킷 프로세스에 로드된 LoadLibraryA, GetProcAddress API 주소를 구해야 한다

- PEB(Process Environment Block, 프로세스 정보 담고 있는 구조체)의 Ldr 멤버(_PEB_LDR_DATA 구조체 포인터) 이용

* PEB 시작주소 = FS:[0x30], PEB.Ldr offset = 0xC

* _PEB_LDR_DATA 구조체 멤버

    1) offset +0x00C: InLoadOrderModuleList (양방향 연결 리스트)

    2) offset +0x014: InMemoryOrderModuleList (양방향 연결 리스트)

    3) offset +0x01C: InInitializationOrderModuleList (양방향 연결 리스트)

* _LDR_DATA_TABLE_ENTRY 구조체들이 LIST_ENTRY에 의해 양방향으로 연결되어있다 (로딩된 모듈마다 구조체 하나 대응된다)

- _PEB_LDR_DATA 구조체에서 양방향 연결 리스트들을 타고 들어가 module name이 "kernel32.dll"일 때까지 traverse

- "kernel32.dll"에 해당하는 구조체를 발견하면 offset +0x18 위치의 DllBase를 구할 수 있다

- Dll 내부 offset을 통해 LoadLibraryA() 주소와 GetProcAddress() 주소 구할 수 있음

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
    	ULONG CheckSum;
        PVOID Reserved6;
    }
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

뭐 대략 이런 식으로?

BYTE ThreadProc[] =
{
    0x60,                               // pushad
    0x9C,                               // pushfd
    0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, // mov eax, fs:[0x30] ; Get PEB
    0x8B, 0x40, 0x0C,                   // mov eax, [eax+0xC] ; Get PEB_LDR_DATA
    0x8B, 0x70, 0x1C,                   // mov esi, [eax+0x1C] ; Get InInitializationOrderModuleList.Flink
    // Find kernel32.dll
    0x90,                               // nop ; Placeholder for the loop
    0xAD,                               // lodsd ; Get _LDR_DATA_TABLE_ENTRY from list
    0x8B, 0x68, 0x24,                   // mov ebp, [eax+0x24] ; Get _LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
    0x81, 0x7D, 0x00, 0x6C, 0x33, 0x00, // cmp dword ptr [ebp], 0x336C ; "kern"
    0x75, 0xF3,                         // jne (loop)
    0x8B, 0x68, 0x18,                   // mov ebp, [eax+0x18] ; Get _LDR_DATA_TABLE_ENTRY.DllBase (kernel32.dll base address)
    // LoadLibraryA
    0xB9, 0xE8, 0x83, 0x00, 0x00,       // mov ecx, 0x83E8 ; LoadLibraryA offset from kernel32.dll
    0x01, 0xCD,                         // add ebp, ecx ; ebp now points to LoadLibraryA
    0x53,                               // push ebx
    0x68, 0x72, 0x74, 0x00, 0x00,       // push 0x00747274 ; "user32"
    0x68, 0x75, 0x73, 0x65, 0x00,       // push 0x00657375
    0x54,                               // push esp
    0xFF, 0xD5,                         // call ebp ; LoadLibraryA("user32.dll")
    0x5B,                               // pop ebx
    // GetProcAddress
    0x8B, 0x5D, 0x00,                   // mov ebx, [ebp] ; GetProcAddress is the first entry in the Export Address Table (EAT)
    0x01, 0xEB,                         // add ebp, ebx ; ebp now points to GetProcAddress
    0x53,                               // push ebx
    0x68, 0x73, 0x61, 0x00, 0x00,       // push 0x00617361
~~