2020 De1CTF题目都挺难的,包含一个简答的 C++,一个 nday的协议栈溢出漏洞,一个 Android 漏洞 和 一个webpwn。
stl
又是一道C++题目,而且经过这几次发现 C++ 题目,其实还是有一部分相像的。比如
vector
的插入和删除操作。这一部分应该反汇编后都及其类似。所以我们一般不考虑在插入删除除存在漏洞。而更应该考虑 是否存在堆溢出、double free 或 UAF 漏洞。这一部分可以先使用手动测试一下。
程序分析
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
程序总体逻辑是实现了 链表、vector、队列和栈
的四种操作。每种操作都有 增加、删除功能,此外 链表和 vetcor
还有输出功能。这里,主要就是测试了一下,是否有堆溢出漏洞和 double free 还有 UAF
漏洞。发现在处理 vector
时,如果删掉了 第一个 vector
,然后由于 vector
链表没有将 第一个 vector
清空,导致可以 double free
和 UAF
漏洞。
利用分析
那么,这里漏洞就是 vector
的第一个 vector
存在 uaf
漏洞。
这里,可以先利用 UAF
漏洞来泄露 heap
地址。
然后利用 double free
漏洞劫持 tcache_perthread_struct
,并释放到 unsortedbin
中,来泄露地址。
最后再通过 double free
来劫持 free_hook
。
EXP
1 | from pwn import * |
pppd
这是一个真实 CVE 漏洞改编的题目。我们需要先分析一下原漏洞的漏洞点,也顺便为我入门相似的 IOT 漏洞打个基础。
CVE-2020-8597
下面分析主要参考这篇文章。
背景知识
EAP
协议是使用可拓展的身份验证协议的简称,全称 Extensible Authentication Protocol
,是一系列验证方式的集合,设计理念是满足任何链路层的身份验证需求,支持多种链路层认证方式。因为 EAP
协议主要用于认证,因此这个漏洞影响了众多协议,如 ppopen
、pptp
等。
EAP
协议帧格式,如下:
字段 | 占用字节数 | 描述 |
---|---|---|
Code | 1个字节 | 表示EAP帧四种类型:1.Request;2.Response 3.Success;4.Failure |
Identifier | 1个字节 | 用于匹配Request和Response。Identifier的值和系统端口一起单独标识一个认证过程 |
Length | 2个字节 | 表示EAP帧的总长度 |
Data | 0或更多字节 | 表示EAP数据 |
- code 用于标识 eap 协议为请求或者响应包,或者认证成功或者认证失败
- length 字段用于表示 eap 帧 的长度,漏洞产生的原因就是因为对这个字段处理不当造成的。
漏洞分析
可以从此处,看到对漏洞修补时的 commit
。
1 | @@ -1420,7 +1420,7 @@ int len; |
在 eap.c
文件中,可以看到分别对 处理 response
和 request
时的 请求 rhostname
长度做了限定。当 vallen<= len+sizeof(rhostname)
时,程序会执行 else
流程。
而在 else
流程中,我们重点关注如下代码:
1 | BCOPY(inp + vallen, rhostname, len - vallen); |
其中 BCOPY
函数原型如下:
1 |
那么这段代码就是将 inp+vallen
处的字符 赋值给 rhostname
,而 rhostname
最大长度为 256
。而 vallen
表示的是数据包头部的长度,为一个常量。len
表示数据包长度,我们输入的 Data
可以增大该字段。
那么分析一下漏洞触发流程:
- 发送大量字符串,使得
vallen<= len+sizeof(rhostname)
- 执行
else
分支,将inp+vallen
的字符 拷贝给rhostname
,长度为len-vallen
- 只要满足
len-vallen
大于 256,则发生栈溢出漏洞。
利用分析
上面已经将 pppd
的漏洞分析的很清楚,是一个 mips
下的栈溢出漏洞。
但是,还有个问题就是 我们如何调试该程序,并发生 exp 和获得 flag。
下面是环境搭建的一部分,但是我到现在仍然没有成功:)
下面的方法主要参考 Xp0int
师傅的题解,当然还有官方题解以及 More Smoked Leet Chicken
的题解,也有讲到环境搭建,但是另外两者都会有不同的报错。
- 我们需要在
qemu
环境内下载一个调试器,这里可以选择这个 gdb-server-mips,把该程序放到img
文件系统的根目录下 - 修改
etc/inittab
文件,该文件是我们内核启动时,会读取的配置文件,修改如下
1 | # Put a getty on the serial port |
简单解释,ttyS0
表示子进程要使用的控制台;sysinit
表示系统启动后最先执行,后面即是要执行的动作。这里是启动 gdbserver
,映射端口 1234
,并打开 监视 pppd
程序,并且将 pppd
程序连接到 ttyS1
设备,用于发送和接受数据包。
- 然后修改
qmeu
的启动文件,如下所示:
1 | -net user,hostfwd=tcp::1234-:1234 -net nic -serial stdio -serial pty |
qemu
将把 TCP
端口 1234
和 /dev/ttyS1
从客户机重定向到主机,因此可以通过qemu
创建的 pty
设备与 pppd
通信。
- 然后重新打包文件系统,启动
qemu
。
然后,与服务端 pppd
通信的最简单的方法是如下命令:
1 | sudo pppd noauth local defaultroute debug nodetach /dev/pts/1 user admin password 1234568 |
本来,我们需要与服务端通信需要我们自己写一个 pppd
格式的数据包,这里 More Smoked Leet Chicken
大佬就是自己实现了一个数据通信。但是,我们可以自己通过 patch pppd
程序,在其 eap_chap_response
函数中,把数据从我们的文件读,从而省略了自己构造数据格式的过程。(这一点是十分巧妙地方法)。
源码中,eap_chap_response
函数如下:
1 | eap_chap_response(esp, id, hash, esp->es_client.ea_name, |
然后,我们可以修改为如下:
1 | char payload[1024] = {0}; |
然后,我们创建 /tmp/sc
文件
1 | $ python3 -c 'from pwn import*; open("/tmp/sc", "wb").write(cyclic(1024))' |
然后使用 gdb
启动并附加远程 pppd
,然后在本地机器上运行已经打过补丁的pppd
:
1 | sudo ./pppd-payload noauth local defaultroute debug nodetach /dev/pts/1 user admin password 1234568 |
这样即可实现与服务端通信。
原理上感觉上面这种方法是很靠谱的,但是我搭了很久的环境仍然没成功。所以下面的exp步骤,都没有经过自己调试。先记录一下,等后面对qemu知识记录足够,再来调试
EXP
既然是 栈溢出漏洞,那么很常见使用 ROP
,并且程序没有开启 PIE
和 NX
,那么只用绕过 canary
即可。
用到的 gadget
如下:
1 | 0042f9a8 bc 02 bf lw ra ,local_4 (sp ) |
该指令是将 栈上的数据,依次存到 ra
返回地址寄存器,和 s4-s0
调用时原寄存器值,以及跳转到 ra
寄存器的地址处。利用这个 gadget
可以跳转到我们想去的地址。
那么接下来就是要控制 sp
。这里需要用到一个全局跳转变量 sigjmp
,如下所示:
1 | 0045c71c void * NaP __sp |
这里,有一个 __sp
指针,且其指向一个固定的栈地址。我们应用这个地址,可以将 sp
寄存器指向一个固定的栈地址。然后在该固定的栈地址处布置上 shellcode
。这里,我们能够控制 sp
后,还需要一个跳转到该 sp
地址并去执行的 gadget
,这里需要使用 jalr
,jalr
的作用是跳转到寄存器存储的地址指向的值,类似于 间接跳转 jmp [$rax]
,而 jr
的作用类似于直接跳转,直接跳转到寄存器的值上去。所以这里我们还需要找到一个 jalr
的 gadget
,用来跳转到 0x45c71c
地址指向的 sp
值。最终如下:
1 | 0x43e310 <__libc_csu_init+96>: lw t9,0(s0) |
那么,我们总体的利用思路就是:
- 利用
0x0042f9a8
地址的gadget
来 将$ra
寄存器设置为0x43e310
,将$s0
寄存器设置为0x45c71c
- 随后跳转到
0x43e310
地址的gadget
,并 使用jalr t9
,跳转到0x43e310
指向的一个 栈地址 - 我们在该地址处布置 上我们的
shellcode
这里,还需要注意由于 该 sp
地址,一次只能放 32 bytes
的shellcode
,所以这里还需要将完整的 shellcode
,分成两部分:jmp shellcode 和 getflag shellcode。
1 | #!/usr/bin/env python3 |
然后,这里有十分精妙的 mips
执行 orw
的过程,下面分析一下,以加深理解。
1 | /* Save dst fd for later */ |
li
命令是将 一个立即数 传递给寄存器,sw
指令是将一个字节 从寄存器 存储到 存储器 这里相当于 push
操作。not
指令是非指令,这里是把 0x67
加到了 $t1
的末尾。addiu
指令 是一个 寄存器与常数相加指令,这里相当于将 栈顶 sub 8
。总体功能就是 实现了 push 0x67616c62f
1 | /* call open('$sp', 'O_RDONLY') */ |
这里先用 add
指令将 $sp
的数据 加 0 后传给 $a0
,$a0
是函数调用的第一个参数。然后 使用 slti
指令 比较 $zeor
与 0xffff
的值,如果大则将 $a1
赋值为0,如果小则将 $a1
赋值为 1,这里就相当于对 $a1
赋值为0。使用 ori
指令,将 $zero
与 SYSOPEN
异或,并将结果赋值给 $v0
,这里相当于将 rax=SYSOPEN
,最后调用 syscall
执行函数。
1 | /* Save src fd for later */ |
这里首先使用 sw
指令将 $v0
的值 赋值给 $sp-4
处,这里相当于将之前 Open
的返回值 压栈。再将 该 值复制给 $s1
寄存器。
1 | /* Load file size */ |
然后,接下来就是调用 sendfile()
函数,其参数依次为 fd
指针,open_fd
指针,和 size
。
mixture
一道 web-pwn,其中 pwn
部分是一个 php-pwn
的栈溢出,题解可以参考我 web-pwn
文章。
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/02/21/2020-De1CTF/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!