在以前的CTF比赛中,大多使用Ubuntu系列来进行题目的部署,大家也比较习惯用LD_PRELOAD来加载远程libc。而近期的CTF比赛当中,经常出现一些非Ubuntu发行版的libc,例如Debian, CentOS, Arch等等。直接在Ubuntu下就不能用LD_PRELOAD来加载了。

SO Hell

由于不同libc发行版和版本号之间可能ABI不同,不同的loader(ld.so)和libc(libc.so.6)如果混用可能无法正常加载。就算同样是Ubuntu系统,不同libc版本之间也不一定能够使用LD_PRELOAD来进行加载。一般来说,我会使用下载对应版本的ld或者直接使用docker来解决libc加载的问题。

识别libc版本

可以直接尝试运行libc,./libc.so.6,可以得到libc相关的信息

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu4) stable release version 2.23, by Roland McGrath et al.

也可以直接strings libc并查找GNU C Library来寻找

用相应版本的ld启动

可以直接google寻找对应版本的libc相关的rpm, deb包等等,然后不需要安装直接解包,提取其中的ld-2.x.so,其他的可以不管。由于ld本身是一个自举的library,可以直接启动并加载对应的libc

1
LD_PRELOAD=./libc.so.6 ./ld-2.28.so ./chal

在这样启动的情况下,ld将被作为一个PIE的程序先被系统的loader加载到对应位置上,而chal则相当于作为一个库加载到地址空间中,实际的地址空间分布将会和直接加载chal有区别。

使用docker解决依赖

docker本身出现的目的之一就是解决应用依赖相关的问题,由于container实际上与host共用同一内核,而且在container中的进程虽然处于自己的namespace中,但在host上依然能够看到对应的进程,这就意味着我们可以使用container启动challenge,并在host上用gdb attach. 这样既解决了库依赖的问题,又不需要在container内部再装工具。

以Hack.lu 2018为例,Pwnable大多使用了Arch进行部署,libc版本很高(2.28),导致Ubuntu无法加载。先pull一个archlinux的镜像。

1
docker pull base/archlinux

如果没有其他依赖的话,可以直接以当前用户启动

1
2
3
4
5
6
7
8
sudo docker run -i --rm \
-h chal \
--name=chal \
-v $(pwd):/chal \
--user $(id -u):$(id -g) \
--workdir /chal \
base/archlinux \

/bin/sh -c "LD_PRELOAD=./libc.so.6 ./chal"

这样在host上可以看到一个已经启动的chal进程,这时就可以与其交互了,当程序退出之后,container会自动销毁.

结合Pwntools

我在之前的blog中介绍了Pwntools的一些调试相关的用法,但现在Pwntools升级之后,实际不需要再用pidof再去获得pid,而是可以直接通过传一个process对象或者进程名的方式进行attach.

1
2
3
4
5
p = process('./chal')
# attach process对象
gdb.attach(p)
# attach 进程名
gdb.attach('chal')

可以将启动docker的脚本保存为一个shell script,然后利用进程名进行attach,同时指定executable file

1
2
p = process('./launch.sh', shell=True)
gdb.attach('chal', exe='./chal')

这样就和平常的调试体验非常相近了

内核相关

但如果实际发行版的内核版本和host不一样,而题目利用方式又恰好和内核相关的话,docker就无能为力了,这种就需要借助虚拟化+gdbserver来复现远程环境了。