作为一年一度的CTF“高考”,今年的高考应用题很迷惑,四道Web三道有非预期,Pwn一个签到一个LLVM(我不理解。Re两道阅读题眼睛快瞎了,第三道Rabit加密还可以被爆破。最迷惑的是可信计算,两道一模一样的题不知道是什么操作。
Reverse
这次高考,Reverse的应用题比重太高了,真的眼睛要瞎了
baby_tree
题目是一个.ast文件,打开看部分内容
(source_file "re.swift"
(func_decl range=[re.swift:1:1 - line:14:1] "check(_:_:)" interface type='(String, String) -> Bool' access=internal
(parameter_list range=[re.swift:1:11 - line:1:49]
(parameter "encoded" type='String' interface type='String')
(parameter "keyValue" type='String' interface type='String'))
(result
(type_ident
(component id='Bool' bind=Swift.(file).Bool)))
(brace_stmt range=[re.swift:1:59 - line:14:1]
(pattern_binding_decl range=[re.swift:2:5 - line:2:33]
(pattern_named type='[UInt8]' 'b')
Original init:
(call_expr type='[UInt8]' location=re.swift:2:19 range=[re.swift:2:13 - line:2:33] nothrow
(constructor_ref_call_expr type='(String.UTF8View) -> [UInt8]' location=re.swift:2:19 range=[re.swift:2:13 - line:2:19] nothrow
(declref_expr implicit type='(Array<UInt8>.Type) -> (String.UTF8View) -> Array<UInt8>' location=re.swift:2:19 range=[re.swift:2:19 - line:2:19] decl=Swift.(file).Array extension.init(_:) [with (substitution_map generic_signature=<Element, S where Element == S.Element, S : Sequence> (substitution Element -> UInt8) (substitution S -> String.UTF8View))] function_ref=single)
(argument_list implicit
(argument
(type_expr type='[UInt8].Type' location=re.swift:2:13 range=[re.swift:2:13 - line:2:19] typerepr='[UInt8]'))
))
(argument_list
(argument
(member_ref_expr type='String.UTF8View' location=re.swift:2:29 range=[re.swift:2:21 - line:2:29] decl=Swift.(file).String extension.utf8
(declref_expr type='String' location=re.swift:2:21 range=[re.swift:2:21 - line:2:21] decl=re.(file).check(_:_:).encoded@re.swift:1:14 function_ref=unapplied)))
))
以下省略600行...
雀石很明显是一颗AST树。但之前没接触过这样的语法,这看起来太抽象了。先对其中一些关键字进行了搜索。继而发现这是一颗swift的AST树。并找到了其编译流程。
可以看出是通过对.swift文件 swiftc -dump-ast 生成得到的。我也去尝试下载了swiftc并通过其他编译参数对其继续向下编译,但很遗憾,不可以。也就是说,这玩意只能硬看。
然后来分析这个东西的结构。虽然,官方文档也说了*-dump-ast*生成的输出可读性很差,但基本的树结构其实还是能看出来的,就是说,多了很多很多的无用信息。比如说,它的缩进表明了节点的父子关系,通过缩进的关系,也可用一些文本编辑器将某些节点包括其所有的子节点均收起。
如图,可看到第二行是一个 func_decl,字面意思知道这里应该是一个函数的定义,其类型为 (String, String) -> Bool。我们先将其收起,从入口点(571行)开始看。
入口点这一部分我快速过了(其实就是瞎看的),可以看到有两个string类型的变量data和key,key的值为"345y",data应该就是输入
然后call指令调用函数,调用的函数名是check,并把data和key作为参数传入,并接收Bool型的返回值result。后面就是判result了。
所以所有的逻辑均在check函数中。重新回到上面分析check函数。check函数的直接子节点表明了参数列表和返回值类型,可以看到接收两个string类型的参数encoded和keyValue,返回Bool。
随后将encoded和keyValue转化成数组并存到b和k中。并在72-96行间声明了r0-r3四个变量。到这里为止程序的意思都是能猜出来的,在98行开始for_each_stmt,程序正式进入算法部分,就要正经分析了。
for_each_stmt的直接子节点表明迭代变量是i,然后有一个直接子节点binary_expr,其操作是CloseRanged,也就是闭区间,它的argument_list带有两个参数,分别是0和一个binary_expr。这个binary_expr可以看到其操作是 Swift.(file).Int extension.-,也就是减法操作。这个减法操作的argument_list同样带有两个参数,分别是 * count re.(file).check(::).b* 和 4。那么我们就可以把这棵树画出来并整理得到这句话即为 for i in CloseRanged(0, count(b)-4)
继续往下看,下面是一条 assign_expr 赋值语句。其有两个直接子节点,均为tuple_expr,也就是元组。且第一个元组是 (r0, r1, r2, r3)。
第二个元组下面有四个形式相似的load_expr,并且直接子节点都是subscript_expr(取下标)。可以很快分析出第一个load_expr表达式是b[i],第二个load_expr表达式是b[i+1],后面两个元素也是一样的,为b[i+2]和b[i+3]。故我们可以整理得到这条赋值语句为 r0, r1, r2, r3 = ( b[i], b[i+1], b[i+2], b[i+3] )
后面全都是一样的分析方法,这里不说了,贴出还原出的伪代码
|
|
算法很简单,exp如下
|
|
babycode
我以为上面这道阅读题就是极限了,原来只是热身(?12点后第二张试卷上又是一篇阅读,而且更抽象。
题目是一个.mrb的Ruby字节码文件。文件头是RITE0300,具体哪个版本我没查,但我只知道这个版本很高(v2.1.0的头是RITE0006,v3.0.0的头是RITE0200),所以我直接下载了最新版v3.1.0的mruby。编译后使用 mruby -v -b babycode.mrb 得到Ruby汇编。
然后翻到一份Ruby字节码的官方文档
Instruction Name | Operand type | Semantics |
---|---|---|
OP_NOP | - | |
OP_MOVE“ | BB | R(a) = R(b) |
OP_LOADL” | BB | R(a) = Pool(b) |
OP_LOADI“ | BsB | R(a) = mrb_int(b) |
OP_LOADI_0' | B | R(a) = 0 |
OP_LOADI_1' | B | R(a) = 1 |
OP_LOADI_2' | B | R(a) = 2 |
OP_LOADI_3' | B | R(a) = 3 |
OP_LOADSYM” | BB | R(a) = Syms(b) |
OP_LOADNIL' | B | R(a) = nil |
OP_LOADSELF' | B | R(a) = self |
OP_LOADT' | B | R(a) = true |
OP_LOADF' | B | R(a) = false |
OP_GETGV“ | BB | R(a) = getglobal(Syms(b)) |
OP_SETGV” | BB | setglobal(Syms(b), R(a)) |
OP_GETSV“ | BB | R(a) = Special[b] |
OP_SETSV” | BB | Special = R(a) |
OP_GETIV“ | BB | R(a) = ivget(Syms(b)) |
OP_SETIV” | BB | ivset(Syms(b),R(a)) |
OP_GETCV“ | BB | R(a) = cvget(Syms(b)) |
OP_SETCV” | BB | cvset(Syms(b),R(a)) |
OP_GETCONST“ | BB | R(a) = constget(Syms(b)) |
OP_SETCONST” | BB | constset(Syms(b),R(a)) |
OP_GETMCNST“ | BB | R(a) = R(a)::Syms(b) |
OP_SETMCNST” | BB | R(a+1)::Syms(b) = R(a) |
OP_GETUPVAR' | BBB | R(a) = uvget(b,c) |
OP_SETUPVAR' | BBB | uvset(b,c,R(a)) |
OP_JMP | S | pc+=a |
OP_JMPIF' | SB | if R(b) pc+=a |
OP_JMPNOT' | SB | if !R(b) pc+=a |
OP_ONERR | sS | rescue_push(pc+a) |
OP_EXCEPT' | B | R(a) = exc |
OP_RESCUE“ | BB | R(b) = R(a).isa?(R(b)) |
OP_POPERR | B | a.times{rescue_pop()} |
OP_RAISE' | B | raise(R(a)) |
OP_EPOP | B | A.times{ensure_pop().call} |
OP_SENDV” | BB | R(a) = call(R(a),Syms(b),R(a+1)) |
OP_SENDVB“ | BB | R(a) = call(R(a),Syms(b),R(a+1),&R(a+2)) |
OP_SEND” | BBB | R(a) = call(R(a),Syms(b),R(a+1),…,R(a+c)) |
OP_SENDB“ | BBB | R(a) = call(R(a),Syms(Bx),R(a+1),…,R(a+c),&R(a+c+1)) |
OP_CALL' | B | R(a) = self.call(frame.argc, frame.argv) |
OP_SUPER' | BB | R(a) = super(R(a+1),… ,R(a+b+1)) |
OP_ARGARY' | BS | R(a) = argument array (16=5:1:5:1:4) |
OP_ENTER | W | arg setup according to flags (23=5:5:1:5:5:1:1) |
OP_KARG” | BB | R(a) = kdict # todo |
OP_KARG2“ | BB | R(a) = kdict; kdict.rm(Syms(b)) # todo |
OP_RETURN' | B | return R(a) (normal) |
OP_RETURN_BLK' | B | return R(a) (in-block return) |
OP_BREAK' | B | break R(a) |
OP_BLKPUSH' | BS | R(a) = block (16=5:1:5:1:4) |
OP_ADD” | BB | R(a) = R(a)+R(a+1) |
OP_ADDI“ | BBB | R(a) = R(a)+mrb_int© |
OP_SUB” | BB | R(a) = R(a)-R(a+1) |
OP_SUBI“ | BB | R(a) = R(a)-C |
OP_MUL” | BB | R(a) = R(a)R(a+1) |
OP_DIV“ | BB | R(a) = R(a)/R(a+1) |
OP_EQ” | BB | R(a) = R(a)==R(a+1) |
OP_LT“ | BB | R(a) = R(a)<R(a+1) |
OP_LE” | BB | R(a) = R(a)<=R(a+1) |
OP_GT“ | BB | R(a) = R |
OP_GE” | BB | R(a) = R(a)>=R(a+1) |
OP_ARRAY' | BB | R(a) = ary_new(R(a),R(a+1)..R(a+b)) |
OP_ARRAY2“ | BB | R |
OP_ARYCAT' | B | ary_cat(R(a),R(a+1)) |
OP_ARYPUSH' | B | ary_push(R(a),R(a+1)) |
OP_AREF' | BB | R(a) = R(a) |
OP_ASET' | BB | R(a) = R(a+1) |
OP_APOST' | BB | R(a),R(A+1)..R(A+C) = R(a)[B..] |
OP_STRING” | BB | R(a) = str_dup(Lit(b)) |
OP_STRCAT' | B | str_cat(R(a),R(a+1)) |
OP_HASH' | BB | R(a) = hash_new(R(a),R(a+1)..R(a+b)) |
OP_HASHADD' | BB | R(a) = hash_push(R(a),R(a+1)..R(a+b)) |
OP_LAMBDA“ | BB | R(a) = lambda(SEQ,OP_L_LAMBDA) |
OP_BLOCK” | BB | R(a) = lambda(SEQ,OP_L_BLOCK) |
OP_METHOD“ | BB | R(a) = lambda(SEQ,OP_L_METHOD) |
OP_RANGE_INC' | B | R(a) = range_new(R(a),R(a+1),FALSE) |
OP_RANGE_EXC' | B | R(a) = range_new(R(a),R(a+1),TRUE) |
OP_OCLASS' | B | R(a) = ::Object |
OP_CLASS” | BB | R(a) = newclass(R(a),Syms(b),R(a+1)) |
OP_MODULE“ | BB | R(a) = newmodule(R(a),Syms(b)) |
OP_EXEC” | BB | R(a) = blockexec(R(a),SEQ) |
OP_DEF“ | BB | R(a).newmethod(Syms(b),R(a+1)) |
OP_ALIAS' | B | alias_method(R(a),R(a+1),R(a+2)) |
OP_UNDEF” | BB | undef_method(R(a),Syms(b)) |
OP_SCLASS' | B | R(a) = R(a).singleton_class |
OP_TCLASS' | B | R(a) = target_class |
OP_ERR' | B | raise(RuntimeError, Lit(Bx)) |
OP_EXT1 | - | make 1st operand 16bit |
OP_EXT2 | - | make 2nd operand 16bit |
OP_EXT3 | - | make 1st and 2nd operands 16bit |
OP_STOP | - | stop VM |
这里点名批评Ruby的文档,表格全部是乱的,我一点一点帮它把表修好(
然后对着字节码恢复逻辑。函数很多,Ruby的字节码巨抽象,大致分析出会将输入作为参数送入函数check
|
|
分析encrypt函数逻辑。这里有个非常疑惑的点,Ruby里应该是没有ord这个函数的,不知道为什么识别出来的字节码带ord函数,同时直接执行这段字节码也会因为找不到ord函数符号而报错。不知道是不是出题人故意让字节码无法运行而这样设计的。
|
|
enc_one函数接收两个四字节数和四个四字节的key做加密。加密算法是魔改的XTEA(魔改的地方有亿点多)。并且,enc_one只执行算法的一次循环,通过times控制循环轮次(轮数为YY轮,也就是16轮),通过GETUPVAR和SETUPVAR指令获取并修改上层变量来控制XTEA的v与sum(类似于lambda的变量闭包访问)。还原得到这部分加密逻辑
|
|
最后,exp,先逆魔改的XTEA
|
|
逆异或
|
|
得到 flag{6ad1c70c-daa4-11ec-9d64-0242ac1200}
可信计算
上次高考的Final考场,接触过了以下可信计算,可惜太仓促了,没玩明白是啥意思。这次在做两篇大阅读之余研究了以下。CTF的可信计算实则就是编程题,给你一套系统(往往是登录系统),该系统存在一些不安全的地方,比如逻辑上的漏洞,需要你编写代码修改该套系统的源码,重新编译并跑测试脚本,如果测试通过即可得到flag。
基于挑战码的双向认证
添加1. term计算MB’= SM3(key, rA||rB),并与MB比较,若一致则确认服务器端正确。
基于挑战码的双向认证2
term计算MA=SM3(key,rB),并将MA发送给Server
|
|
基于挑战码的双向认证3
笑死,和第二题一模一样,把第二题的代码贴过去就出了。后来听说hacker_server里似乎多check了一些东西但是没启动。反正就是非预期了。这波啊,是高考试卷印错了,印了两道一样的题目。