
前言
万事开头难……准备开始学习堆利用了,不过发现想要在 Arch Linux 上获得预期的源码级调试体验有点有点费劲,没有像 ubuntu 那样的 libc6-dbg
软件包,试过配置 debuginfod
服务,也没有解决问题(很爆炸,等我把这个项目撸完后才发现,原来用 ubuntu 的 debuginfod 地址就行了,不能用 arch 的……)。glibc-all-in-one 下载下来的 glibc 动态链接库默认也是 stripped 的,虽然它提供了 build 脚本,但那是针对 ubuntu 开发的,Arch 上使用会出现很多问题(比如我编译 glibc 2.23 就没成功)。
有关在宿主机上编译低版本 glibc 的问题
学习堆利用难免要自己写点程序,使用对应版本的 glibc 编译。起初我是用 glibc-all-in-one 下载对应 glibc 共享库,然后编译的时候通过 -Wl,--rpath="$GLIBC_PATH/lib"
和 -Wl,--dynamic-linker="$GLIBC_PATH/lib/ld-linux-x86-64.so.2"
指定使用对应的 loader 和 libc. 但运行却遇到 Version 'GLIBC_2.34' not found
问题,程序跑不起来,以致每次为了编译个程序还得启动个 docker,太麻烦了。
通过 strings
看了一下,编译出来的程序有 __libc_start_main@GLIBC_2.34
,使用了高版本 glibc, 其它函数倒是正确使用了低版本 glibc, 比如 malloc@GLIBC_2.2.5
. 我本来以为是宿主机 gcc 版本太高,而我需要的 glibc 是 2.23, 可能之间相差太大了,所以不兼容。但是后来才知道,这和 gcc 毛关系都没有,而是因为 gcc 会从系统默认的 libc 动态链接库路径获得各种共享对象文件,虽然我编译的时候设置了 loader 和查找 libc 的路径,但是其它库文件还是会从默认路径去取。导致一部分是正确链接了低版本库,一部分没找到的就链接了系统默认的高版本库。
总之期间走了不少弯路,花了两天去折腾怎么安装低版本 gcc,因为 Arch 是滚动更新,所有软件都保持在最新版本,bro 想弄个低版本 gcc 太不容易了。AUR 上提供的 gcc 包安装编译各种问题层出不穷。本以为看到 AUR 是看到了希望,现实却让人大失所望……
因为路线错误,一心以为换个低版本 gcc 就能解决问题,所以我还是坚持研究出来了怎么在 Arch 上编译低版本 gcc,学到了如何写 PKGBUILD、如何使用 Arch build system 和如何使用 chroot 等……虽然折腾期间学到了不少新知识,但很不幸,当我兴奋地将历经九九八十一难编译出来的低版本 gcc 安装到机子上后……编译!运行!草!为毛还是 Version 'GLIBC_2.34' not found
!感觉受到了致命打击……研究了两天的成果并没有软用,但至少也说明了和 gcc 版本没关系,而是另有所因……
最终,在我的严厉拷问(软磨硬泡)下,AI 给出了一个我从未见过的 -B
编译选项。我也是非常渴望解决问题啊,直接就拿来测试了。再编译,运行……?居然成功了!之后我 gcc --help
查了一下这个选项的含义,发现对其的描述是 -B <directory> Add <directory> to the compiler's search paths.
. 好家伙,搞了半天编译的时候只设置 loader 和 libc 路径并没有用啊,还得设置一个默认搜索路径……不过我也曾试过用 -I
和 -L
设置库路径,却没成功,可能是编译不只是用到了库,还有别的东西?
至此,直接在宿主机下编译使用低版本 glibc 程序的问题,终于 tmd 解决了!
这里必须要吐槽一下,即便是 GPT-5 对于这些问题,知识面也不是很足啊,我天天问,各种角度问,它就是给不出正确的解决方案,老是搁那儿一本正经地胡说八道……
gdb 调试没有符号信息
解决了使用宿主机编译低版本 glibc 程序的问题,接下来就该解决一下没有 debug symbols 的问题了。glibc-all-in-one 下载的 glibc 都是 stripped 的,虽然里面提供了 .debug
目录,保存了调试信息,但当我将这写调试信息加载到 gdb 中后,gdb 的 heap 命令就被破坏了。原因可能是直接加载调试信息并不会自动设置 libc 基地址,加载出来的都是各种符号在 libc 里面的偏移。而 heap 指令又依赖 glibc 信息和各种结构体信息,比如 mp_
什么的……因此如果只是加载了偏移地址,而没有设置基地址,那自然会破坏这些指令的配置。这块儿我也研究了半天,试过加载的时候自动设置基地址,不过好像没有这样的指令,也试过根据 heap-config
所需的信息手动计算物理地址修复 heap 指令的使用,但是既麻烦,也没有成功……
让人心态爆炸的是,等我撸完了这个项目后才发现 glibc-all-in-one 下载下来的共享对象文件里面的 .debug
目录,包含的并不是单纯的调试信息文件,而是 tmd 没 strip 的共享对象文件……为此我之前还特地写了一个 gdb 插件 load-symbols,迭代加载指定目录下的 debug 信息……只能说当时被别人的博客误导了,然后自己也没注意看,眼睛瞎了,隔了那么就才发现……要不是在群里和其他师傅们交流,我可能到现在都没发现也说不准……不过没事,手动 patchelf 指定它的路径写起来也麻烦,更重要的是,它虽然也提供了没 strip 的 libc,但这只能解决已有二进制文件的运行问题,如果想在宿主机上自己编译使用低版本 libc 的程序就不行了,还是需要有编译好的版本。
无奈,摆在我面前的是两条路:
- 调试的时候手动计算各种符号的物理地址,下断点
- 使用别人配置好的虚拟机环境,或者自己整一个
老实说两个方法都很麻烦,我一个也不想用……我只是想本地能直接使用 b *_int_free
这样的指令而已,为啥那么费劲呀……
被逼无奈,当时是准备搭一个 Docker 调试环境的,试了下 pwndocker,但是发现里面的工具版本都比较老了,于是强迫症+完美主义的我想自己从头开始构建一个 Docker Pwn 环境。最后确实是搭好了,但是我一次都没用过……心理那叫一个难受啊,哈哈哈,我本来就用的 Linux, 只想在本地解决所有问题,而不是每次要做点什么的时候还必须放到虚拟机里面去做,那多麻烦,多奇怪啊,加之我平时虚拟机用的就少,还得学习一下它的用法,感觉学习成本太高了,我只是想开开心心地直接开始做事。还有一个原因就是虚拟机环境和我本地环境配置有很大出入,所以用起来一点也不舒服,而我又懒得去配,让它和我本地统一……懒成这样是不是没救了?
Dynabox
嗯……上面就是创建这个项目的原因了,虽然这个项目底层本质上还是用虚拟机,但这或许是最好的解决本地不能编译的问题的方法了。不过我的脚本已经将虚拟机的操作完全封装好了,不需要学习任何有关 Docker 的使用,所以还是很方便滴。
项目地址下面给出了,感兴趣的师傅可以去试试看,能给个 star 的话最好不过了哈哈。
至于这个工具怎么用我就不在这里赘述了,请师傅们移步项目 README. 本来写这篇博客也就是为了随便记录一下折腾的过程,虽然不知道写这个有啥用,但就是想写哈哈哈。
后期打算用其它语言重构一下,因为考虑到后面子命令越来越多,shell 脚本可能会长到难以维护。正好也能锻炼一下开发能力吧,因为平时自己写代码的时间太少了。
比较可惜的是,也是在我自己撸完这个项目后才发现的,有个叫 pwninit 的项目已经实现了和我这个项目类似的功能,不过他的项目并没有解决本地编译低版本 glibc 程序的问题,所以我这个项目还是有点不一样的东西的 xD