Balsn CTF 2019 pwn 题解

白帽子论坛 5月前 480

非常棒的比赛,有一大堆0解题,其中还有glibc-2.29新的利用方式,以及密码pwn,学到了很多知识。

文件链接:https://github.com/Ex-Origin/ctf-writeups/tree/master/balsn_ctf_2019/pwn 。

 

SecPwn

程序里面有很多的漏洞,但是也有相应的保护机制,我们必须要绕过这些保护机制来达到任意代码执行。

但我们只有7次使用这些漏洞的机会,每个周期将关闭一个文件描述符,直到关闭0,也就意味着socket被彻底断开了。

下面是我的方法:

  • 利用格式字符串的漏洞来泄漏 libc.so.6 的地址。
  • 使用 Secure write 漏洞从 libc.so.6 的got表中泄漏出 ld-linux-x86-64.so.2 的地址。

由于没有 Secure write 的限制,所以我们可以泄漏任何地址。

刚开始,我以为libc_addr和ld_addr的偏移量是固定的,然后我使用固定的值可以打通本地,但是服务器却不行,原因就是偏移量根本不是固定的。

  • 利用 Secure write 漏洞从 ld-linux-x86-64.so 中泄漏 image base address 。因为 link_map 会存储一些关于finit和init的地址信息。
  • 使用 bss overflow 漏洞来存储我们的布局,为了后面进行SROP和ROP。
  • 利用 Secure read 漏洞劫持程序流。

我们不能使用 Secure read 来直接修改 libc 、堆和栈,其限制如下。


附件当中已经给了我们 ld-linux-x86-64.so.2, 所以我们能在执行 fini 操作是劫持程序流。

刚开始,我劫持的是如下的代码:


我直接把函数指针修改为 system 函数,由于它的第一个参数是 link_map, 所以我们可以利用 Secure read 来写 sh 到 link_map->addr ,这样程序就是执行 system(“sh”)。

但是同时程序也没有交互性了,即使起了shell也再也不能使用了。

然后我继续寻找可以劫持程序流的地址,最后我找到了一个更好的地址。


这个函数指针原本是没有参数的,但是 rdi寄存器 恰好残留了 .bss 的指针,这就意味着我们能控制其第一个参数。

  • 使用 setcontext 函数进行 SROP 劫持 rsp 寄存器,然后进行 ROP 来运行 shellcode。我们可以用反向 shellcode 来重建程序的交互性。

脚本:


 

KrazyNote

又是一道没有上锁的 kernel pwn,开始时我确实被其代码优化给绕晕了,后来才慢慢的梳理出其功能来,其实就是在简单的模仿 heap 题。

思路

在 edit 时,利用缺页中断句柄函数使得其暂停,然后重置其heap 的布局,使得恰好能 edit 其结构的size,这样我们就能控制整个 heap。

但是,这只能让我们能拥有任意读写权限,我们并不能知道其 cred 地址在哪。

这里还是要感谢 r3kapig 公开的exp,这里我简述一下劫持 cred 的原理。

task_struct结构是控制线程、进程(线程、进程都是用该结构体)的结构体,而其名字记录在task_struct->comm中,下面是 task_struct 的定义(不同内核的 task_struct 的定义可能不同)。

截取自: linux-5.1.9/include/linux/sched.h


可以看到其名字上面恰好有两个指针指向了 cred。

截取自:linux-5.1.9/include/linux/cred.h


具体思路就是利用 prctl 设置线程名,也就是task_struct->comm,然后在利用 任意读写漏洞查找我们设置的线程名,这样就能泄露出 cred 地址,然后利用任意写漏洞修改 uid 就能完成提权。

代码

这里是主要代码逻辑,其他代码可以在文件链接中查看。


 

securenote

靶机环境是 glibc-2.27 。

题目难度非常的高,毕竟是0解题。

off by one 漏洞


create_note函数用strcpy函数,没有限制长度,直接溢出到下一个chunk的size。

思路

这里用的是题目作者的思路,核心思想就是:

  • 没必要深挖AES的漏洞
  • AES的CRT模式仅仅是和一个随机数异或出的加密字节流而已
  • 我们能恢复加密的字节流通过 明文和密文 进行对照,因为他们仅仅是异或了而已
  • 如果计数器相同,则加密后的数据流任然相同

核心步骤就是通过strcpy修改到top_chunk的size的低字节,这并不会造成任何错误,但是如果我们此时再malloc一个chunk的话,原先被修改的size就会刷新,导致show_note的时候并不能正常停止,这样我们就能泄露出密文出来。

由于我们知道top_chunk的部分,以及我们申请的部分的内容(如果仅仅申请1个字节的的话,那么剩余的0x17个字节就是0x00),这样通过密文和明文,就能计算出其用来异或的字节流了,之后便能利用该字节流来完成信息泄露。

由于其他字节和0异或仍然是其本身,所以我们能利用该技巧直接获得异或字节流,在结合show输出的异或后的字节流,我们就能还原其异或前的字节流,也就是在内存中存储的字节流(加密后的字节流)。

加密后的字节流再和加密前的字节流进行异或就能得到异或字节流,这样我们就能直接利用异或字节流来控制加密后的字节流。关键点在于counter要一致,异或字节流才会相同。


上面代码中delete(3);create(3, '')的作用就是重置counter,每次重置后counter的值就是一个固定的值。这样加密后的内容就会是'g' * 0x18 + p64(0x41),我们就能控制其内存。

具体步骤

  • 该top_chunk的size,在申请一个chunk,导致show_note时可以溢出,得到异或字节流,至于为什么一开始就能得到异或字节流这个问题,其实刚开始的时候只要让内存大部分都是0x00字节,这样就能获得直接异或字节流,当然其不是所有都是,还有一些非0字节内存我们没法控制,所以我将其称为部分异或字节流(partial_xor_stream)。
  • 利用 partial_xor_stream 泄露出heap地址,这个可做可不做,后面不会用到heap地址,其原理就是用 partial_xor_stream 和 show_note 出的解密后的字节流进行异或,就能知道其内存的实际情况。
  • off by one 改 size ,导致 chunk overlap ,其实并不能直接修改size,而是爆破,我们只能查看其 size 的实际内存 是否已经是我们需要的值,概率是1/256。我们只需要不停的爆破就行。
  • 之后便是 free 掉可控chunk,让其 fd 上留下 heap 指针,然后利用上一步的做法修改fd,因为我们不能直接爆破使得 fd 为任意地址,我们只能使用1337次,这么少的次数不允许我们这么做。但是我们可以让其tcache成链,留下heap指针,爆破低字节使其指向conunter即可,但是这样会将chunk的size部分摧毁掉,反正tcache不检查size。
  • 劫持counter,使我们后面能获得一个固定的counter值,为了实现写内存的功能
  • large bin 泄露 libc 地址
  • 劫持hook,拿shell

脚本

由于受到使用次数限制,成功的概率大约为1/16


 

PlainText

这题也真心不错,这里引出了 glibc-2.29 off by one 的全新绕过方法,比赛时仅有RPISEC战队做出来了,赛后我询问了该战队思路,并对其完成复现。

源程序下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/balsn_ctf_2019/pwn/PlainNote 。

致谢

首先,非常感谢RPISEC战队的Jack Dates所提供的思路,没有这个思路恐怕我还在思考怎么绕过prevsize check

Jack Dates

漏洞

明显的 off off one,难点在于环境是glibc-2.29,由于其增加了新的检查,原先的方法都将失效。


主要失效原因是:glibc 在 unlink 的关键点都加上了 prevsize check,而我们根本无法直接修改正常chunk的size,导致想要 unlink 变得几乎不可能。


思路

正如 Jack Dates 所提供的思路,我们不需要绞尽脑汁的去思考如何绕过 prevsize check,我们只需要利用 large bin 的残留指针再结合堆的恰当布局,则能构造出一个fake chunk,后面我将其称作fake_chunk_B

主要原理就是利用残余在 large bin 上的 fd_nextsize / bk_nextsize 指针。首先,我们拿回 large bin,后面我将其称作chunk_A,而 fake_chunk_B 就是 chunk_A + 0x10,在chunk_A 的 bk 位置上写好size,fd先不管,然后部分覆盖chunk_A 的 fd_nextsize 到一个我们可以控制其 bk 的 chunk上(比如从 small bin 或者 unsorted bin 中拿出的chunk,如果其bin中有多个chunk的话,那么拿出来的chunk的bk上必定残留了heap指针,我们可以通过部分覆盖使其指向 fake chunk,以便绕过unlink 检查),由于 chunk_A 的 bk_nextsize 我们并没对其修改,所以其指向的是 chunk_A 本身,为了绕过 unlink 检查( p->fd->bk == p && p->bk->fd == p),我们需要将这个该 fake_chunk_B 的 bk 指向其本身,也就是 chunk_A 的 fd 指向chunk_A + 0x10 并且不能修改已经保存好的其他数据,原本我们可以利用 tcache 的链表特性来完成这一操作,奈何 glibc-2.29的tcache会对 bk 也进行修改,那么则会直接改掉 fake_chunk_B 的 szie,导致unlink失败,但是我们任然可以利用 fastbin 的链表特性来完成这一操作,在chunk_A上写好heap地址后,在进行部分覆盖使其指向chunk_A + 0x10,则这样就能绕过 glibc-2.29 的检查。

由于最后一个字节总是有’’填充,所以我们需要爆破0x..........00..(点为任意十六进制)这样的heap地址。综上所诉该攻击方式的概率是 1/16

沙箱绕过


由于沙箱是白名单的形式,我们只能利用特定的系统的调用的来拿flag,而且printfputs这类的函数都不能使用,还有setcontext函数也并不能正常使用,因为其中使用了sys_rt_sigprocmask

setcontext函数汇编如下:


原本在 glibc-2.27 的话,参数直接是rdi,而不会像这里这样转换到rdx,导致不可以直接利用。

通过仔细观察gadgets,找到了一条非常好用的 gadget:mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

我们可以利用该 gadget 修改 rdx 的值,然后在配合 setcontext 进行 SROP 劫持rsp到heap上,然后在进行ROP将flag读出即可。

脚本




上一篇:Truegaze:一款针对AndroidiOS应用源码的静态分析工具
下一篇:揭密无文件勒索病毒攻击,思考网络安全新威胁
最新回复 (0)
返回
发帖
免责声明:本站部分资源来源于网络,如有侵权请发邮件(bmz@baimaozi.cn)告知我们,我们将会在24小时内处理。