Shellcode Essentials
Shellcode Essentials: Finding Windows APIs Dynamically
Background
Recently, I was analyzing one of the Expiro variants (around 2022) that was decrypting its code during runtime. It needed to resolve its APIs manually by parsing its Process Environment Block (PEB). While this technique is well documented, I decided to explore it further by writing my own shellcode.
Understanding Shellcode
Shellcodes are position-independent code, meaning they can run without requiring the Windows loader to load their APIs. However, to perform meaningful operations, they need to call Windows APIs. Windows provides two crucial functions:
LoadLibrary: Used to load modules in the current processGetProcAddress: Used to find the address of a function within a loaded module
If a shellcode can locate the address of these two functions, it gains the ability to:
- Load any required modules using
LoadLibrary - Find and call functions within those modules using
GetProcAddress
The Hunt for kernel32.dll
The LoadLibrary function resides within kernel32.dll, which is loaded into all Windows processes by default. Therefore, if we can:
- Find the base address of
kernel32.dllin memory - Parse its PE header
- Locate the export table
We can ultimately find the address of the LoadLibrary function and bootstrap our shellcode’s API resolution capabilities.
Let’s start with parsing the PEB. The FS register can be used to get the TEB of any process, and using [fs:0x30] we get the PEB of the process.
Now the PEB further contains a field called LDR of type _PEB_LDR_DATA at offset 0xC.
Digging further down the list and following the _PEB_LDR_DATA structure, we see that it contains fields like InLoadOrderModuleList, InMemoryOrderModuleList, InInitializationOrderModuleList. These are the head of a linked list which contains the loaded modules by load order, in memory order, and initialization order respectively.
Let’s focus our attention on InLoadOrderModuleList. According to WinDbg, its type is _LIST_ENTRY, and Microsoft tells us that it’s a head to a doubly-linked list, and each link (*Flink and *Blink) is a pointer to LDR_DATA_TABLE_ENTRY structure.
So if we follow the InLoadOrderModuleList, we should land on a LDR_DATA_TABLE_ENTRY structure. Let’s attach our debugger to notepad.exe and follow the first _LIST_ENTRY structure.
The _LDR_DATA_TABLE_ENTRY has a field called BaseDllName, which is the name of our module, which is loaded first.
If we now follow the next _LIST_ENTRY again, we get ntdll.dll.
Let’s follow the _LIST_ENTRY for one last time, we get the kernel32.dll. Notice that we have another field called BaseDll that is the base address of the loaded DLL.
This load order of module name, ntdll.dll, and kernel32.dll is going to be the same for Windows 10. We can use this information now to locate the base address of kernel32.dll.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>
#include <stdio.h>
extern int get_kernel32_address();
int main()
{
int kernel32_address, getproc_address;
kernel32_address = get_kernel32_address();
printf("kernel32 address is %x\r\n", kernel32_address);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
global _get_kernel32_address
section .text
_get_kernel32_address:
xor eax, eax
mov eax, [fs:0x30] ; PEB
mov eax, [eax + 0xC] ; LDR offset
mov eax, [eax + 0xC] ; InLoadOrderModuleList
mov eax, [eax] ; ntdll
mov eax, [eax] ; kernel32
mov eax, [eax + 0x18] ; DLLBase
ret
Searching the LoadLibrary/GetProcAddress
Now that we have the base address of kernel32.dll, it’s time to parse its header and exports in order to get the address of LoadLibrary/GetProcAddress. We can parse any one as an example. Let’s parse GetProcAddress.
We can start off by jumping to the export directory.
We can first jump to the NumberOfNames to find out the total number of named functions. After that, we can loop over all the name pointers to get a match with “GetProcAddress”.
Here are a few things that we need to consider for the next stage:
- The
AddressOfNameshas the array of name pointers. We need to iterate over this and compare with “GetProcAddress”. - Once we find the index with a matching name, we can use that same index in
AddressOfNamesOrdinalsto get a value that can be used into theAddressOfFunctionsas an index. - Now this value from
AddressOfNamesOrdinalscan directly act as an index intoAddressOfFunctions. This index will give us the address ofGetProcAddress.
Fig: Here is a neat diagram illustrating the above (resources.infosecinstitute.com)
Let’s put it all together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <Windows.h>
#include <stdio.h>
extern int get_kernel32_address();
extern int fetch_getprocaddress(int);
int main()
{
int kernel32_address, getproc_address;
kernel32_address = get_kernel32_address();
printf("kernel32 address is %x\r\n", kernel32_address);
getproc_address = fetch_getprocaddress(kernel32_address);
printf("GetProcAddress is at %x\r\n", getproc_address);
}
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
global _get_kernel32_address
global _fetch_getprocaddress
section .text
_get_kernel32_address:
xor eax, eax
mov eax, [fs:0x30] ; PEB
mov eax, [eax + 0xC] ; LDR offset
mov eax, [eax + 0xC] ; InLoadOrderModuleList
mov eax, [eax] ; ntdll
mov eax, [eax] ; kernel32
mov eax, [eax + 0x18] ; DLLBase
ret
_fetch_getprocaddress:
push 0x00007373 ; ss
push 0x65726464 ; erdd
push 0x41636F72 ; Acor
push 0x50746547 ; PteG
sub esp, 8 ; for local variables
mov eax, [esp + 0x20] ; kernel32_address
mov ebx, [eax + 0x3C] ; e_elfnew
add ebx, eax ; Nt header 'PE'
add ebx, 0x78 ; Export directory offset
mov ebx, [ebx] ; Export directory rva
add ebx, eax ; Export directory VA
add ebx, 0x18 ; Number of names offset
mov ecx, [ebx] ; Number of names
add ebx, 0x4 ; Address of Functions offset
mov [esp + 0x4], ebx ; local var, offset of address of functions
add ebx, 0x4 ; Address of names offset
mov ebx, [ebx] ; Address of names rva
add ebx, eax ; Address of Names VA
mov edx, ecx ; Number of names
xor ecx, ecx ; using as index
mov ecx, -1
sub ebx, 4
loop_start:
inc ecx
cmp ecx, edx
jz loop_end
add ebx, 4 ; dword holding address of function
mov esi, eax
add esi, [ebx] ; VA of function name string start
mov edi, [esi]
cmp edi, [esp+0x8] ; compare GetP
jnz loop_start
mov edi, [esi + 0x4]
cmp edi, [esp + 0xC] ; compare roca
jnz loop_start
mov edi, [esi + 0x8]
cmp edi, [esp + 0x10] ; ddre
jnz loop_start
movsx edi, word [esi + 0xC]
movsx esi, word [esp + 0x14]
cmp esi, edi
jnz loop_start
loop_end:
cmp ecx, edx
jz end_program ; terminating due to unmatched function name
mov ebx, [esp + 0x4] ; local var, offset of address of functions
add ebx, 0x8 ; offset of address of ordinals
mov ebx, [ebx] ; rva of address of ordinals
add ebx, eax ; VA of address of ordinals
shl ecx, 1
add ebx, ecx ; get the matched dword index
shr ecx, 1 ; restore ecx
movsx ebx, word [ebx] ; get the index to address of functions
mov [esp], ebx ; store the address of function index as local var
mov ebx, [esp + 0x4] ; local var, offset of address of functions
mov ebx, [ebx] ; rva of address of functions
add ebx, eax ; VA of address of functions
mov ecx, [esp]
shl ecx, 2
add ebx, ecx ; get the appropriate index of function
add eax , [ebx] ; get the VA address of getprocaddress
add esp, 0x18
ret
end_program:
add esp, 0x18
xor eax, eax
ret
Let’s check if everything works as expected by fetching the address of GetComputerName API.
Find all the reference code here: GitHub Repository












