Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEB module list structure incorrect #1126

Closed
jeremybeaume opened this issue Apr 7, 2022 · 3 comments
Closed

PEB module list structure incorrect #1126

jeremybeaume opened this issue Apr 7, 2022 · 3 comments

Comments

@jeremybeaume
Copy link

Hi, I recently came across a bug in Qiling PEB structure implementation, while working on a packed binary.
I'm opening this ticket for the sole purpose of referencing it in the incoming PR :)

Describe the bug
A sample moving through the module list in the PEB (PEB.Ldr.InMemoryOrderModuleList) listing the loaded dll fails because the PEB_LDR_DATA is not initalized correctly (the offsets for the LIST_ENTRY are incorrects, they don't point back on themselves, resulting on a bad "end of loop" condition).

Sample Code

Here are the steps to reproduce the bug. I crafted a binary walking through the PEB and printing the module list and related informations:

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

int main()
{
    PEB *PEB_ptr = NULL;
    __asm__(
        "mov %[PEB_ptr], fs:[0x30];"
        : [PEB_ptr] "=r"(PEB_ptr) // output
        :
        :);

    PEB_LDR_DATA *peb_ldr_data = PEB_ptr->Ldr;
    LIST_ENTRY *list_head = &(peb_ldr_data->InMemoryOrderModuleList);

    // goes through the linked list to find kernel32.dll
    // stops when return to header element (the list head is linked to the tail)
    for (LIST_ENTRY *list_entry = list_head->Flink; list_entry != list_head; list_entry = list_entry->Flink)
    {
        LDR_DATA_TABLE_ENTRY *ldr_entry = (LDR_DATA_TABLE_ENTRY *)((char *)list_entry - sizeof(LIST_ENTRY));
        WCHAR *name = ldr_entry->FullDllName.Buffer;
        printf("ldr_entry at %08x, name at 0x%08x, base=0x%08x, str=", (unsigned int)ldr_entry, (unsigned int)name, ldr_entry->DllBase);
        char *c = (char *)name;
        while (*c != 0)
        {
            printf(c); // trick to print the widestring
            c += 2;
        }
        printf(" \n");
    }
}

You should compile it with this line : i686-w64-mingw32-gcc test.c -o test.exe -masm=intel.

Running the sample with the following code :

from qiling import *
from qiling.const import *

ql = Qiling(["./test.exe"], "./rootfs/x86_windows/", verbose=QL_VERBOSE.OFF)
ql.run()

Gives this error:

ldr_entry at 0500020c, name at 0x05000280, base=0x00400000, str=C:\Windows\System32\test.exe 
ldr_entry at 050002d0, name at 0x05000344, base=0x4b280000, str=C:\Windows\System32\ntdll.dll 
ldr_entry at 05000398, name at 0x0500040c, base=0x6b800000, str=C:\Windows\System32\kernel32.dll 
ldr_entry at 050004cf, name at 0x05000543, base=0x10100000, str=C:\Windows\System32\msvcrt.dll 
ldr_entry at 000067fc, name at 0x00000000, base=0x050004df, str=[x] 	CPU Context:
[x] 	ah	: 0x0
[x] 	al	: 0x0
[x] 	ch	: 0x4
[...]
[x] 	Disassembly:
[x] 	PC = 0x004015ce (./test.exe + 0x15ce)

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    ql.run()
  File "/home/user/qiling-PR/qiling/core.py", line 587, in run
    self.os.run()
  File "/home/user/qiling-PR/qiling/os/windows/windows.py", line 188, in run
    self.ql.emu_start(self.ql.loader.entry_point, self.exit_point, self.ql.timeout, self.ql.count)
  File "/home/user/qiling-PR/qiling/core.py", line 720, in emu_start
    self.uc.emu_start(begin, end, timeout, count)
  File "/home/user/.local/lib/python3.8/site-packages/unicorn/unicorn.py", line 523, in emu_start
    raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)

It fails, because when cycling through the LDR_DATA_TABLE_ENTRY, it doesn't comes back to the PEB_LDR_DATA.InMemoryOrderModuleList, as the offset are incorrects.

I edited qiling/loadeer/pe.py, in init_ldr_data to show the structure content at the of the function:

for i in range(0, ldr_size, self.ql.arch.pointersize):
    print(f"{hex(ldr_addr + i)} : {ldr_data.bytes()[i:i+self.ql.arch.pointersize].hex()}")

Which shows :

0x67f4 : 00000000
0x67f8 : 00000000
0x67fc : 00000000
0x6800 : fc670000
0x6804 : fc670000
0x6808 : 04680000
0x680c : 04680000
0x6810 : 0c680000
0x6814 : 0c680000
0x6818 : 00000000
0x681c : 00000000
0x6820 : 00000000

The list entries (at 0x6800 and after) don't point on themselves, which is causing the end condition of walkin through the list (back at the origin) to never happen, and ends up causing a segfault when reading the dll name (null pointer I'm guessing).

Expected behavior

The list entries should point on themselves :

def init_ldr_data(self):
        ldr_addr = self.structure_last_addr
        ldr_size = len(LdrData(self.ql).bytes())
        ldr_data = LdrData(
            self.ql,
            base=ldr_addr,
            in_load_order_module_list={
                'Flink': ldr_addr + 8 + self.ql.arch.pointersize,
                'Blink': ldr_addr + 8 + self.ql.arch.pointersize
            },
            in_memory_order_module_list={
                'Flink': ldr_addr + 8 + 3 * self.ql.arch.pointersize,
                'Blink': ldr_addr + 8 + 3 * self.ql.arch.pointersize
            },
            in_initialization_order_module_list={
                'Flink': ldr_addr + 8 + 3 * self.ql.arch.pointersize,
                'Blink': ldr_addr + 8 + 3 * self.ql.arch.pointersize
            }
        )
        self.ql.mem.write(ldr_addr, ldr_data.bytes())
        self.structure_last_addr += ldr_size
        self.LDR = self.ql.LDR = ldr_data

Which gives

0x67f4 : 00000000
0x67f8 : 00000000
0x67fc : 00000000
0x6800 : 00680000
0x6804 : 00680000
0x6808 : 08680000
0x680c : 08680000
0x6810 : 10680000
0x6814 : 10680000
0x6818 : 00000000
0x681c : 00000000
0x6820 : 00000000

Note that the list now point on themselves. The offsets are also consistent with qiling/os/windows/structs.py/LdrData.bytes:

        s += self.ql.pack32(self.Length)  # 0x0
        s += self.ql.pack32(self.Initialized)  # 0x4
        s += self.ql.pack(self.SsHandle)  # 0x8
        s += self.ql.pack(self.InLoadOrderModuleList['Flink'])  # 0x0c
        s += self.ql.pack(self.InLoadOrderModuleList['Blink'])
        s += self.ql.pack(self.InMemoryOrderModuleList['Flink'])  # 0x14
        s += self.ql.pack(self.InMemoryOrderModuleList['Blink'])

And that solves the issue (no more segfault).

jeremybeaume pushed a commit to jeremybeaume/qiling that referenced this issue Apr 7, 2022
@elicn
Copy link
Member

elicn commented Apr 7, 2022

Hi and thanks for the detailed description.
Since you mentioned you are working on a patch, I wanted to mention that there is a big PR pending (#1118) with a lot of changes and fixes to the PE loader, including many Windows structures. Could you please check whether this problem reproduces with that new code? (please note that DLL loading time may increase due to other changes in that PR).

@jeremybeaume
Copy link
Author

Oh, I actually have another issue with DLL loading, maybe it's already fixed ! I'll check with the mentionned PR, and i'll come back to you :)

@jeremybeaume
Copy link
Author

It is indeed solved with the PR #1118, I'll close the issue, and wait for the PR to be merged, thank you for pointing this out !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants