Fix Vmmap for Qemu User Targets

注意
本文最后更新于 2024-02-12,文中内容可能已过时。

Fix vmmap for qemu user targets

vmmap 是 pwndbg 插件中提供的一个功能,用于打印程序的内存布局。

在新版本的 pwndbg 中,运行 qemu-user 模拟的程序是无法显示内存布局的,这对我们调试带来的很大的麻烦。经过搜索发现,在旧版本的 pwndbg 插件中是可以正常显示 qemu-user 模拟的内存布局的,通过查看 git log 寻找原因,尝试修复此功能。

寻找问题源头

最后一个可用的 commit 是 Fix early arch detection & the 'Cannot find ELF base!' warning,在下一个 commit elf.py: optimize get_ehdr 中,优化了 get_ehdr 函数的性能,对比如下

image-20230217164124455

get_ehdr 函数的主要功能是提供一个地址,然后返回这个地址范围 ELF 头部的 ELF header (Ehdr) 结构

旧的实现是

  1. 使用 find_elf_magic 函数,从提供的地址开始,一直往前搜索 \x7fELF 的地址,直到搜索到则返回
  2. 如果没有找到 Ehdr 头则返回 None, None

新的实现是

  1. vmmap 中直接查找这个地址所在的区域,再判断开头是否为 \x7fELF,如果一致则记录。
  2. 如果不一致,再去 vmmap 列表中找到与此区域第一个相同的 objfile 的地址,再次判断开头是否为 \x7fELF,如果一致则记录
  3. 如果都没有找到正确的结果,则返回 None, None

新的实现,在一般情况下是没有问题的,但是在 qemu-user 模拟的程序下就会出现问题。

pwndbg.vmmap.find()pwndbg.vmmap.get() 的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def find(address):
    if address is None:
        return None

    address = int(address)

    for page in get():
        if address in page:
            return page

    return explore(address)

def get():
    if not pwndbg.proc.alive:
        return tuple()
    pages = []
    pages.extend(proc_pid_maps())

    if not pages and pwndbg.arch.current in ('i386', 'x86-64') and pwndbg.qemu.is_qemu():
        pages.extend(monitor_info_mem())

    if not pages:
        # If debugee is launched from a symlink the debugee memory maps will be
        # labeled with symlink path while in normal scenario the /proc/pid/maps
        # labels debugee memory maps with real path (after symlinks).
        # This is because the exe path in AUXV (and so `info auxv`) is before
        # following links.
        pages.extend(info_auxv())

        if pages: pages.extend(info_sharedlibrary())
        else:     pages.extend(info_files())

        pages.extend(pwndbg.stack.stacks.values())

    pages.extend(explored_pages)
    pages.extend(custom_pages)
    pages.sort()
    return tuple(pages)

在绝大多数情况下,程序直接从 proc_pid_maps 中获取全部的内存布局,不会执行后续的 if not pages 中的逻辑,而如果程序是在 qemu-user 下模拟的,那么插件也无法从 monitor_info_mem 中获取信息,那么就会调用 info_auxv(),并使用 AUXV(Auxiliary Vector) 的数据信息,来检测程序的内存布局,AUXV 是用来在动态链接器初始工作时没有完善的运行环境时,提供给动态链接器工作的一些提示性的信息,在 GDB 中输入 auxv 可以进行查看。

info_auxv 的代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@pwndbg.memoize.reset_on_exit
def info_auxv(skip_exe=False):
    """
    Extracts the name of the executable from the output of the command
    "info auxv". Note that if the executable path is a symlink,
    it is not dereferenced by `info auxv` and we also don't dereference it.

    Arguments:
        skip_exe(bool): Do not return any mappings that belong to the exe.

    Returns:
        A list of pwndbg.memory.Page objects.
    """
    auxv = pwndbg.auxv.get()

    if not auxv:
        return tuple()

    pages    = []
    exe_name = auxv.AT_EXECFN or 'main.exe'
    entry    = auxv.AT_ENTRY
    base     = auxv.AT_BASE
    vdso     = auxv.AT_SYSINFO_EHDR or auxv.AT_SYSINFO
    phdr     = auxv.AT_PHDR

    if not skip_exe and (entry or phdr):
        pages.extend(pwndbg.elf.map(entry or phdr, exe_name))

    if base:
        pages.extend(pwndbg.elf.map(base, '[linker]'))

    if vdso:
        pages.extend(pwndbg.elf.map(vdso, '[vdso]'))

    return tuple(sorted(pages))

先是从 pwndbg.auxv.get() 中获取 AUXV 信息

1
2
3
@pwndbg.memoize.reset_on_objfile
def get():
    return use_info_auxv() or walk_stack() or AUXV()

AUXV 来源有以下几个部分,依次执行,直到能够成功获取到结构信息

  1. 使用 GDB 功能 info auxv 获取到的 AUXV 信息
  2. 在栈中寻找 AUXV
  3. 查找是否存在 AUXV 结构的符号信息

然后再使用几个结构体的指针来定位内存布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def map(pointer, objfile=''):
    """
    Given a pointer into an ELF module, return a list of all loaded
    sections in the ELF.

    Returns:
        A sorted list of pwndbg.memory.Page objects

    Example:

        >>> pwndbg.elf.load(pwndbg.regs.pc)
        [Page('400000-4ef000 r-xp 0'),
         Page('6ef000-6f0000 r--p ef000'),
         Page('6f0000-6ff000 rw-p f0000')]
        >>> pwndbg.elf.load(0x7ffff77a2000)
        [Page('7ffff75e7000-7ffff77a2000 r-xp 0x1bb000 0'),
         Page('7ffff77a2000-7ffff79a2000 ---p 0x200000 1bb000'),
         Page('7ffff79a2000-7ffff79a6000 r--p 0x4000 1bb000'),
         Page('7ffff79a6000-7ffff79ad000 rw-p 0x7000 1bf000')]
    """
    ei_class, ehdr         = get_ehdr(pointer)
    return map_inner(ei_class, ehdr, objfile)

没错,这里又用了之前所见的函数 get_ehdr,从而导致了 Python 脚本无限递归,最后超出了最大递归限制从而 crash,递归结构如下

1
2
3
4
5
6
7
pwndbg.elf.get_ehdr()
	pwndbg.vmmap.find()
		pwndbg.vmmap.get()
			pwndbg.vmmap.info_auxv()
				pwndbg.elf.map()
					pwndbg.elf.get_ehdr()
						...

修复无限递归

很快,就有师傅提出了这个问题,指出在 qemu-user 模拟下的程序无法正常调试,issues RecursionError when connecting to qemu target,随后在 [coomit Fix #954](https://github.com/pwndbg/pwndbg/commit/d753c0455f79c158a1dbda6bc2127f365896b380) 修复了这个问题。

image-20230217172614653

开发者在这里表示,虽然修复的不是很理想,但是他让程序返回了 Page(0, 0xfffffffff...),让我们来看看实现

image-20230217172757341

image-20230217172816447

简单的来说,这两处修改,让 qemu-user 模拟的程序直接显示了 0-0xffffffff,而不进行其他的内存布局探索,使得在此之后调试 qemu-user 程序都无法正常显示内存布局,不过他对此做了一些解释

image-20230217174323285

For the 3rd case, the problem is related to the QEMU user emulation’s gdbstub being underdeveloped. It lacks a few features related to getting files from the debugged target. I have been working some time ago on a patch to fix that in QEMU, which you can find here: https://lore.kernel.org/all/[email protected]/ and while it works, it need a few improvements and tests. I hope to find some time in the soonish future to work on that and finish that.

简单的来说,我们这里的就是问题 3,在调试 qemu-user 模拟进程时获取内存映射信息出错了,他对此的解释是 qemu-usergdbstub 开发不足导致的问题,无法从远程调试中获取内存布局,他提交了一个补丁来尝试解决这个问题,但在目前看来,似乎这个补丁还没有被应用上,所以关于 qemu-user 模拟的进程,只能显示 0-0xffffffff,等待补丁合并后再做进一步更新。

In theory, for 3rd we could parse the QEMU’s process memory maps and filter them by ourselves. However, this would only work if the QEMU process is run locally and would not work if it hosts its gdbstub from a remote server.

结合这些内容看,似乎在旧的版本中所支持的 qemu-user 并不是完整的内容,只是能够支持在本机运行的 qemu-user 程序,如果是在远程运行的 qemu-user 程序,还需要等待补丁合并后再做进一步的更新。

解决方案

那这个问题,目前终于水落石出了,我总结了两种解决方案来处理这个问题(如果真的不是必须要用,可以再等等)

  1. 使用 git reset --hard 66d5d6cc512e4813fb6246db6ba89b52e5ef8e1c 回退到修改 get_ehdr 前的最后一个版本
  2. 方法如下

替换新版中的 pwndbg\gdblib\elf.py 中的 get_ehdr 函数如下,恢复到 66d5d6cc512e4813fb6246db6ba89b52e5ef8e1c 前的情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def find_elf_magic(pointer, max_pages=1024, search_down=False, ret_addr_anyway=False):
    """Search the nearest page which contains the ELF headers
    by comparing the ELF magic with first 4 bytes.

    Parameter:
        search_down: change the search direction
        to search over the lower address.
        That is, decreasing the page pointer instead of increasing.
            (default: False)
    Returns:
        An integer address of ELF page base
        None if not found within the page limit
    """
    addr = pwndbg.lib.memory.page_align(pointer)
    step = pwndbg.lib.memory.PAGE_SIZE
    if search_down:
        step = -step

    max_addr = pwndbg.gdblib.arch.ptrmask

    for i in range(max_pages):
        # Make sure address within valid range or gdb will raise Overflow exception
        if addr < 0 or addr > max_addr:
            return None

        try:
            data = pwndbg.gdblib.memory.read(addr, 4)
        except gdb.MemoryError:
            return addr if ret_addr_anyway else None

        # Return the address if found ELF header
        if data == b'\x7FELF':
            return addr

        addr += step

    return addr if ret_addr_anyway else None


def get_ehdr(pointer):
    """Returns an ehdr object for the ELF pointer points into.
    """
    # Align down to a page boundary, and scan until we find
    # the ELF header.
    base = pwndbg.lib.memory.page_align(pointer)

    # For non linux ABI, the ELF header may not be found in memory.
    # This will hang the gdb when using the remote gdbserver to scan 1024 pages
    base = find_elf_magic(pointer, search_down=True)
    if base is None:
        if pwndbg.abi.linux:
            print("ERROR: Could not find ELF base!")
        return None, None

    # Determine whether it's 32- or 64-bit
    ei_class = pwndbg.gdblib.memory.byte(base+4)

    # Find out where the section headers start
    Elfhdr   = read(Ehdr, base)
    return ei_class, Elfhdr

替换 pwndbg\gdblib\vmmap.pyget 函数的以下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    # TODO/FIXME: Do we still need it after coredump_maps()?
    # Add tests for other cases and see if this is needed e.g. for QEMU user
    # if not, remove the code below & cleanup other parts of Pwndbg codebase
    if not pages:
        # If debuggee is launched from a symlink the debuggee memory maps will be
        # labeled with symlink path while in normal scenario the /proc/pid/maps
        # labels debuggee memory maps with real path (after symlinks).
        # This is because the exe path in AUXV (and so `info auxv`) is before
        # following links.
        pages.extend(info_auxv())

        if pages:
            pages.extend(info_sharedlibrary())
        else:
            if pwndbg.gdblib.qemu.is_qemu():
                return (pwndbg.lib.memory.Page(0, pwndbg.gdblib.arch.ptrmask, 7, 0, "[qemu]"),)
            pages.extend(info_files())

        pages.extend(pwndbg.gdblib.stack.stacks.values())

变为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    # TODO/FIXME: Do we still need it after coredump_maps()?
    # Add tests for other cases and see if this is needed e.g. for QEMU user
    # if not, remove the code below & cleanup other parts of Pwndbg codebase
    if not pages:
        # If debuggee is launched from a symlink the debuggee memory maps will be
        # labeled with symlink path while in normal scenario the /proc/pid/maps
        # labels debuggee memory maps with real path (after symlinks).
        # This is because the exe path in AUXV (and so `info auxv`) is before
        # following links.
        pages.extend(info_auxv())

        if pages:
            pages.extend(info_sharedlibrary())
        else:
            #if pwndbg.gdblib.qemu.is_qemu():
            #    return (pwndbg.lib.memory.Page(0, pwndbg.gdblib.arch.ptrmask, 7, 0, "[qemu]"),)
            pages.extend(info_files())

        pages.extend(pwndbg.gdblib.stack.stacks.values())

总结

这是个之前在比赛中遇到多次,但一直没有尝试解决的问题,今天正好又一次调试 qemu-user 模拟的程序,但不再是在比赛中,有充足的时间来研究这个问题。最后结合搜索及 git log 来了解了这个问题的来源及目前的情况,同时也加深了自己对常用工具 pwndbg 的理解,这么一个日常的插件,仅仅只是一个小小的 vmmap 功能,就能够牵扯到如此多的知识和问题,这说明在平常使用工具时候,应该多思考背后的实现原理,这样就算遇到问题,也可以快速的定位和解决。

0%