Shellcode: finding the base address of kernel32 in Windows 7

If you’ve coded shellcode before, you know that the code often needs to find out the base address address where kernel32.dll is loaded in memory. Most publicly available code expects the second entry in the “InitializationOrder” list to be kernel32. Unfortunately, it seems that this is not the case in the public Windows 7 beta.

I’ve create a solution to this problem that should be able to find kernel32.dll on all versions of Windows with minimal code size increase. It works by walking the “InInitializationOrder” list mentioned above and checking the length of the name of the module: the Unicode string “kernel32.dll” has a terminating 0 as the 12th character. From my (limited) testing, it seems that scanning for a 0 as the 24th byte in the name allows the code to find kernel32.dll correctly.

More details can be found here.

The code:

    XOR     ECX, ECX                    ; ECX = 0
    MOV     ESI, [FS:ECX + 0x30]        ; ESI = &(PEB) ([FS:0x30])
    MOV     ESI, [ESI + 0x0C]           ; ESI = PEB->Ldr
    MOV     ESI, [ESI + 0x1C]           ; ESI = PEB->Ldr.InInitOrder
next_module:
    MOV     EBP, [ESI + 0x08]           ; EBP = InInitOrder[X].base_address
    MOV     EDI, [ESI + 0x20]           ; EBP = InInitOrder[X].module_name (unicode)
    MOV     ESI, [ESI]                  ; ESI = InInitOrder[X].flink (next module)
    CMP     [EDI + 12*2], CL            ; modulename[12] == 0 ?
    JNE     next_module                 ; No: try next module.
 

NB. See the comments for a problem (and solution) on Win2K targets courtesy of aniway.

14 Comments to “Shellcode: finding the base address of kernel32 in Windows 7″

  1. qwerty
    2009/07/25

    why not just scan the whole list? ;)

  2. Vincent
    2009/07/25

    @qwerty: Because it will increase its size ?

  3. qwerty
    2009/07/26

    it would, the following is taken from code on packetstorm.

    http://www.packetstormsecurity.org/shellcode/rev_overlap32.asm

    load_dll:
    lodsd ; Flink
    xchg eax,ebx
    lodsd
    lodsd ; DllBase
    xchg eax,ebx
    push eax ; save

    the above code is 6 bytes, the shellcode scans all modules, but uses either 16 or 32 bit hashes to resolve api.

    it’s just another way to do it that was obviously overlooked ;)

  4. SkyLined
    2009/07/26

    @qwerty: Your suggestion is to not look for kernel32.dll at all, but just scans all functions in all DLLs loaded in the process for a specific function using a hash. While this saves you the bytes for code that finds kernel32.dll, it also means you need to have a very good hashing algorithm and/or “large” hashes to prevent collisions, which waste a lot of bytes.
    My own implementation of a bindshell uses a very simple hashing algorithm and 1 byte hashes. I think you’ll find that the savings made there far outweigh the cost of having to find kernel32.dll.

  5. qwerty
    2009/07/26

    well, this is certainly true, wouldn’t argue with you there.

    i like this kind of code, don’t get me wrong.

    but i’d say at some point in future, a length check might be insufficient, meaning extra code required for extra checks.

    in certain situations, it would be ideal to use 1 byte hashes or even hardcoded addresses but this usually means less stable code.

    next_module:
        MOV     EBP, [ESI + 0x08]           ; EBP = InInitOrder[X].base_address
        MOV     EDI, [ESI + 0x20]           ; EDI = InInitOrder[X].module_name (unicode string)
        MOV     ESI, [ESI]                  ; ESI = InInitOrder[X].flink (next module)
        CMP     [EDI + 12*2], CL            ; modulename[12] == 0 ? strlen("kernel32.dll") == 12
        JNE     next_module                 ; No: try next module.
    

    what happens if it finds ADVAPI32.DLL first? or some other module which happens to be same length as KERNEL32.DLL?

    because of the 1 byte hashes, there can only be 256 possible values, it may resolve the wrong apis and crash.

    this is a problem which might occur in process with large number of DLL loaded, hence why it may be better to have a good hashing algorithm and scan all modules.

    the difference in size is not that big, maybe at most 20 bytes.

  6. SkyLined
    2009/07/26

    @qwerty: There is no version of Windows that will load another module with a 12 character name before kernel32.dll. That may change in future versions of Windows but for now it can be used to find it. This means that the above code will not fail (unless some existing versions of Windows put a bunch of NULLs after the name of “ntdll.dll” rather than non-NULL bytes, but I’ve not seen this).

    Unfortunately, 20 bytes is a lot for a shellcode: it would mean a 10% size increase for mine.

  7. aniway
    2009/11/21

    Thanks for sharing, SkyLined. It’s a creative solution. I have tried it on windows 7, it worked beautifully.

    I’m not sure whether you have tested it on windows 2k. I have to make the following change in order to make it work in my case:

    Original:
    CMP [EDI + 0x18], CL

    New:
    CMP [EDI + 0x18], CX

    As we are checking the termination of UNICODE string, so I think the change is harmless.

    Cheers & regards,

    Aniway

  8. SkyLined
    2009/11/22

    Hi Aniway,

    Thanks for the feedback! I’ve not tested this on Win2K, but I don’t know why it would not work as-is.
    The fix you suggest makes no sense to me, can you explain what the problem is and how this change solves it?

    I designed the code to look for a UNICODE ‘\0′ character in a string that only contains lower-ascii characters coverted to UNICODE. This means that one byte of each character is always 0 and the code need only CMP the other byte to 0 (CL) to find this ‘\0′ character. In this context, I don’t understand why a CMP with CX adds any value and how this could solve any issue.

    Cheers,

    SkyLined

  9. aniway
    2009/11/24

    Hi SkyLined,

    I understand what you are saying;) theoretically, checking one byte should be enough and it saves one byte of space. But here is the whole story:

    I was exploiting a java 0day using a simple calc.exe shellcode from metasploit. The shellcode worked well on Windows 2k and XP, but of course not on Windows 7. So I changed the shellcode as you posted and it worked on Windows 7, but kept failing on Windows 2k after that. I tried a few Win2k VM, same thing everywhere.

    I tried to dig the problem out, but whenever there’s olly attached to the affected application, the shellcode runs successfully!!! So I never see a case that really requires a 2 byte check.

    As that’s the only change I made to the shellcode, so I started to get picky at the code. The only thing I can think out was that the one byte check might not be sufficient. So I changed it to check 2 bytes, and It worked.

    Is it possible that due to unininlized memory or whatever reason, there might happen to be one byte NULL at offset 0×18 for a shorter module name? Or there’s any other reason you can think of?

    Cheers,

    Aniway

  10. SkyLined
    2009/11/25

    I cannot think of any reason why the code would not work on Win2K nor any way how the change you suggest could change the behavior of the code in a way that would fix the issue. I haven’t got a Win2K machine at the ready to test, so I’ll take your word for it.

    Thanks for the feedback!

  11. aniway
    2009/11/27

    Hi Skylined,

    Just let you know that I figured it out.

    As the first module name being checked is ntdll.dll, on win2k, the unicode name and the data following it is as below:


    6e 00 74 00 64 00 6c 00 6c 00 2e 00 64 00 6c 00 ntdll.dl
    6c 00 00 00 42 00 08 00 00 01                   l.B..
                            ^^
    This is the byte we are checking.

    As you can see, if we only check one byte, the code will think ntdll.dll is kernel32.dll. Checking 2 bytes fixes that problem.

    Cheers,

    Aniway

  12. SkyLined
    2009/11/30

    Ah, that makes sense: the code assumes that the “ntdll.dll” string is followed by bytes that do not contain a NULL byte at offset +12 from the start of the string. It appears this is true for all versions of Windows after w2k (though I have not tested them all to confirm this). It seems that win2k happens to have a NULL byte at this offset after the string, causing the code to mistakenly assume it is kernel32.dll. Checking for a WORD NULL at that location solves this. Thanks for the feedback!

  13. Aniway
    2009/11/30

    Cheers:)

Trackbacks & Pingbacks

  1. Exploit writing tutorial part 9 : Introduction to Win32 shellcoding | Peter Van Eeckhoutte's Blog

Leave a Comment

*

*