0x01 Dockerfile -> js -> wasm 入口分析

Dockerfile -> js

Dockerfile -> entrypoint.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM node:22-bookworm-slim AS chroot

ENV NODE_ENV=production
RUN install -d -o 1000 -g 1000 /home/user
COPY --chmod=0444 flag.txt /flag.txt
COPY --chown=1000:1000 --chmod=0444 sparxicle.js sparxicle.wasm /home/user/

FROM gcr.io/kctf-docker/challenge@sha256:413451e53e8ff4d359f551699e7b73594d0be5b87e7b2bd284bd3f9475418e7d AS challenge

COPY --from=chroot / /chroot
WORKDIR /home/user
COPY --chmod=0444 nsjail.cfg /home/user/
COPY --chmod=0555 entrypoint.sh /home/user/
CMD ["/home/user/entrypoint.sh"]
1
2
3
4
5
#!/bin/bash
set -Eeuo pipefail

kctf_setup
exec kctf_drop_privs socat "TCP-LISTEN:1337,reuseaddr,fork" "EXEC:kctf_pow nsjail --config /home/user/nsjail.cfg -- /usr/local/bin/node /home/user/sparxicle.js"

关键在于/usr/local/bin/node /home/user/sparxicle.js"

js -> wasm

这一步准备了一些函数供wasm内部使用,比如用实现了_sparxie_redeem函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// read the flag
function _sparxie_redeem(noncePtr, nonceLen) {
    if (nonceLen !== 32) {
        throw new Error("invalid backstage witness")
    }
    const nonce = HEAPU8.slice(noncePtr, noncePtr + nonceLen);
    let witness = 0;
    for (const byte of nonce) {
        witness = (witness * 257 ^ byte) >>> 0
    }
    let flag = null;
    try {
        flag = require("fs").readFileSync("/flag.txt", "utf8").trim()
    } catch (_) {}
    if (!flag) {
        throw new Error("backstage flag is unavailable")
    }
    out("[Sparxie] The final encore reached backstage.");
    out(flag);
    out("[analytics] backstage witness " + witness.toString(16).padStart(8, "0"))
}

然后将其加入到asmLibraryArg中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var asmLibraryArg = {
    "__syscall_dup3": ___syscall_dup3,
    "__syscall_fcntl64": ___syscall_fcntl64,
    "__syscall_ioctl": ___syscall_ioctl,
    "__syscall_open": ___syscall_open,
    "_emscripten_throw_longjmp": __emscripten_throw_longjmp,
    "abort": _abort,
    "clock": _clock,
    "emscripten_memcpy_big": _emscripten_memcpy_big,
    "emscripten_resize_heap": _emscripten_resize_heap,
    "fd_close": _fd_close,
    "fd_read": _fd_read,
    "fd_seek": _fd_seek,
    "fd_write": _fd_write,
    "getTempRet0": _getTempRet0,
    "invoke_vii": invoke_vii,
    "setTempRet0": _setTempRet0,
    "sparxie_prepare": _sparxie_prepare,
    "sparxie_redeem": _sparxie_redeem,
    "time": _time
};

createWasm的流程

1
2
3
4
5
6
7
8
9
读取 xxx.wasm
        ↓
准备 imports
        ↓
WebAssembly.instantiate
        ↓
拿到 instance.exports
        ↓
返回 exports

首先看读取wasm binary的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var wasmBinaryFile;
wasmBinaryFile = "sparxicle.wasm";
if (!isDataURI(wasmBinaryFile)) {
    wasmBinaryFile = locateFile(wasmBinaryFile)
}

function getBinary(file) {
    try {
        if (file == wasmBinaryFile && wasmBinary) {
            return new Uint8Array(wasmBinary)
        }
        if (readBinary) {
            return readBinary(file)
        } else {
            throw "sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)"
        }
    } catch (err) {
        abort(err)
    }
}

function instantiateSync(file, info) {
    var instance;
    var module;
    var binary;
    try {
        binary = getBinary(file);
        module = new WebAssembly.Module(binary);
        instance = new WebAssembly.Instance(module, info)
    } catch (e) {
        var str = e.toString();
        err("failed to compile wasm module: " + str);
        if (str.includes("imported Memory") || str.includes("memory import")) {
            err("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time).")
        }
        throw e
    }
    return [instance, module]
}

最后提供一个函数instantiateSync, 返回wasm实例.

它在craeteWasm中被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createWasm() {
    var info = {
        "env": asmLibraryArg,
        "wasi_snapshot_preview1": asmLibraryArg
    };

    function receiveInstance(instance, module) {
        var exports = instance.exports;
        Module["asm"] = exports;
        wasmMemory = Module["asm"]["memory"];
        updateGlobalBufferAndViews(wasmMemory.buffer);
        wasmTable = Module["asm"]["__indirect_function_table"];
        addOnInit(Module["asm"]["__wasm_call_ctors"]);
        removeRunDependency("wasm-instantiate")
    }
    addRunDependency("wasm-instantiate");
    if (Module["instantiateWasm"]) {
        try {
            var exports = Module["instantiateWasm"](info, receiveInstance);
            return exports
        } catch (e) {
            err("Module.instantiateWasm callback failed with error: " + e);
            return false
        }
    }
    var result = instantiateSync(wasmBinaryFile, info);
    receiveInstance(result[0]);
    return Module["asm"]
}

最后通过receiveInstance获得exports表(instance.exports).

上面的createWasm()调用后,得到asm变量 (我们上面得到的exports表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var asm = createWasm();
var ___wasm_call_ctors = Module["___wasm_call_ctors"] = asm["__wasm_call_ctors"];
var ___errno_location = Module["___errno_location"] = asm["__errno_location"];
var _free = Module["_free"] = asm["free"];
var _main = Module["_main"] = asm["main"];
var _malloc = Module["_malloc"] = asm["malloc"];
var ___stdio_exit = Module["___stdio_exit"] = asm["__stdio_exit"];
var ___funcs_on_exit = Module["___funcs_on_exit"] = asm["__funcs_on_exit"];
var _setThrew = Module["_setThrew"] = asm["setThrew"];
var _saveSetjmp = Module["_saveSetjmp"] = asm["saveSetjmp"];
var stackSave = Module["stackSave"] = asm["stackSave"];
var stackRestore = Module["stackRestore"] = asm["stackRestore"];
var stackAlloc = Module["stackAlloc"] = asm["stackAlloc"];
var dynCall_jiji = Module["dynCall_jiji"] = asm["dynCall_jiji"];

这里对Module["_main"]等函数进行了赋值(都来自asm[<some_exported_symbol>]

接着看最后的启动函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var calledRun;

function ExitStatus(status) {
    this.name = "ExitStatus";
    this.message = "Program terminated with exit(" + status + ")";
    this.status = status
}
var calledMain = false;
dependenciesFulfilled = function runCaller() {
    if (!calledRun) run();
    if (!calledRun) dependenciesFulfilled = runCaller
};

function callMain(args) {
    var entryFunction = Module["_main"];
    var argc = 0;
    var argv = 0;
    try {
        var ret = entryFunction(argc, argv);
        exit(ret, true);
        return ret
    } catch (e) {
        return handleException(e)
    } finally {
        calledMain = true
    }
}

function run(args) {
    args = args || arguments_;
    if (runDependencies > 0) {
        return
    }
    preRun();
    if (runDependencies > 0) {
        return
    }

    function doRun() {
        if (calledRun) return;
        calledRun = true;
        Module["calledRun"] = true;
        if (ABORT) return;
        initRuntime();
        preMain();
        if (shouldRunNow) callMain(args);
        postRun()
    } {
        doRun()
    }
}
Module["run"] = run;

function exit(status, implicit) {
    EXITSTATUS = status;
    if (keepRuntimeAlive()) {} else {
        exitRuntime()
    }
    procExit(status)
}

function procExit(code) {
    EXITSTATUS = code;
    if (!keepRuntimeAlive()) {
        ABORT = true
    }
    quit_(code, new ExitStatus(code))
}
var shouldRunNow = true;
run();

var entryFunction = Module["_main"]这一步定义了入口函数,而 Module["_main"]在上一部已经被装填.

wat结构

将wasm反汇编后,可以得到wat代码.

先看开头,出现了一些typeimport.

wat索引注释

(;a comment;) 在wat中为注释,

这里(type (;0;) (func (param i32) (result i32)))(;0;) 代表idx=0的函数原型,

(import "env" "time" (func (;1;) (type 0)))(;1;)代表idx=1的函数 ``` (module (type (;0;) (func (param i32) (result i32))) (type (;1;) (func (param i32 i32 i32 i32) (result i32))) (type (;2;) (func (param i32 i32 i32) (result i32))) (type (;3;) (func (param i32 i32))) (type (;4;) (func (param i32 i32 i32))) (type (;5;) (func (param i32 i32) (result i32))) (type (;6;) (func (param i32))) (type (;7;) (func (param i32 i32 i32 i32))) (type (;8;) (func (param i32 i32 i32 i32 i32) (result i32))) (type (;9;) (func (param i32 i32 i32 i32 i32))) (type (;10;) (func (param i32 i64 i64 i64 i64))) …… (import “env” “abort” (func (;0;) (type 11))) (import “env” “time” (func (;1;) (type 0))) (import “env” “clock” (func (;2;) (type 13))) (import “env” “sparxie_prepare” (func (;3;) (type 5))) (import “env” “sparxie_redeem” (func (;4;) (type 3))) (import “env” “__syscall_open” (func (;5;) (type 2))) (import “env” “__syscall_fcntl64” (func (;6;) (type 2))) (import “env” “__syscall_ioctl” (func (;7;) (type 2))) (import “wasi_snapshot_preview1” “fd_read” (func (;8;) (type 1))) (import “wasi_snapshot_preview1” “fd_write” (func (;9;) (type 1))) (import “wasi_snapshot_preview1” “fd_close” (func (;10;) (type 0))) (import “env” “emscripten_memcpy_big” (func (;11;) (type 2))) (import “env” “__syscall_dup3” (func (;12;) (type 2))) (import “env” “emscripten_resize_heap” (func (;13;) (type 0))) (import “env” “setTempRet0” (func (;14;) (type 6))) (import “env” “_emscripten_throw_longjmp” (func (;15;) (type 11))) (import “env” “getTempRet0” (func (;16;) (type 13))) (import “env” “invoke_vii” (func (;17;) (type 4))) (import “wasi_snapshot_preview1” “fd_seek” (func (;18;) (type 8))) (func (;19;) (type 11) …… ) …… (func (;497;) (type 8) (param i32 i32 i32 i32 i32) (result i32) (local i64) local.get 1 local.get 2 i64.extend_i32_u local.get 3 i64.extend_i32_u i64.const 32 i64.shl i64.or local.get 4 local.get 0 call_indirect (type 12) local.tee 5 i64.const 32 i64.shr_u i32.wrap_i64 call 14 local.get 5 i32.wrap_i64) (table (;0;) 152 152 funcref) (memory (;0;) 512 2048) (global (;0;) (mut i32) (i32.const 1189344)) (export “memory” (memory 0)) (export “__wasm_call_ctors” (func 19)) (export “__errno_location” (func 449)) (export “free” (func 155)) (export “main” (func 496)) (export “malloc” (func 254)) (export “__stdio_exit” (func 460)) (export “__funcs_on_exit” (func 492)) (export “__indirect_function_table” (table 0)) (export “setThrew” (func 43)) (export “saveSetjmp” (func 44)) (export “stackSave” (func 40)) (export “stackRestore” (func 41)) (export “stackAlloc” (func 42)) (export “dynCall_jiji” (func 497)) (elem (;0;) (i32.const 1) func 98 100 105 145 159 160 162 168 171 149 174 175 177 179 236 237 231 289 350 351 367 375 420 421 154 157 163 223 400 353 302 414 425 432 456 455 453 457 488 486 487 126 164 166 167 165 169 170 172 173 176 178 180 181 182 183 185 186 187 188 189 193 194 195 224 226 228 229 230 232 233 234 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 303 304 364 365 366 368 370 374 376 379 380 382 383 384 385 386 387 389 390 354 356 357 358 359 360 361 362 401 403 404 405 406 407 408 415 416 417 418 419 447 448 446 444 445 443 442 441 440 437 438 439 433 434 435 436 430 468 469) (data (;0;) (i32.const 1025) “” )

)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1. `(type (;0;) (func (param i32) (result i32)))`.

定义idx=0的type, 为导入函数、内部函数提供函数原型.

2. `(import "env" <name> <func>)`
   
风格导入外部函数,产生函数0-18, 

3. `(func (;19;) (type 11))`
   
定义idx=19的内部函数,使用type=11. 内部函数接上导入函数的编号,从19开始持续到497.

`call 123`之类的指令会直接寻找对应的函数编号.

4. `(table (;0;) 152 152 funcref)` 
   
定义了idx=0的表,类型为函数引用,(min_size, max_size) = (152, 152). 

1. `(memory (;0;) 512 2048)`
   
定义了idx=0的memory, (init_pages, max_pages) = (512pages, 2048pages) (wasm中1page = 64KB), 

`i32.load`、`i32.store`等都会使用这里的内存区域.

6. `(global (;0;) (mut i32) (i32.const 1189344))`
   
定义一个idx=0的全局变量. 作为唯一的一个全局变量,基本就是栈指针了,初始值为`0x1225e0`,指向前面的memory.

某个函数头部有:

(local i32 i32) global.get 0 i32.const 16 i32.sub local.tee 2 global.set 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第一步在栈上先后分配了两个变量1, 2.  

接着`global.get 0`将栈上压入栈指针,后续计算`sp-0x10`的操作.

`local.tee 2`类似`local.set 2`,但是前者并不消耗栈上的值,后者会.

`global.set 0`实现`sp = sp-0x10`

> **local变量**
>
> local变量包含函数本身的param和开头的`(local i32 i32)`,下表从0开始.
{: .prompt-info }

7. `(export "main" (func 496))`

导出idx=496的函数, 名称为`main`. (在js中有:`var _main = Module["_main"] = asm["main"]`)

另外`(export "__indirect_function_table" (table 0))`把idx=0的table导出给js端,让其能够使用`__indirect_function_table`

另外在wasm内部也可以使用下列方式来间接调用:

local.get 0
call_indirect (type 1)

1
2
3
4
5
6
7
8
(注意这里local.get 0是获取local变量0的值,然后作为offset进行间接调用)


8. `(elem (;0;) (i32.const 1) func 98 100 105 145 159 160 ...)`

初始化前面的那张table. 

`(i32.const 1)`表示起始table index = 1. 大致为:

table[0] = null table[1] = func 98 table[2] = func 100 table[3] = func 105

1
2
3
4
5
6
7
8
9
9. `(data (;0;) (i32.const 1025) ...)`

定义idx=0的data segment. 把后面这一串数据复制到memory的addr=1025处.

比如对于
`(data (;162;) (i32.const 24048) "SPARXIE::RUNTIME::RANDOM\00\00\00\00\00\00\00\00SPARXIE::ENCORE::PROOF")`
的引用,会通过下面的形式:

i32.const 24048 i32.const 24 call 427

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
这里的24048相当于字符串指针,直接作为参数传递.

### 0x02 wasm c代码分析

#### entry
main.c中的入口函数:
```c

int main(void) {
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  fputs(sparxie_banner, stdout);
  puts("[catalogue] one Spotlight Pass remains");
  puts("[studio] upload one SPX2 creator cartridge:");

  size_t packed_len = 0;
  uint8_t *packed = read_cartridge(&packed_len);
  if (packed == NULL) {
    puts("[moderation] upload too large");
    return 1;
  }

  uint8_t *chunk = NULL;
  size_t chunk_len = 0;
  char error[160];
  if (cartridge_unpack(packed, packed_len, &chunk, &chunk_len, error,
                       sizeof(error)) != 0) {
    printf("[moderation] %s\n", error);
    free(packed);
    return 1;
  }
  free(packed);

  lua_State *state = luaL_newstate();
  if (state == NULL) {
    free(chunk);
    return 1;
  }
  open_sandbox(state);
  luaL_requiref(state, "sparxie", luaopen_sparxie, 1);
  lua_setglobal(state, "sparxie");
  if (sparxie_install_authority(state) != 0) {
    puts("[studio] backstage initialization failed");
    lua_close(state);
    free(chunk);
    return 1;
  }

  int status = luaL_loadbufferx(state, (const char *)chunk, chunk_len,
                                "@vanishing-encore", "t");
  free(chunk);
  if (status == LUA_OK)
    status = lua_pcall(state, 0, 0, 0);
  if (status != LUA_OK) {
    const char *detail = lua_tostring(state, -1);
    printf("[chat] %s\n", detail != NULL ? detail : "the stream glitched");
    lua_close(state);
    return 1;
  }

  lua_close(state);
  puts("[studio] stream ended");
  return 0;
}

cartridge header parse

首先执行read_cartridge函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define INPUT_LIMIT (96u * 1024u)
#define CARTRIDGE_HEADER_SIZE 32u
#define CARTRIDGE_MAGIC "SPX2LIVE"
#define CARTRIDGE_MAGIC_SIZE 8u

static uint8_t *read_cartridge(size_t *length) {
  uint8_t *buffer = (uint8_t *)malloc(INPUT_LIMIT);
  if (buffer == NULL)
    return NULL;

  if (!read_exact(buffer, CARTRIDGE_HEADER_SIZE)) {
    free(buffer);
    return NULL;
  }

  size_t packed_len = CARTRIDGE_HEADER_SIZE;
  if (memcmp(buffer, CARTRIDGE_MAGIC, CARTRIDGE_MAGIC_SIZE) == 0) {
    uint32_t plain_len = load32(buffer + CARTRIDGE_MAGIC_SIZE);
    if (plain_len > INPUT_LIMIT - CARTRIDGE_HEADER_SIZE ||
        !read_exact(buffer + CARTRIDGE_HEADER_SIZE, plain_len)) {
      free(buffer);
      return NULL;
    }
    packed_len += plain_len;
  }

  if (ferror(stdin)) {
    free(buffer);
    return NULL;
  }
  *length = packed_len;
  return buffer;
}

可以看出这是某种packet解析器。其中header size = 32Bytes, 所以先读取固定长度为CARTRIDGE_HEADER_SIZE的数据,

1
2
3
4
5
6
# header
+0  magic: "SPX2LIVE"  8B
+8  plain_len: u32     4B
+12 padding:           20B
# payload
+32 content            size = plain_len

该函数将包含header在内的完整buffer返回, 存储在packed变量中,后续又执行:

1
cartridge_unpack(packed, packed_len, &chunk, &chunk_len, error,sizeof(error))

cartridge payload unpack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
int cartridge_unpack(const uint8_t *input, size_t input_len, uint8_t **output,
                     size_t *output_len, char *error, size_t error_len) {
  if (input_len < HEADER_SIZE || memcmp(input, "SPX2LIVE", 8) != 0) {
    snprintf(error, error_len, "that clip is not part of today's stream");
    return -1;
  }

  uint32_t plain_len = load32(input + 8);
  uint32_t nonce = load32(input + 12);
  uint32_t claimed = load32(input + 16);
  uint32_t header_tag = load32(input + 20);
  uint32_t lanes = load32(input + 24);
  uint32_t encore = load32(input + 28);

  if (plain_len == 0 || plain_len > MAX_CHUNK ||
      input_len != HEADER_SIZE + (size_t)plain_len) {
    snprintf(error, error_len, "the engagement numbers do not add up (%zu/%zu)",
             input_len, HEADER_SIZE + (size_t)plain_len);
    return -1;
  }
  if (lanes != 4 ||
      header_tag != mix(nonce ^ plain_len ^ UINT32_C(0xa11ce5ed))) {
    snprintf(error, error_len, "the broadcast mask slipped");
    return -1;
  }

  uint8_t *plain = (uint8_t *)malloc(plain_len);
  if (plain == NULL) {
    snprintf(error, error_len, "Planarcadia ran out of bandwidth");
    return -1;
  }

  uint32_t state[4] = {
      mix(nonce ^ UINT32_C(0x243f6a88)), mix(nonce ^ UINT32_C(0x85a308d3)),
      mix(nonce ^ UINT32_C(0x13198a2e)), mix(nonce ^ UINT32_C(0x03707344))};
  // 解密循环
  for (uint32_t i = 0; i < plain_len; ++i) {
    unsigned lane = (i + (nonce & 3u)) & 3u;
    state[lane] = mix(state[lane] + UINT32_C(0x9e3779b9) + i);
    uint8_t key = (uint8_t)(state[lane] >> ((i & 3u) * 8u));
    plain[i] = input[HEADER_SIZE + i] ^ key ^ (uint8_t)(i * 29u);
  }

  uint32_t actual = checksum(plain, plain_len, nonce);
  uint32_t expected_encore = mix(claimed ^ nonce ^ UINT32_C(0xe1a7104e));
  if (actual != claimed || encore != expected_encore) {
    free(plain);
    snprintf(error, error_len, "the audience rejected this cut (%08x/%08x)",
             actual, claimed);
    return -1;
  }

  *output = plain;
  *output_len = plain_len;
  return 0;
}
1
2
3
4
5
6
7
8
9
10
# header
+0  magic: "SPX2LIVE" 
+8  plain_len: u32     
+12 nonce: u32
+16 claimed: u32
+20 header_tag: u32
+24 lanes: u32
+28 encore: u32
# payload
+32 content            size = plain_len

能够确定是利用header中的一些字段对payload进行解密, 最后将解密后的明文buffer指针(堆分配)传入第三个参数,对应main.c调用中的&chunk.

lua code execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
lua_State *state = luaL_newstate();
  if (state == NULL) {
    free(chunk);
    return 1;
  }
  open_sandbox(state);
  luaL_requiref(state, "sparxie", luaopen_sparxie, 1);
  lua_setglobal(state, "sparxie");
  if (sparxie_install_authority(state) != 0) {
    puts("[studio] backstage initialization failed");
    lua_close(state);
    free(chunk);
    return 1;
  }

  int status = luaL_loadbufferx(state, (const char *)chunk, chunk_len,
                                "@vanishing-encore", "t");
  free(chunk);
  if (status == LUA_OK)
    status = lua_pcall(state, 0, 0, 0);
  if (status != LUA_OK) {
    const char *detail = lua_tostring(state, -1);
    printf("[chat] %s\n", detail != NULL ? detail : "the stream glitched");
    lua_close(state);
    return 1;
  }

  lua_close(state);
  puts("[studio] stream ended");

lua_State *state = luaL_newstate();获得一个完整lua虚拟机的context.

open_sandbox(state); 自定义函数,开启沙箱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void open_sandbox(lua_State *state) {
  static const luaL_Reg safe_libraries[] = {{LUA_GNAME, luaopen_base},
                                            {LUA_COLIBNAME, luaopen_coroutine},
                                            {LUA_TABLIBNAME, luaopen_table},
                                            {LUA_STRLIBNAME, luaopen_string},
                                            {LUA_MATHLIBNAME, luaopen_math},
                                            {LUA_UTF8LIBNAME, luaopen_utf8},
                                            {NULL, NULL}};
  for (const luaL_Reg *library = safe_libraries; library->func != NULL;
       ++library) {
    luaL_requiref(state, library->name, library->func, 1);
    lua_pop(state, 1);
  }
  lua_pushnil(state);
  lua_setglobal(state, "dofile");
  lua_pushnil(state);
  lua_setglobal(state, "load");
  lua_pushnil(state);
  lua_setglobal(state, "loadfile");
  lua_pushnil(state);
  lua_setglobal(state, "print");
  lua_pushnil(state);
  lua_setglobal(state, "tostring");
  lua_getglobal(state, LUA_STRLIBNAME);
  lua_pushnil(state);
  lua_setfield(state, -2, "format");
  lua_pop(state, 1);
}

luaL_requiref(state, "sparxie", luaopen_sparxie, 1); 是lua C api, 注册sparxie模块,而用于注册的c函数为luaopen_sparxie, 在头文件中声明:

1
int luaopen_sparxie(lua_State *state);
1
2
3
4
5
6
7
8
int status = luaL_loadbufferx(state,
  (const char *)chunk, chunk_len,
  "@vanishing-encore"/*chunk名*/,
  "t"   
  /* "t" = 只允许 text Lua chunk
  "b" = 只允许 binary Lua bytecode
  "bt" = 两者都允许 */
);

则把我们解密后的chunk数据作为lua源代码进行编译. (文本形式)

编译完成后,lua stack的栈顶会有一个函数,后面的status = lua_pcall(state, 0, 0, 0); 会执行这个函数.

后续会把state信息输出,可以尝试用来打印flag:

1
2
    const char *detail = lua_tostring(state, -1);
    printf("[chat] %s\n", detail != NULL ? detail : "the stream glitched");

到这里我们能够明白其大致逻辑:除去外层的加解密部分,它给我们提供了一个受限的lua环境,以及自定义的模块。我们需要编写lua脚本来在这个环境中泄漏flag. 那么下一步就是分析wasm中的luaopen_sparxie.

在这一题的wasm中,luaopen_sparxie这样的lua函数名信息已经不再存在,我们只能通过其c形式来推演类似的wat码定位.

在wasm中, luaL_requiref(state, "sparxie", luaopen_sparxie, 1); 的调用可能会变成类似:

1
2
3
4
5
local.get $state
i32.const 24048       ;; 假设指向 "sparxie\0"
i32.const 17          ;; 假设 table[17] = luaopen_sparxie
i32.const 1
call $luaL_requiref

的形式,接着定位得到:

1
2
3
4
local.get 2        ;; lua_State *
i32.const 17945    ;; "sparxie"
i32.const 33       ;; luaopen_sparxie function pointer slot
call 153

这里的33是间接调用号,在函数表中对应func 425

于是我们可以用字符串来定位,找到关键点:

0x03 lua function