In the previous part, we found the address of Kernel32.dll dynamically by walking through the LDR struct. In this part, we will be focusing on finding the address of the functions(known as DLL symbols) within the Kernel32.dll and calling them by supplying the arguments. One of the methods of calling these symbols is by using LoadLibraryA to find the address of these symbols from Kernel32.dll. We will however not be doing that since that’s too easy. The main aim of my blog series is make folks understand how exactly the assembly code works, so that it becomes easy to write null-free shellcode for any function that they want. In this part, I will be explaining how to write shellcode for GetComputerNameA and we will walk through our executable in x32dbg disassembler to view the hostname of the computer its running on.
So, all that we have right now is just the address of Kernel32.dll which is 74F50000. Our next step would be to find the address of GetComputerNameA within Kernel32.dll and call it by supplying necessary parameters. In the previous blogpost, I wrote a C code to find the address of this function from Kernel32.dll and we know that the address in my system is 74F69AC0. Now if we subtract the address of our kernel32.dll from 74F69AC0, we get 19AC0. Now the thing is if I could just add this value to our kernel32.dll’s address and try to run it, it would run in my system, but it will most probably not work in any other system. The thing is Microsoft keeps updating their kernel32.dll, and because of this there may be new functions/symbols added to the DLL. And because of this, the address of our function will fluctuate depending upon the updates of kernel32.dll. So, we cannot simply add this address and try to make it work. Thus, we will have to dynamically walk through all the symbols inside the kernel32.dll, compare the function name GetComputerNameA with all the functions inside the kernel32.dll in a loop, and when we hit this compare string, whatever address we get for this string, should be our address which we can add to our kernel32.dll’s address in order to get the base address of our function. Sounds simple? Let’s proceed!
Now, in order to truly understand the structure of Kernel32.dll, let’s start PEview and view the contents of this DLL. Kernel32.dll is located in C:\Windows\System32 which can be seen as below:
Once we open up kernel32.dll, what we see are the internals of a PE (portable executable). If you click on IMAGE_DOS_HEADER on the left-hand side, then you will see something called as RVA (Relative Virtual Address), Data with some contents in it and a Description on the right-hand side. Now before we go into the depth of PEview, let’s first clearly understand what these terms stand for. RVA stands for Relative Virtual Address. This address is independent of the RAM and is fixed until the DLL is recompiled or modified. Inside Kernel32.dll, there are multiple RVAs. For example, if I say RVA of xyz function in kernel32.dll is 200, then the base address of the function becomes 74F50000 + 200 = 74f50200. This was just a theoretical view of how the base address is found. Let’s walk through the PEview first and let me explain how to find the base address of GetComputerNameA. Usually, any binary file contains a lot of information, especially when its an executable file. DLL and EXEs both come under the family of executables. Now, the start of any executable always contains tons of information as to whether it’s a DOS file or a ELF file, whether its MZ(Magic number for executable) or any other format. We can see that ours is MZ at RVA00000000 which means it is an executable. Similarly, it contains several other information such as OEM identifier, OEM information and lots of metadata. We are not interested in all these. Our area of interest starts at RVA0000003C or 0x3C in short. If you want to understand how the PE Header works, you can read up on that here.
The RVA of Offset to New EXE Header is 0x3C and it contains another address i.e. F8. Now, if you check the left hand side in IMAGE_NT_HEADER->Signature, it shows that at RVA F8, we have the IMAGE_NT_SIGNATURE and it means that it is the starting point of our PE Header. The IMAGE_NT_HEADERS is basically a windows struct and more info can be found here. This struct contains 3 things. The MSDN states that the third thing in this struct is IMAGE_OPTIONAL_HEADER which is also another windows struct. The last object in this struct is IMAGE_DATA_DIRECTORY and if you visit the MSDN site for this struct, it states that it contains the Virtual Address (RVAs) of different tables and their size in the struct. So, the simplest way to explain this would be something like this.
RVA + size of that Table + size of address of the Size of Table = RVA of next Table
|RVA||Size of Table||Table Name|
|1000||1500 (size of XYZ struct)||XYZ|
|2504 (1000 + 1500 + 4)||2000 (size of ABC struct)||ABC|
|4508 (2504 + 2000 + 4)||1900 (size of DEF struct)||DEF|
Diagram is for representational purpose only
Great, this is how we calculate the RVAs of tables. Remember that each address will always be of 4 bytes, so we would also need to add that. Now, if we delve into the actual address, here is how it would go. If you go to PEview, in IMAGE_OPTIONAL_HEADER, then the first Table in IMAGE_DATA_DIRECTORY is EXPORT Table and its RVA is 170. It stores the RVA of IMAGE_EXPORT_DIRECTORY i.e. 972E0
The IMAGE_EXPORT_DIRECTORY is responsible for storing all information related to the functions in the DLL. Also, if you check PeView, it contains three important entries.
- RVA 972FC (972E0+0x1C) -> Address Table RVA contains 97308 which is the RVA of EXPORT Address Table (below IMAGE_EXPORT_DIRECTORY)
- RVA 97300 (972E0+0x20)-> Name Pointer Table RVA contains 98C14 which is the RVA of EXPORT Name Pointer Table (below EXPORT Address Table)
- RVA 97304(972E0+0x24)-> Address Table RVA contains 9A520 which is RVA of EXPORT Ordinal Table (below EXPORT Name Pointer Table)
Great! So, let me explain what these tables are.
- Export Address Table contains RVAs which contains the RVA of all symbols/functions in kernel32.dll
- Export Name Pointer Table points to the names(strings) of the symbols/functions in kernel32.dll and the ending Ordinal value of the function
- Export Ordinal Table contains the starting ordinal value of the function (which will always be one less than ending Ordinal value of the function)
So, in order to find the base address of GetComputerNameA, this is what we have to do:
- Find the RVA of Offset to Exe Header (0x3C)
- Find the RVA of Export Table [170-F8(Start of PE Header) = 0x78]
- Find the RVA of IMAGE_EXPORT_DIRECTORY (972E0) in RVA of Export Table
- Find the EXPORT Address Table’s RVA (972E0 + 0x1C = 97308)
- Find the EXPORT Name Pointer Table’s RVA (972E0 + 0x20 = 97300)
- Find the Ending Ordinal Value of GetComputerNameA in EXPORT Name Pointer Table by looping and comparing strings in EXPORT Name Pointer Table (1DF)
- Subtract the Ending Ordinal Value by 1 to get the Starting Ordinal Value, OR loop through Ordinal Table to find the Starting Ordinal Value of GetComputerNameA. (We will use the subtraction method, since that is easier to do in Asm) (1DF – 1 = 1DE)
- Add the EXPORT Address Table’s RVA to (Starting Ordinal Value * Size of Address ) to get the RVA of RVA of GetComputerNameA (97308 + 1DE*4 = 97A80)
- Find the RVA of GetComputerNameA in RVA of RVA of GetComputerNameA (RVA 97A80 contains 19AC0). Add the RVA of GetComputerNameA to the Base Address of Kernel32.dll (19AC0 + 74F50000 = 74F69AC0)
Now, in order to make this easier to understand, I built a diagram specifically for this:
OKAY! GREAT! Now, you might want to loop over the above parts multiple times before going ahead. This can get a bit confusing, but if you understand the concepts properly, it really becomes a piece of cake. Now, let’s proceed to write the whole thing we did above in assembly. We will first put the function name on stack, which we will later use for string comparison:
So, I simply reversed the text GetComputerNameA since we the value on stack goes from last to first (little endianness) and then converted it to hex and split it into 8 bytes each. Now let’s push this on stack:
;;;;#push the function name on stack - GetComputerNameA mov edx, 0x41656d61 ;#ameA in reverse = Aema push edx ;#Push to stack push 0x4e726574 ;#terN in reverse = Nret push 0x75706d6f ;#ompu in reverse = upmo push 0x43746547 ;#GetC in reverse = CteG ;#Find the address of Export table and store it in ebx mov edx, [eax + 0x3c] ;#Data at 0x3c is F8 which is moved to edx register: edx = F8 add edx, eax ;#add F8 to the base address of the kernel32.dll to get the RVA till the PE section: edx = 74F500F8 mov edx, [edx + 0x78] ;#add 0x78(170 - F8) to 74F500F8 to get RVA of Image Export Directory: edx = 972E0 add edx, eax ;#add 972E0 to the base address of the kernel32.dll(74F50000) to get the base address of Image Export Directory: edx = 74FE72E0 ;#Find the address of the Export Name Pointer table and store it in ecx mov ecx, [edx + 0x20] ;#add 74FE72E0 and 0x20(97300 - 972E0) to get the RVA of Export Name Pointer Table: ecx = 98C14 add ecx, eax ;#add 98C14 to the base address of kernel32.dll to get the base address of Export Name Pointer Table: ecx = 74FE8C14 mov [ebp-4], ecx ;#Moved the base address of Export Name Pointer Table to [ebp-4] variable on stack: [ebp-4] = 74FE8C14 ;#Find the address of the Export Address table and store it in edx mov edx, [edx + 0x1c] ;#add 74FE72E0 and 0x1c(972FC - 972E0) to get the RVA of Export Address Table: edx = 97308 add edx, eax ;#add 97308 to the base address of kernel32.dll to get the base address of Export Address Table: edx = 74FE7308
Great! Now, the next step is to usually loop through and compare the string on stack to the strings in Export Name Pointer Table. But before that, let’s consider the point of Export Ordinal Table. If we go to the PEview again, inside Export Ordinal Table, we see that it starts from RVA 9A520 and the ordinal value starts from 0004 i.e. AcquireSRWLockExclusive¸ and it keeps getting incremented by one for each function. So, if we scroll down to GetComputerNameA, the ending ordinal value is 1DF and it shows the starting is 1DE (in hex) = 478 in decimal. So what we can simply do is, run a counter which starts from integer 4 and increment it by one till the string GetComputerNameA is matched in the Export Name Pointer Table[ebx-4]. But there is one major problem here. Everything would be fine if and only if the Ordinal Values were in chronological order. We can see in the Export Ordinal Table that the ordinal value starts from 4, but in Export Address Table, it starts from 1. If you scroll down in the Export Ordinal Table, you will realise that the first three values of Export Address Table (BaseThreadInitThunk, InterlockedPushListSList, Wow64Transition), have been randomly inserted into the Export Ordinal Table at random places, and that is why Export Ordinal Table starts from 0004.
And as you can see, its been randomly inserted after the 47th value in Export Name Pointer Table:
However, these places are fixed throughout the OS. So, what we have to do here is instead of using a separate counter for the Ordinal Value, we will use the same counter which is used to run the loop, and once we find the string, we will simply patch the counter by adding 1 or 2 integers to it:
;;;;;#Find the address of GetComputerNameA in kernel32.dll findproc: xor ecx, ecx ;#empty ecx register for string comparison counter mov esi, esp ;#move GetComputerNameA from stack to esi register mov edi, [ebp-4] ;#move the base address of Export Name Pointer Table to edi: edi = 74FE8C14 mov edi, [edi + ebx*4] ;#base address of Export Name Pointer Table + Ordinal Value * 4: edi = RVA of the Function's Name(string) in Export Name Pointer Table add edi, eax ;#add RVA of Name of Function(string) to base address of kernel32.dll to get the Name of the Function in Strings add cx, 8 ;#move string length of GetComputerNameA to the cx register: len(GetComputerNameA) = 16 bytes = 8 WORD repe cmpsw ;#compare the number of words in cx register(8) in left to right order from edi register and esi register. Stores the output in ZF Flag jz findaddr ;#Jump to findaddr label(break loop) if ZF Flag is 1(TRUE), else continue inc ebx ;#increase the counter to calculate the ordinal value loop findproc ;#loop findproc label findaddr:
Now, if we run this in x32dbg, this is what we see when the loop ends:
The EBX value which was used to run the loop is 1DC instead of 1DE. So, we will have to patch the counter by adding 2, so that it becomes 1DE and then we can proceed to extract the address from it:
findaddr: add ebx, 2 ;#patching the EBX counter to change 1DC to 1DE mov edi, [edx + ebx*4] ;#74FE7308 + 1DE*4: edi = 19AC0 add eax, edi ;#add base address of kerneldll32 to above address to get GetComputerNameA address: eax = 74F69AC0
So, finally if you run now, you should see the function name in your EAX register:
NOTE: Unfortunately, the address of my kernel32.dll changed from 74F50000 to 76340000 when building this tutorial. So, my final address should be 74F50000 + 19AC0 =74F69AC0, but instead it became 76340000 + 19AC0 = 76359AC0
Great, now that we have the address of GetComputerNameA, all this is left is to allocate buffer for the parameters of this function. GetComputerNameA accepts 2 parameters:
- Pointer to Buffer with length of MAX_COMPUTERNAME_LENGTH + 1 = 15 + 1 = 16 bytes
- Pointer to a DWORD with the size of our buffer above.
So, all we need to do is allocate 16 bytes on stack, move the DWORD into a register, push the pointer to this buffer on stack and then push the pointer on the register and then call the address that we stored above in EAX register:
;;;;#Push parameters to stack - GetComputerNameA(LPSTR lpBuffer, LPDWORD nSize) sub esp, 16 ;#allocate space for LPSTR lpBuffer = MAX_COMPUTERNAME_LENGTH + 1 = 15 + 1 = 16 bytes mov esi, esp ;#move the stack pointer to lpBuffer in esi add ecx, 16 ;#since ecx was xor'd above, its empty and we can use it to store the value 16 push ecx ;#push DWORD nSize on stack push esp ;#push the pointer to nSize (LPDWORD nSize) on stack push esi ;#push the pointer to lpBuffer on stack call eax ;#Call the function GetComputerNameA
Once, the above is called, it should put your hostname as a string in ESI register which was used as a buffer above:
I think I will stop here with this part, since this could be too much to understand at one point. In the next part, I will be walking through writing null free shellcode and different ways to minimise the shellcode.