Leveraging from PE parsing technique to write x86 shellcode

1. Introduction

Shellcode is often used alongside an exploit to subvert a running program, or by an injector performing a process injection. Hence, shellcode must dynamically locate the required WIN32 API functions to work reliably and efficiently in different Windows versions, and for that task, it typically uses LoadLibraryA and GetProcAddress that are exported from "kernel32.dll".
In this post, we will explore the world of Win32 shellcode development using what we learned from the previous blog regarding PEB structure, and specifically, we will understand how the shellcode leverage from PE parsing technique.
Thus, everything will be done directly within the debugger via IDA Pro, as well as we will easily get the opcodes and test our shellcode step by step. Finally, to truly understand the structure of Kernel32.dll, we will use CFF Explorer and view the contents of this precious DLL. Now, let's fasten our seat belts and start!

2. Finding Kernel32 Base Address

In our previous post "Digging into Windows PEB", we conclude that any executable file is being loaded in the memory, the Windows loads beside it the main core libraries kernel32.dll & ntdll.dll and saves the addresses of these libraries in the base address. The figure below describes the data structures that are followed to find the base address of kernel32.dll:
So, we will retrieve the base address of kernel32.dll from the PEB as shown in the following sample assembly code:
1
BITS 32
2
3
global _start
4
5
section .text
6
7
_start:
8
xor eax, eax ; Avoid Null Bytes
9
mov eax, [fs:eax + 0x30] ; EAX = PEB
10
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
11
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemoryOrderModuleList
12
lodsd ; EAX = Second module (ntdll.dll)
13
xchg eax, esi ; move to next element
14
lodsd ; EAX = Third(kernel32.dll)
15
mov eax, [eax + 0x10] ; EAX = Base address
Copied!
To understand this sample of assembly code just take a look at my previous post.
With this assembly code, we can find the kernel32.dll base address and store it in eax register, thus we need to assemble it via nasm. As the program is written in x86 assembly, the elf32 file type is specified using the-f flag then disassembled into opcodes using objdump :
Now, let's test our shellcode within the context of a C program, the shellcode can be placed in a test program (titled runner.c in this example) written in C, as shown below:
1
#include <windows.h>
2
3
const char main[] = "\x31\xc0\x64\x8b\x40\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x40\x10";
Copied!
This program should be compiled and executed in IDA PRO for debugging purposes:
Click to visualize EAX value holding base address of kernel32.dll
Now, eax register points to a memory address 0x75720000, which indicates that we got the base address of the kernel32.dll successfully. We can substantiate this result with the fact that we're pointing into e_magic which is a member of MS-DOS header of kernel32.dll :
Pointing into e_magic
The first field, e_magic, is called also the magic number. This field is used to identify an MS-DOS-compatible file type. All MS-DOS-compatible executable files set this value to 0x5A4D, which represents the ASCII characters MZ. At this level, we retrieve the address of memory where kernel32.dll is loaded!

3. Finding the export table of Kernel32.dll

Before diving into this part, I would like to highlight some mandatory definitions :
  • Relative Virtual Address(RVA): In an image file, this is the address of an item after it is loaded into memory, with the base address of the image file subtracted from it.The RVA of an item almost always differs from its position within the file on disk (file pointer). --> RVA = VA - BaseAddress
  • Virtual Address (VA):Same as RVA, except that the base address of the image file is not subtracted. The address is called a VA because Windows creates a distinct VA space for each process, independent of physical memory. For almost all purposes, a VA should be considered just an address. A VA is not as predictable as an RVA because the loader might not load the image at its preferred location. --> VA = RVA + BaseAddress
We found the base address of kernel32.dll in memory. Now we need to parse this PE file and find the export directory:
1
mov ebx, [eax + 0x3c] ; RVA of PE signature
2
add ebx, eax ; VA of PE signature
3
mov ebx, [ebx + 0x78] ; RVA of the exported directory
4
add ebx, eax ; VA of the exported directory
5
mov esi, [ebx + 0x20] ; RVA of the exported function names table
6
add esi, eax ; VA of the exported function names table
Copied!
e_lfanew is a 4-byte offset into the file where the PE file header is located. It is necessary to use this offset to locate the PE header in the file.
(Lines 1-2) We know that we can find the “e_lfanew” pointer at the offset 0x3C:
After this operation mov ebx, [eax + 0x3c], the ebx should hold the value F8, as depicted in the following figure:
Now, we can find the address of PE signature by adding kernel32 base address and the PE signature RVA: 0x75720000 + F8 = 0x757200F8 and we find the PE signature there:
As you know the PE header is a structure that contains the following information:
1
typedef struct _IMAGE_NT_HEADERS {
2
DWORD Signature;
3
IMAGE_FILE_HEADER FileHeader;
4
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
5
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Copied!
Signature member identifying the file as a PE image. The bytes are 0x4550(we could notice the value presented is 50 45 the reason is little-endian) which represents the ASCII characters "PE" as you can see above in our debugging process.
(Lines 3-4) The IMAGE_OPTIONAL_HEADER is a structure containing more useful information for us:
1
typedef struct _IMAGE_OPTIONAL_HEADER {
2
WORD Magic;
3
BYTE MajorLinkerVersion;
4
BYTE MinorLinkerVersion;
5
DWORD SizeOfCode;
6
DWORD SizeOfInitializedData;
7
....
8
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
9
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Copied!
It contains our main member which is DataDirectory that contains information such as imported and exported functions.
At the offset 0x78 of the PE header, we can find the RVA of Export Directory:
EBX hold RVA of export Directory
most of you will ask how we get this offset very simple:
or sizeof(PE_Signature) + sizeof(IMAGE_FILE_HEADER) + offsetof(IMAGE_OPTIONAL_HEADER,DataDirectory) = 120 bytes (78 in hex)
Again, we add this value to the eax register and we are now placed on the export directory of the kernel32.dll.
The export directory is the following structure:
1
typedef struct _IMAGE_EXPORT_DIRECTORY {
2
DWORD Characteristics;
3
DWORD TimeDateStamp;
4
WORD MajorVersion;
5
WORD MinorVersion;
6
DWORD Name;
7
DWORD Base;
8
DWORD NumberOfFunctions;
9
DWORD NumberOfNames;
10
DWORD AddressOfFunctions;
11
DWORD AddressOfNames;
12
DWORD AddressOfNameOrdinals;
13
};
Copied!
The relevant fields in the _IMAGE_EXPORT_DIRECTORY:
  • AddressOfFunctions is an array of RVAs that points to the actual export functions. It is indexed by an export ordinal. The shellcode needs to map the export name to the ordinalto use this array.
This mapping is done via AddressOfNames and AddressOfNameOrdinals arrays. These two arrays exist in parallel. They have the same number of entries, and equivalent indices into these arrays are directly related.
  • AddressOfNames is an array of 32-bit RVAs that point to the strings of symbol names.
  • AddressOfNameOrdinals is an array of 16-bit ordinals. For a given index id into these arrays, the symbol at AddressOfNames[id] has the export ordinal value at AddressOfNameOrdinals[id].
(Lines 5-6) In the IMAGE_EXPORT_DIRECTORY structure, at the offset 0x20, contains an RVA of the exported function names table which is 0x000945B4:
Again :p most of you will ask how we get this offset very simple:
Let's retrieve the address of exported function names table by adding the Name Pointer Table RVA 0x000945B4 with kernel32 base address 0x75720000, which results in 0x757B45B4 that store the name of an RVA of the first exported function 0x00096BCA:

4. Find LoadLibrayA using Hashed export Names

It’s not always a good idea to use ASCII strings, an UNICODE string since it will just make our shellcode bigger! and also easy to spot. So it would be better to use a hash value to look up our targeted WIN32 API functions.
For that reason, we used the C program from StackOverflow that resolves all exported WIN32 API functions that exist in kernel32.dll (we're doing the same via assembly version), and in every callback, we will generate a unique hash for the corresponding exported function via the following code snippet:
1
#include <Windows.h>
2
#include <stdio.h>
3
#include <string.h>
4
#include <stdlib.h>
5
6
7
void EnumExportedFunctions (char *, void (*callback)(char*));
8
int Rva2Offset (unsigned int);
9
10
typedef struct {
11
unsigned char Name[8];
12
unsigned int VirtualSize;
13
unsigned int VirtualAddress;
14
unsigned int SizeOfRawData;
15
unsigned int PointerToRawData;
16
unsigned int PointerToRelocations;
17
unsigned int PointerToLineNumbers;
18
unsigned short NumberOfRelocations;
19
unsigned short NumberOfLineNumbers;
20
unsigned int Characteristics;
21
} sectionHeader;
22
23
sectionHeader *sections;
24
unsigned int NumberOfSections = 0;
25
26
int Rva2Offset (unsigned int rva) {
27
int i = 0;
28
29
for (i = 0; i < NumberOfSections; i++) {
30
unsigned int x = sections[i].VirtualAddress + sections[i].SizeOfRawData;
31
32
if (x >= rva) {
33
return sections[i].PointerToRawData + (rva + sections[i].SizeOfRawData) - x;
34
}
35
}
36
37
return -1;
38
}
39
40
void EnumExportedFunctions (char *szFilename, void (*callback)(char*)) {
41
FILE *hFile = fopen (szFilename, "rb");
42
43
if (hFile != NULL) {
44
if (fgetc (hFile) == 'M' && fgetc (hFile) == 'Z') {
45
unsigned int e_lfanew = 0;
46
unsigned int NumberOfRvaAndSizes = 0;
47
unsigned int ExportVirtualAddress = 0;
48
unsigned int ExportSize = 0;
49
int i = 0;
50
51
fseek (hFile, 0x3C, SEEK_SET);
52
fread (&e_lfanew, 4, 1, hFile);
53
fseek (hFile, e_lfanew + 6, SEEK_SET);
54
fread (&NumberOfSections, 2, 1, hFile);
55
fseek (hFile, 108, SEEK_CUR);
56
fread (&NumberOfRvaAndSizes, 4, 1, hFile);
57
58
if (NumberOfRvaAndSizes == 16) {
59
fread (&ExportVirtualAddress, 4, 1, hFile);
60
fread (&ExportSize, 4, 1, hFile);
61
62
if (ExportVirtualAddress > 0 && ExportSize > 0) {
63
fseek (hFile, 120, SEEK_CUR);
64
65
if (NumberOfSections > 0) {
66
sections = (sectionHeader *) malloc (NumberOfSections * sizeof (sectionHeader));
67
68
for (i = 0; i < NumberOfSections; i++) {
69
fread (sections[i].Name, 8, 1, hFile);
70
fread (&sections[i].VirtualSize, 4, 1, hFile);
71
fread (&sections[i].VirtualAddress, 4, 1, hFile);
72
fread (&sections[i].SizeOfRawData, 4, 1, hFile);
73
fread (&sections[i].PointerToRawData, 4, 1, hFile);
74
fread (&sections[i].PointerToRelocations, 4, 1, hFile);
75
fread (&sections[i].PointerToLineNumbers, 4, 1, hFile);
76
fread (&sections[i].NumberOfRelocations, 2, 1, hFile);
77
fread (&sections[i].NumberOfLineNumbers, 2, 1, hFile);
78
fread (&sections[i].Characteristics, 4, 1, hFile);
79
}
80
81
unsigned int NumberOfNames = 0;
82
unsigned int AddressOfNames = 0;
83
84
int offset = Rva2Offset (ExportVirtualAddress);
85
fseek (hFile, offset + 24, SEEK_SET);
86
fread (&NumberOfNames, 4, 1, hFile);
87
88
fseek (hFile, 4, SEEK_CUR);
89
fread (&AddressOfNames, 4, 1, hFile);
90
91
unsigned int namesOffset = Rva2Offset (AddressOfNames), pos = 0;
92
fseek (hFile, namesOffset, SEEK_SET);
93
94
for (i = 0; i < NumberOfNames; i++) {
95
unsigned int y = 0;
96
fread (&y, 4, 1, hFile);
97
pos = ftell (hFile);
98
fseek (hFile, Rva2Offset (y), SEEK_SET);
99
100
char c = fgetc (hFile);
101
int szNameLen = 0;
102
103
while (c != '\0') {
104
c = fgetc (hFile);
105
szNameLen++;
106
}
107
108
fseek (hFile, (-szNameLen)-1, SEEK_CUR);
109
char* szName = calloc (szNameLen + 1, 1);
110
fread (szName, szNameLen, 1, hFile);
111
112
callback (szName);
113
114
fseek (hFile, pos, SEEK_SET);
115
}
116
}
117
}
118
}
119
}
120
121
fclose (hFile);
122
}
123
}
124
125
void calculate_hash(char* szName) {
126
DWORD hash = 0;
127
DWORD i = 0;
128
for(i; i < strlen(szName); i++) {
129
hash <<= 1;
130
hash += szName[i];
131
}
132
printf("%s:0x%08x\n", szName, hash);
133
134
}
135
136
int main (int argc, char **argv) {
137
printf("Loading %s\n", argv[1]);
138
EnumExportedFunctions(argv[1], calculate_hash);
139
return 0;
140
}
Copied!
If you notice above the calculate_hash function basically it's a "loop for" that simply shifts left by 1 the value existing in hash variable then add it to szName[i] which hold an exported function name.
Generate all hashes of exported functions that actually exist in kernel32.dll:
1
.\dll_hash_calculator.exe C:\Windows\SysWOW64\kernel32.dll > hashes.txt
Copied!
as result:
let's first inspect our first exported function in memory using the following assembly code :
1
push 0x00059ba3
2
xor ecx, ecx ;prepare counter
3
mov edx, eax ;save eax into edx
4
call _find_addr
5
6
_find_addr:
7
inc ecx ;increment name index counter
8
lodsd ;load name rva into eax and increment esi by 4 to next RVA
9
add eax,edx ;add kernel32 base address to get VA of function name
Copied!
(Line 1) we push the precomputed hash value of LoadLibraryA on the stack since we will use it after to find our targeted function.
(Line 2) we set ecx register to 0 for mapping the export name of the targeted WIN32 API function(LoadLibraryA) with his ordinal to retrieve his address in AddressOfFunctions array.
(Line 3) we save eax register that actually holds the base address of kernel32.dll into edx, because after we will use lodsd that will overwrite our eax register.
Loads a byte, word, or doubleword from the source operand into the AL, AX, or EAX register, respectively.
(Line 6-9) we create a procedure called "_find_addr" which is presented via a label in our asm code and we will use lodsd that will take esi register the pointer to the first function name. The lodsd instruction will place in eax the offset to the function name ( “AcquireSRWLockExclusive”) and we add this with the edx (kernel32 base address) to find the correct pointer. Note that the lodsd instruction will also increment the esi register value with 4! This helps us because we do not have to increment it manually, we just need to call again lodsd to get the next function name pointer:
Remember we're incrementing ecx register, which will be the counter of our functions and the function ordinal number.
Next step, we need to calculate the hash of every exported function name as we did in the C language version:
1
find_addr:
2
inc ecx
3
lodsd
4
add eax,edx
5
call _calculate_hash
6
7
_calculate_hash:
8
push ecx
9
push edx
10
11
xor ecx, ecx
12
mov edi, ecx
13
mov edx, edi
14
15
_loop:
16
shl edi,1 ;
17
mov dl, BYTE [eax + ecx]
18
add edi, edx
19
inc ecx
20
cmp BYTE[eax + ecx], 0
21
jne _loop
22
23
pop edx
24
pop ecx
25
ret
Copied!
(Line 7-13) we saved all values set on ecx and edx on the stack since we will need them after, then we cleared respectively ecx, edi, and edx. For clarity, we will not clear eax since now it points into the first BYTE of the first exported function:
(Line 15-21) Mainly the _loop function it's an asm representation of C language hash function mentioned previously, the instructionshl edi,1shift by left the value stored in edi. We stored the first BYTE of the exported function name in dl(8 bits version of edx) and of course, ecx is used in this case to ensure that we're keeping tracking every BYTE, this is done viamov dl, BYTE [eax + ecx]then we should add edx to edi. However, we need to confirm that we reached the end of the exported function name and this is done via the following instructioncmp [edi + ecx], 0since every exported function name it's a null-terminated string and finally, we keep looping till the ZF is set to 1:
Now, edi hold the hash of the first exported function name 2A992F1D, we can confirm that by grepping into our hash.txt generated previously:
(Line 23-25)Finally, we need to restore all registers values that we pushed on that stack and get back to our function fin_addr via ret instruction.
Now, we need to compare if the value stored in edi match the hash of LoadLibraryA which already pushed on the stack:
1
_find_addr:
2
inc ecx
3
lodsd
4
add eax,edx
5
call _calculate_hash
6
cmp edi, [esp + 4]
7
jnz _find_addr
8
ret
Copied!
(Line 1- 5 ) Already explained previously.
(Line 6-8) You should put in mind that whenever the function _calculate_hash is called, it will return a hash value of an exported function name in edi register, and to make sure that that we find LoadLibraryA hash function we need to set this instruction : cmp edi, [esp + 4 ] and keep looping till ZF is set to 1. For debugging purposes, we will set a BP at ret instruction:
At that point we're basically reaching our goal which is finding the hash value of LoadLibraryA:
  • eax point at the beginning of LoadLibraryA.
  • edi holds the hash value of LoadLibrary 0x00059ba3.
  • The most precious value for us to retrieve the address of LoadLibraryA is:
    • ecx = 0x000003C6 which is the function ordinal number.

5.Find the address of LoadLibraryA function

At this point, we only found the ordinal number of the LoadLibrayA function, but we can use it to find the actual address of this function:
1
call _get_addr
2
push edi
3
4
_get_addr:
5
mov esi, [ebx + 0x24] ; RVA of function ordinal table
6
add esi, edx ; VA of function ordinal table
7
mov cx, WORD[esi + ecx * 2] ; get LoadLibray biased_ordinal
8
dec ecx ; get LoadLibray ordinal
9
mov esi, [ebx + 0x1c] ; RVA of AddressOfFunctions = The Export Address Table
10
add esi, edx ; VA of the Exported Table
11
mov edi, [esi + ecx * 4] ; RVA of LoadLibrayA
12
add edi, edx ; VA of LoadLibrayA
13
ret
Copied!
(Line 4-5) At this point, we have in ebx a pointer to the IMAGE_EXPORT_DIRECTORY structure. At the offset 0x24 of the structure, we can find the “AddressOfNameOrdinals” offset. In line 5, we add this offset to edx register which is the base address of the kernel32.dll so we get a valid pointer to the name ordinals table. Some of you may ask the logic behind 0x24:
(Lines 6-7) The esi register contains the pointer to the name ordinals array.
The name ordinals array (export ordinal table) is an array of 16-bit unbiased indexes into the export address table. Ordinals are biased by the Ordinal Base field of the export directory table. In other words, the ordinal base must be subtracted from the ordinals to obtain true indexes into the export address table.
This array contains two-byte numbers. Up to now, we have the biased_ordinal of LoadLibraryA function in the ecx register, so this way we get the function address ordinal (index). This will help us to get the function address.
May one of you get confused regarding this instruction mov cx, [esi + ecx * 2].In fact, we want the value of the ecx=59ba3 element of the name ordinals array of type T: you do [arraystart + (ecx*sizeof(T))] --> [esi + ecx * 2] and the ordinal array it stores ordinals in 2 bytes=T, and finally we stored this value in 2 bytes version of ecx which is cx.
We have to subtract biased_ordinal from OrdinalBase to get the ordinal number of our function:
1
ordinal = biased_ordinal - OrdinalBase; //represented by the instruction dec ecx
Copied!
Since in our case OrdinalBase equal to 1:
Until now, we have the ordinal number stored in ecx register of LoadLibrayA in our hands as depicted below:
(Lines 8-9) At the offset 0x1c, we can find the “Export Address Table” array. We just add the base address of kernel32.dll and we are placed at the beginning of the array. Some of you may ask again the logic behind 0x1c:
(Lines 10-11) Now that we have the correct index for the “Export Address Table” array in ecx, we can find the LoadLibrayA function pointer (RVA of LoadLibraryA) at the AddressOfFunctions[ecx] location:
We use "ecx * 4" because each pointer has 4 bytes and esi points to the beginning of the array.
In the end, we add the base address so we will have in the edi the pointer to the LoadLibraryA function:
Finally, we resolve dynamically at runtime the address of LoadLibraryA is 75A60BD0 and of course, we set ret instruction to return from the actual procedure _get_addr.
(Line 2) We're basically pushing the address of LoadLibrayA on the stack through push edibecause we will use it after in our main procedure.

6.Find the address of GetProcAddress function

Roughly the same steps that we deeply explain in the previous section to find LoadLibrayA address however, I will clarify some instructions:
1
_start:
2
xor eax, eax ; Avoid Null Bytes
3
mov eax, [fs:eax + 0x30] ; EAX = PEB
4
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
5
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemoryOrderModuleList
6
lodsd ; EAX = Second module (ntdll.dll)
7
xchg eax, esi ; move to next element
8
lodsd ; EAX = Third(kernel32)
9
mov eax, [eax + 0x10] ; EAX = Base address
10
11
mov ebx, [eax + 0x3c] ; RVA of PE signature
12
add ebx, eax ; VA of PE signature
13
mov ebx, [ebx + 0x78] ; RVA of the exported directory
14
add ebx, eax ; VA of the exported directory
15
mov esi, [ebx + 0x20] ; RVA of the exported function names table
16
add esi, eax ; VA of the exported function names table
17
mov edx, eax ; save eax into edx
18
push esi ; save the VA of the exported function names table
19
20
push 0x00059ba3 ; hash of LoaLibraryA
21
xor ecx, ecx ; counter for storing the ordinal number of LoaLibraryA
22
call _find_addr
23
call _get_addr
24
push edi
25
26
27
mov esi, [esp + 8] ; restore VA of the exported function names table
28
29
30
push 0x0015bdfd
31
xor ecx, ecx ; prepare counter
32
call _find_addr
33
call _get_addr
34
push edi
35
36
_get_addr: ...
37
_find_addr: ...
38
_calculate_hash: ...
Copied!
First, let's agree that the precomputed hash of GetProcAddress is: 0dfdx0015b
(Line 18) We're saving the value of esi on the stack why? As you know esi register is holding the address of exported function names and since we will start the process of finding the address of LoadLibray this value will be overwritten the fact that lodsd instruction will increment the esi register value with 4. Hence, we push it on the stack then retrieve back after finding the address of LoadLibraryA via the following instruction: mov esi, [esp + 8] then, we can smoothly start the process of finding GetProcAddress address.
(Line 34) We're doing the same approach as before pushing the address of GetProcAddress on the stack throughpush edisince we will use it after in our main procedure.

7. Load user32.dll library & Get MessageBox function address

We previously found the LoadLibraryA function address, we will use it now to load into memory the "user32.dll" library which contains our MessageBox function that will use it as POC to leverage from the technique discussed on this blog:
1
HMODULE LoadLibraryA(
2
LPCSTR lpLibFileName
3
);
Copied!
  • lpLibFileName is the name of the module which will be in our case "user32.dll".
1
_do_main:
2
mov edi, [esp + 8]
3
push "ll"
4
push "32.d"
5
push "user"
6
push esp
7
call edi
8
9
push "oxA"
10
push "ageB"
11
push "Mess"
12
push esp
13
push eax
14
mov edi, [esp + 32]
15
call edi
Copied!
(Lines 1-7) we set the procedure _do_main which represents our main function. Then, as you notice previously we push on the stack the "LoadLibraryA" address. So we retrieve it through the stack pointer esp. Now, we want to call "LoadLibraryA("user32.dll")". So we need to place the user32.dll string on the stack.
At esp, we have the "user32.dll" string. We push this parameter on the stack to load the library and this will return in eax the user32.dll library base address where the DLL is loaded into memory. We will need it later:
We loaded into memory the user32.dll library, now we want to call GetProcAddress to get the address of the MessageBox function.
1
FARPROC GetProcAddress(
2
HMODULE hModule,
3
LPCSTR lpProcName
4
);
Copied!
  • hModule A handle to the DLL module that contains the function or variable. The LoadLibraryA, function returns this handle.
  • lpProcNameThe function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be in the low-order word; the high-order word must be zero.
(Line 9-15) We want to call "GetProcAddress(user32.dll, "MessageBox")" so again we need to place the MessageBox string on the stack. At esp, we have the "MessageBox " string then we push this parameter on the stack as well as we push also the eax register which contains the user32.dll base address, and calls edi register which holds GetProcAddress function:
After, calling GetProcAddress, it will return in eax the MessageBox base address as depicted above, since we will need it after.

8. Call MessageBox function

Now we have all the ingredients to call MessageBox function, we just need to prepare the right parameters for it:
1
int MessageBox(
2
HWND hWnd,
3
LPCTSTR lpText,
4
LPCTSTR lpCaption,
5
UINT uType
6
);
Copied!
  • hWnd A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no owner window.
  • lpText The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage return and/or linefeed character between each line.
  • lpCaption The dialog box title. If this parameter is NULL, the default title is Error.
  • uTypeThe contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups of flags.
As an example, we want to call:
1
int MessageBox(
2
NULL,
3
(LPCWSTR)L"T3nb3w",
4
(LPCWSTR)L"T3nb3w",
5
MB_OK
6
);
Copied!
Remember that the calling convention for x86, arguments are push in reverse order:
Thus, we can do that via the following asm code:
1
push "3w"
2
push "T3nb"
3
mov esi, esp
4
5
xor ebx, ebx
6
push ebx ; MB_OK = 0x00000000L
7
push esi ; (LPCWSTR)L"T3nb3w"
8
push esi ; (LPCWSTR)L"T3nb3w"
9
push ebx ; NULL
10
call eax
Copied!
(Line 1-3)So, we need to place the "T3nb3w" string on the stack. At esp, we have the "T3nb3w" string then we move this parameter into esi.
(Line 5-10) we cleared ebx register and we push respectively the following registers ebx, esi, esi, and ebx, finally we're calling eax that hold already the base address of MessageBox.
Below shows our assembly in a debugger. The MessageBox pops after call eax instruction is executed:

8. Final Shellcode

Now we just need to add all parts together and the final shellcode is the following:
1
BITS 32
2
3
global _start
4
5
section .text
6
7
_start:
8
xor eax, eax ; Avoid Null Bytes
9
mov eax, [fs:eax + 0x30] ; EAX = PEB
10
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
11
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemoryOrderModuleList
12
lodsd ; EAX = Second module (ntdll.dll)
13
xchg eax, esi ; move to next element
14
lodsd ; EAX = Third(kernel32)
15
mov eax, [eax + 0x10] ; EAX = Base address
16
17
mov ebx, [eax + 0x3c] ; RVA of PE signature
18
add ebx, eax ; VA of PE signature
19
mov ebx, [ebx + 0x78] ; RVA of the exported directory
20
add ebx, eax ; VA of the exported directory
21
mov esi, [ebx + 0x20] ; RVA of the exported function names table
22
add esi, eax ; VA of the exported function names table
23
mov edx, eax ; save eax into edx
24
push esi ; save the VA of the exported function names table
25
26
push 0x00059ba3
27
xor ecx, ecx ; prepare counter
28
call _find_addr
29
call _get_addr
30
push edi
31
32
33
mov esi, [esp + 8] ; restore VA of the exported function names table
34
35
36
push 0x0015bdfd
37
xor ecx, ecx ; prepare counter
38
call _find_addr
39
call _get_addr
40
push edi
41
jmp _do_main
42
43
_get_addr:
44
mov esi, [ebx + 0x24] ; RVA of function ordinal table
45
add esi, edx ; VA of function ordinal table
46
mov cx, WORD [esi + ecx * 2] ; get LoadLibray biased_ordinal
47
dec ecx ; get LoadLibray ordinal
48
mov esi, [ebx + 0x1c] ; RVA of AddressOfFunctions or the Export Table
49
add esi, edx ; VA of the Exported Table
50
mov edi, [esi + ecx * 4] ; RVA of LoadLibrayA
51
add edi, edx ; VA of LoadLibrayA
52
ret
53
54
55
_find_addr:
56
inc ecx ;increment name index counter
57
lodsd ;load name rva into eax and increment esi by 4 to next rva
58
add eax,edx ;add kernel32.dll base address to get va of function name
59
call _calculate_hash ;get the hash
60
cmp edi, [esp + 4] ;compare our hash
61
jnz _find_addr ;loop if not matching
62
ret ;return, ecx now holds the name array index of our function
63
64
_calculate_hash:
65
push ecx
66
push edx
67
68
xor ecx, ecx
69
mov edi, ecx
70
mov edx, edi
71
72
_loop:
73
shl edi,1
74
mov dl, BYTE [eax + ecx]
75
add edi, edx
76
inc ecx
77
cmp BYTE[eax + ecx], 0
78
jne _loop
79
80
pop edx
81
pop ecx
82
ret
83
84
_do_main:
85
mov edi, [esp + 8]
86
push "ll"
87
push "32.d"
88
push "user"
89
push esp
90
call edi
91
92
push "oxA"
93
push "ageB"
94
push "Mess"
95
push esp
96
push eax
97
mov edi, [esp + 32]
98
call edi
99
100
push "3w"
101
push "T3nb"
102
mov esi, esp
103
104
xor ebx, ebx
105
push ebx
106
push esi
107
push esi
108
push ebx
109
call eax
Copied!
Note this shellcode is used to learn PE parsing Export Table technique through debugging it's not 100% operational.
Our shellcode will only work on processes that have already kernel32.dll loaded. However, if you create a suspended process the only loaded modules will be the exe and ntdll.dll, so the shellcode wouldn't work if you inject it into a brand new suspended process. In this case, we could alter the shellcode to use ntdll!LdrLoadDll(Undocumented Function) instead of kernel32!LoadLibrary.

9. Conclusion

Frankly, it took time for me to understand this wonderful technique of PE Parsing Export Table used by sophisticated malware thus I tried to explain it from scratch. Using the shellcode’s PE parsing ability instead of GetProcAddress has the additional benefit of making reverse-engineering of the shellcode more difficult. Also, using a hash of WIN32 API function name was a good idea to hide them from casual inspection.
I hope you have learned step by step how we can leverage from PE Parsing Export Table to write Windows shellcode then, resolve all of the shellcode's libraries so that it can interact with the system.
Final Note: I am not a shellcode developer expert I'm just a learner, If you think I said anything incorrect anywhere, feel free to reach out to me and correct me, I would highly appreciate that. And finally, thank you very much for taking your time to read this post.

References

Portable Executable File Format
kjk
PE Format - Win32 apps
docsmsft
IMAGE_NT_HEADERS32 (winnt.h) - Win32 apps
docsmsft
Win32 API to enumerate dll export functions?
Stack Overflow
Control: x86 Instruction Set Reference
Introduction to Windows shellcode development – Part 3
Security Café
MessageBox function (winuser.h) - Win32 apps
docsmsft