资讯动态

Pwndbg 中的 Libc 支持实现指南:从 LibcProvider 协议到 glibc/musl 实战

发布时间:2026/9/16 16:47:19 来源:尧图企业网站定制
Pwndbg 中的 Libc 支持实现指南从 LibcProvider 协议到 glibc/musl 实战【免费下载链接】pwndbgExploit Development and Reverse Engineering with GDB LLDB Made Easy项目地址: https://gitcode.com/GitHub_Trending/pw/pwndbg导读本文是 Pwndbg 官方贡献者指南《Implementing Libc support》的深度展开。它围绕 Pwndbg 的 libc 抽象层pwndbg/libc/模块族展开系统讲解libc 支持在 Pwndbg 中的确切含义、LibcProvider协议的全部接口与实现要点、libcinfo命令的输出逻辑以及从源码编译 glibc / musl 并动态/静态链接测试程序的完整步骤。读完本文你将掌握如何为 Pwndbg 新增一个 libc 实现如 bionic、如何在 libc ELF 中寻宝式地挑选长期稳定且具备区分度的符号、以及如何规避 MiniDebugInfo、版本探测缺失等真实陷阱。Libc 支持在 Pwndbg 中的定位C 标准库libc是 C 语言的标准库其他语言也经常通过 libc 来复用操作系统接口实现因此系统中几乎所有动态链接程序都会用到 libc。Pwndbg 对某个 libc 实现的支持由一个pwndbg/libc/libc 名.py文件提供该文件实现了 pwndbg.libc.dispatch.LibcProvider 协议。目前官方支持 GNU C Library (glibc) 与 musl。需要澄清一个重要概念你完全可以自由地在其他 libc 编译的程序上使用 Pwndbg。支持 libc X意味着我们提供了一些针对 libc X 的特性具体表现为libcinfo命令对受支持的 libc 不会返回unknown。例如当前仓库中尽管尚未支持 bionicAndroid 的 libc却已经支持 jemalloc而 mallocngmusl 分配器与 glibc 分配器代码也都正确地依赖pwndbg/libc/musl.py与pwndbg/libc/glibc.py的实现。从源码结构看历史上 Pwndbg 中太多 libc 特有代码被塞进了分配器专属代码里这次抽象的目的正是把 libc 特有功能从分配器代码中剥离让不关心分配器的其他子系统也能复用 libc 特性。当前实现集中在 pwndbg/libc/ 目录dispatch.py—— 定义LibcProvider协议、LibcType枚举与LibcURLs数据结构facade.py—— 门面层负责发现进程中的 libc/ld 映射并分派到具体实现glibc.py、musl.py—— 两个受支持实现unknown.py—— 兜底实现在不清楚 libc 时给出非承诺式答案util.py—— 版本解析、导出符号检查等公共工具facade.py中_libc_implementations: tuple[LibcProvider, ...] (glibc, musl, unknown)这行代码注释明确写着Order is important顺序很重要候选映射会按顺序询问每个实现先声称成功的实现优先胜出因此新实现插入时需要注意顺序。Scaffolding添加一个新 libc 的骨架假设你要添加 bionic 支持官方流程是创建pwndbg/libc/bionic.py文件在其中实现 pwndbg.libc.dispatch.LibcProvider 规定的所有函数将bionic模块加入 pwndbg/libc/facade.py 的_libc_implementations元组实现完所有函数后添加一个与test_musl.py等价的测试文件test_bionic.py。作为开发前提你需要拿到 libc 的源码、知道如何编译它、并知道如何用它与程序做动态/静态链接。一个跨架构提示下面用到的所有 binutils 工具如nm、objcopy、readelf都可以换成 LLVM 同名版本llvm-nm、llvm-objcopy、llvm-readelf因为大多数发行版默认的 binutils 只支持宿主机架构而 LLVM 版本内置了所有架构的支持。关于静态链接的注意事项要稳健地支持静态编译二进制文件的 libc 特有功能非常困难所以这只能算尽力而为best effort。当程序静态链接时facade.py的__get_libc()会把主可执行模块的路径同时当作 libc 与 ld 路径返回见 facade.py但仍会尝试推断 libc 实现。Glossary三个核心概念exported symbol导出符号动态库有一组属于其 API 的函数即可从其他目标文件调用的函数。即使 strip 一个动态库这些符号仍会保留它们被称为导出符号也叫动态符号位于 ELF 的.dynsym节符号名在.dynstr节。导出符号既可以是函数也可以是全局变量。导出它们nm -D libc.so或效果更好readelf --dyn-syms --extra-sym-info libfoo.so如果 libc 是动态链接的这些符号几乎总是存在如果是静态链接的通常就没有。我们期望所有动态库都包含部分导出符号如scanf、exit、printf……因为它们是 C 标准的一部分但有些符号是 libc 特有的例如__freadahead由 musl 和 bionic 提供glibc 没有。在 util.py 中has_exported_symbols()的实现就是检查fscanf是否存在于给定映射中——因为任何 libc 都必须实现fscanf这构成了导出符号存在性的判据。debug info调试信息即编译时加-g得到的那部分内容行号、结构体、函数局部变量等。对 Pwndbg 而言最重要的是类型types。能直接从调试信息中提取struct malloc_chunk这样的类型远比猜结构体布局好原因有二结构可能随版本变化如果 Pwndbg 还没跟上最新 libc 版本或版本检测困难有调试信息的用户仍可无障碍调试使用来自调试信息的类型可以支持发行版之外的分支版本、中间版本和自定义补丁的 libc。如果存在调试信息那么导出符号与内部符号通常也会存在。方便的是调试信息往往可以通过 debuginfod测试需要排除 debuginfod 的干扰验证纯符号探测逻辑。internal symbols内部符号内部符号是非导出非动态符号。直接这样看$ nm libc.so nm: libc.so: no symbols # ^ 这是发行版的默认情况一个例子是 glibc 和 musl 中的__libc_version。实现 LibcProvider 的各个函数动手前请先通读 pwndbg.libc.dispatch 中的 docstring与文档作者保持一致的理解。协议总共定义 8 个方法type、version、has_internal_symbols、has_debug_info、urls、verify_libc_candidate、verify_ld_candidate、libc_same_as_ld。其中type、urls、libc_same_as_ld三个实现起来很 trivial。协议关键契约如下摘自 dispatch.py方法语义与约束type()返回当前激活的 libc 实现类型LibcType枚举version(libc_filepath)以元组返回版本无法恢复时返回(-1, -1)has_internal_symbols(libc_filepath)是否拥有内部库符号必须基于变量而非函数判断以免被 MiniDebugInfo 欺骗has_debug_info()是否拥有结构体类型等调试信息urls(ver)返回可读源码、压缩源码、主页、git 仓库四类 URL实现version()的必须assert ver is not None否则assert ver is Noneverify_libc_candidate(mapping_name)验证给定映射是否是本 libc必须足够精确不能让其他实现给出冲突答案。返回 False 既表示拒绝也表示我不知道verify_ld_candidate(mapping_name)验证给定映射是否是本 libc 的 loader约束同上libc_same_as_ld()libc 与 ld 是否作为同一目标文件加载若返回 Trueverify_ld_candidate必须直接调用verify_libc_candidate协议还要求一个 libc 实现至少要实现verify_libc_candidate与verify_ld_candidate之一另一个可直接返回False。Treasure hunt寻宝式选符号在 libc 的源码仓库中你会大量使用git tag。你要寻找满足某些条件的符号和模式这些条件包括在代码库中存在已久。因为市面上有一大堆针对古老 libc 版本编译的程序我们希望支持它们。对于所有引用 libc 符号的函数你必须注明该符号是在哪个版本加入的、是哪一年、以及该符号在其首个版本中的定义 permalink。例如# The __polevll symbol is an internal symbol in musl. Doesnt exist in bionic nor glibc. # It was added in version v0.8.7 (year 2012). # https://elixir.bootlin.com/musl/v0.8.7/source/src/math/__polevll.c#L63仓库中的实现严格遵守这一约定。例如 glibc.py 对__GI_exit的注释说明它自至少 2.3 版本起存在直到至少 2.42并给出了 glibc-2.3 的 libc-symbols.h 则对__freadaheadv0.9.22012 年引入和/tmp/tmpnam_XXXX字符串v1.1.22014 年加入分别给出源码链接。version版本探测检查你的 libc 是否内嵌版本号最简单的方式是 grepgrep 你编译用的git tag$ strings libc.so.6 | grep 2.42 GLIBC_2.42 glibc 2.42 NPTL 2.42 GNU C Library (GNU libc) stable release version 2.42.可见版本号清晰存在。如果没有你就得发挥创造力了。如果运气好libc 会把版本作为符号提供。对 musl 和 glibc 都是__libc_version。遗憾的是musl 直到 2019 年v1.1.21才开始在构建的 ELF 中提供版本对 glibc 和 musl 而言__libc_version都是内部符号——如果没有内部符号你就得另寻他法恢复版本方法是扫描 libc ELF 的相应节参见pwndbg.libc.glibc._get_version()。来看 glibc.py 的实际实现先尝试lookup_symbol_addr(__libc_version, objfile_endswithlibc_filepath)直接读内存字符串并解析失败则读取.rodata节在其中查找bGNU C Library横幅再用正则rbrelease version (\d)\.(\d)提取主次版本号。此外 glibc.py 还提供了pwndbg.config参数glibc格式如2.31用户手动指定版本时会直接优先返回该值否则回退到_get_version()的自动探测。has_debug_info应该很简单只需找一个自 libc 首个版本就存在、至今仍存在的结构体。glibc 用的是struct malloc_chunk见 glibc.pymusl 用的是struct __ptcb见 musl.py注释说明它自首个版本 0.5.0 起就存在于 bits/pthread.h。两者都通过pwndbg.aglib.typeinfo.load()检测类型是否可加载。has_internal_symbols 与 MiniDebugInfo 陷阱直观上这些符号通常以_或__开头不难找。但有一个重要陷阱MiniDebugInfo。它允许分发一些最小调试信息用于符号化回溯Fedora 就用它分发 musl 等包。这份调试信息放在.gnu_debugdata节中用 LZMA 压缩。可以这样提取其中的符号objcopy --dump-section .gnu_debugdataminidebuginfo.xz libc.so xz -d minidebuginfo.xz readelf --syms minidebuginfo实际中这种东西很容易包含内部函数符号但不会包含内部全局变量。这有点麻烦因为这类情况下has_internal_symbols应返回False而内部全局变量通常对我们非常有用。对大多数 LibcProvider 函数假阴性比假阳性可接受得多假阴性通常只是回退到启发式方法假阳性则可能导致错误推断。因此has_internal_symbols要对抗 MiniDebugInfo必须找一个长期存在、非导出的全局变量文档还建议尽量选冷门的、不太可能出现在程序其他目标文件中的符号——通常是源码里的static全局变量。辅助命令编译带调试信息的 libc 后执行readelf --syms --wide libc.so | awk {print $4, $5, $6, $7, $8} | sort -u all-syms.txt readelf --dyn-syms --wide libc.so | awk {print $4, $5, $6, $7, $8} | sort -u dyn-syms.txt comm -23 all-syms.txt dyn-syms.txt internal-syms.txt grep OBJECT internal-syms.txt internal-vars.txt然后查看internal-vars.txt找一个存在已久的符号。测试该符号在 strip 后不出现可以cp libc.so libc.so.debug strip libc.so仓库中的落地实现可以印证这一策略glibc 选用__libc_version存在于所有版本 glibc 的内部符号见 glibc.pymusl 选用c_messages——一个自首个发布 v0.5.02011就存在的内部全局变量见 musl.py。文档还前瞻性地提到将来若有必要可以把LibcProvider拆成has_internal_function_symbols与has_internal_variable_symbols再让has_internal_symbols在 facade 层定义为两者的与运算但在出现需求之前不要过度复杂化。verify_libc_candidate只有当我们确定时才返回 True。除了参考现有实现没有更好的技巧在.rodata中搜索字符串是已被验证有效的策略。glibc 的实现glibc.py展示了先便宜后昂贵的两段式策略如果存在内部符号直接检查__GI_exit是否在映射中它自至少 2.3 版本存在、其他 libc 没有没有内部符号时读取.rodata检查是否包含bGNU C Library。musl 的实现musl.py则更讲究先检查导出符号场景——如果该映射有导出符号fscanf存在但找不到 musl/bionic 特有、glibc 没有的__freadahead直接判负随后在.rodata中搜索b/tmp/tmpnam_XXXX来自tmpnam.c最后还有一个面向静态链接的兜底——搜索 mallocng 的size_classes数组字节模式_MALLOCNG_SIZE_CLASSES_LE/BE该数组存在于任何调用malloc()的 musl 二进制中适用于 musl v1.2.1见 musl.py。这正呼应了文档静态链接是 best effort的论断静态链接时链接器只包含被引用的代码/数据tmpnam字符串可能缺失必须另找malloc()必然引用的模式。verify_ld_candidate 与 libc_same_as_ld如果你实现了verify_libc_candidate通常不必再实现verify_ld_candidate——除非libc_same_as_ld()返回True此时verify_ld_candidate必须调用verify_libc_candidate。两个现有实现的对照很清晰glibc 中 ld 与 libc 是不同映射verify_ld_candidate直接返回Falselibc_same_as_ld()返回Falseglibc.pymusl 中 ld 与 libc 是同一映射有些发行版叫它 libc有些叫 ld所以verify_ld_candidate直接转发给verify_libc_candidatelibc_same_as_ld()返回Truemusl.py。这在 facade.py 的分派逻辑里很关键当只有 libc 或只有 ld 被验证通过时facade 会依据libc_same_as_ld()决定是把同一路径同时当作两者返回还是退回到候选列表。而__check_candidatesfacade.py中如果 libc 与 ld 被不同实现声称会抛出LibcNotFound冲突异常。从 facade 看完整的识别流水线了解整体流程有助于理解每个函数被调用的时机。facade.py 的__get_libc()被缓存到start与objfile事件收集进程全部模块节跳过主可执行文件按 basename 精确匹配libc.so.6、libc.sold-linux-x86-64.so.2、ld-linux.so、ld-musl-x86_64.so.1得到几乎肯定的候选或用正则^libc6?[-_\.]、ld.*\.so(?:\.\d)?收集可能候选兼容LD_PRELOAD加载的libc-2.36.so、libc6_2.36-0ubuntu4_amd64.so等命名静态链接时把主模块同时作为 libc 与 ld 候选传入按_libc_implementations顺序询问各实现是否声称这些映射没人声称时返回unknown实现完全找不到候选时抛LibcNotFound。公共 APIwhich()、version()、filepath()、loader_filepath()、addr()、loader_addr()、has_exported_symbols()、has_internal_symbols()、has_debug_info()、urls()等都建立在__get_libc()之上。libcinfo 命令支持效果的验收窗口libcinfo命令直接消费 facade API 并打印结果见 libcinfo.py它输出 libc 类型、版本、链接方式、libc/ld 地址与路径、以及三个符号能力标志。对应的集成测试 test_command_libcinfo.py 精确验证了程序加载前libc: unknown运行到main后libc: glibc、libc version: 2.x、has debug info: yes、has internal symbols: yes、has exported symbols: yes且路径包含libc.so.6与ld-linux。而 test_musl.py 验证了 musl 场景下pwndbg.libc.filepath() pwndbg.libc.loader_filepath()、addr() loader_addr() ! 0等 musl 特有行为test_musl_versions.py 则按 Dockerfile.musl-test-libs 解析出的多个 musl 版本 × 动态/静态两种链接方式做参数化断言pwndbg.libc.version()与预期版本完全一致。编译 libcglibc 与 musl 实战编译 libc、以及用 libc 编译程序都可能有些怪癖。每当你在 Pwndbg 中为新 libc 添加支持请把编译说明写进本文档对应位置方便后来贡献者参考。如果你使用clangd作为 C/C LSP 实现且不用完整 IDE通常需要生成compile_commands.json以便clangd实现 Go-To-Definition 等功能。通常用bear完成把make换成bear -- make会在当前目录生成compile_commands.json并被clangd自动消费替代品是compiledb两者各有优劣。这些工具对成功编译并非必需——下文若出现compiledb whatever_actual_command你完全可以直接运行whatever_actual_command。注意用bear --干净编译成功后若做了增量修改不要再对重编译使用bear --否则会覆盖你的compile_commands.json。另外在不同版本间切换 checkout 再编译时上次构建的产物可能干扰你。遇到编译问题nuke 仓库重新拉取常常能救你有时make clean够用有时不够。glibc主要参考自 Stack Overflow官方说明见 glibc Wiki: Testing/Builds。git clone git://sourceware.org/git/glibc.git cd glibc mkdir -p build/install cd build export GLIBC_INSTALL$(pwd)/install echo $GLIBC_INSTALL ../configure --prefix $GLIBC_INSTALL bear -- make -j $(nproc) make install cp compile_commands.json ../.libc 和 ld 位于$GLIBC_INSTALL/lib/。用 glibc 动态编译确保当前 shell 会话中已设置GLIBC_INSTALL[ -n $GLIBC_INSTALL ] gcc \ -L $GLIBC_INSTALL/lib \ -I $GLIBC_INSTALL/include \ -Wl,--rpath$GLIBC_INSTALL/lib \ -Wl,--dynamic-linker$GLIBC_INSTALL/lib/ld-linux-x86-64.so.2 \ -o main \ main.c用ldd main检查一切正常。也可以在 Pwndbg 中打开该二进制运行start、vmmap视需要再运行libcinfo验证识别结果。用 glibc 静态编译官方不支持但勉强能用[ -n $GLIBC_INSTALL ] gcc \ -L $GLIBC_INSTALL/lib \ -I $GLIBC_INSTALL/include \ -Wl,--rpath$GLIBC_INSTALL/lib \ -Wl,--dynamic-linker$GLIBC_INSTALL/lib/ld-linux-x86-64.so.2 \ -static \ -o main \ main.c清理编译产物rm -rf buildmusl官方说明见 musl INSTALL。git clone git://git.musl-libc.org/musl cd musl mkdir -p build/lib export MUSL_INSTALL$(pwd)/build echo $MUSL_INSTALL ./configure --enable-debug --prefix$MUSL_INSTALL --syslibdir$MUSL_INSTALL/lib compiledb make -j $(nproc) make installlibc 和 ld 位于$MUSL_INSTALL/lib/。用 musl 动态编译[ -n $MUSL_INSTALL ] $MUSL_INSTALL/bin/musl-gcc \ -L $MUSL_INSTALL/lib \ -Wl,-rpath$MUSL_INSTALL/lib \ -o main \ main.c一些较老的 musl 版本还要求传--no-pie。用 musl 静态编译[ -n $MUSL_INSTALL ] $MUSL_INSTALL/bin/musl-gcc \ -L $MUSL_INSTALL/lib \ -static \ -o main \ main.c清理编译产物make clean make distclean mv .gitignore ../nya1234 git clean --force mv ../nya1234 .gitignore rm -rf build说明musl 的清理流程先把.gitignore挪出仓库再git clean --force是为了避免git clean删掉.gitignore本身它是被跟踪文件理论上不会但实践中该流程被验证可靠之后再把它挪回来。给贡献者的自检清单把上面的内容浓缩成一份可执行清单通读协议先读 dispatch.py 中每个方法的 docstring建文件创建pwndbg/libc/name.py实现全部 8 个方法注册把模块加入 facade.py 的_libc_implementations注意顺序选符号为has_internal_symbols选一个存在已久、冷门的内部全局变量为verify_libc_candidate优先考虑.rodata字符串特征给每个引用的符号附上引入版本、年份与首个版本的 permalink对抗 MiniDebugInfo用objcopy --dump-section .gnu_debugdata...验证所选变量不会出现在 MiniDebugInfo 中写测试参考 test_musl.py 创建test_name.py覆盖动态/静态两种链接方式并验证libcinfo不再返回unknown补文档把新 libc 的编译与链接指令写进本文档即 docs/contributing/libc-provider.md供后来者查阅。延伸阅读协议与数据结构定义pwndbg/libc/dispatch.py门面层与候选发现逻辑pwndbg/libc/facade.pyglibc 实现版本探测、__GI_exit验证、safe-linking 判定pwndbg/libc/glibc.pymusl 实现c_messages、__freadahead、mallocng size_classes 模式pwndbg/libc/musl.py兜底实现pwndbg/libc/unknown.py公共工具pwndbg/libc/util.pylibcinfo命令pwndbg/commands/libcinfo.py测试用例test_musl.py、test_musl_versions.py、test_command_libcinfo.py【免费下载链接】pwndbgExploit Development and Reverse Engineering with GDB LLDB Made Easy项目地址: https://gitcode.com/GitHub_Trending/pw/pwndbg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价