Ciscn 2022 Re&可信计算 WriteUp

Posted by Qfrost on 2022-05-30
Estimated Reading Time 16 Minutes
Words 3.4k In Total
Viewed Times

作为一年一度的CTF“高考”,今年的高考应用题很迷惑,四道Web三道有非预期,Pwn一个签到一个LLVM(我不理解。Re两道阅读题眼睛快瞎了,第三道Rabit加密还可以被爆破。最迷惑的是可信计算,两道一模一样的题不知道是什么操作。

Reverse

这次高考,Reverse的应用题比重太高了,真的眼睛要瞎了

baby_tree

题目是一个.ast文件,打开看部分内容

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
(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编译过程

可以看出是通过对.swift文件 swiftc -dump-ast 生成得到的。我也去尝试下载了swiftc并通过其他编译参数对其继续向下编译,但很遗憾,不可以。也就是说,这玩意只能硬看。

然后来分析这个东西的结构。虽然,官方文档也说了*-dump-ast*生成的输出可读性很差,但基本的树结构其实还是能看出来的,就是说,多了很多很多的无用信息。比如说,它的缩进表明了节点的父子关系,通过缩进的关系,也可用一些文本编辑器将某些节点包括其所有的子节点均收起。

baby_tree_1

如图,可看到第二行是一个 func_decl,字面意思知道这里应该是一个函数的定义,其类型为 (String, String) -> Bool。我们先将其收起,从入口点(571行)开始看。

入口点这一部分我快速过了(其实就是瞎看的),可以看到有两个string类型的变量data和key,key的值为"345y",data应该就是输入
baby_tree_2

然后call指令调用函数,调用的函数名是check,并把data和key作为参数传入,并接收Bool型的返回值result。后面就是判result了。
baby_tree_3

所以所有的逻辑均在check函数中。重新回到上面分析check函数。check函数的直接子节点表明了参数列表和返回值类型,可以看到接收两个string类型的参数encoded和keyValue,返回Bool。
baby_tree_4

随后将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)
baby_tree_5

继续往下看,下面是一条 assign_expr 赋值语句。其有两个直接子节点,均为tuple_expr,也就是元组。且第一个元组是 (r0, r1, r2, r3)。
baby_tree_6

第二个元组下面有四个形式相似的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] )
baby_tree_7

后面全都是一样的分析方法,这里不说了,贴出还原出的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
def check(encoded : str, keyValue : str) -> bool:
b = list(encoded)
k = list(keyValue)
for i in range(len(b)-4+1):
r0, r1, r2, r3 = ( b[i], b[i+1], b[i+2], b[i+3] )
b[i+0] = r2^(0xFF& (k[0]+ (r0>>4)) )
b[i+1] = r3^( 0xFF& (k[1] + (r1>>2)) )
b[i+2] = r0^k[2]
b[i+3] = r1^k[3]
k[0], k[1], k[2], k[3] = k[1], k[2], k[3], k[0]

return b == ans

算法很简单,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ans = [
88, 35, 88, 225, 7, 201, 57, 94, 77, 56, 75, 168, 72, 218, 64, 91, 16, 101, 32, 207, 73, 130, 74, 128, 76, 201, 16, 248, 41, 205, 103, 84, 91, 99, 79, 202, 22, 131, 63, 255, 20, 16
]
key = b"345y"
k=list(key)

k[0], k[1], k[2], k[3] = k[2], k[3], k[0], k[1]
for i in range(38, -1, -1):
r1 = ans[i+3]^k[3]
r0 = ans[i+2]^k[2]
r3 = ( 0xFF& (k[1] + (r1>>2)) )^ans[i+1]
r2 = (0xFF& (k[0]+ (r0>>4)) )^ans[i+0]
k[0], k[1], k[2], k[3] = k[3], k[0], k[1], k[2]
ans[i], ans[i+1], ans[i+2], ans[i+3] = r0, r1, r2, r3
print(bytes(ans))

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

1
2
3
4
5
6
7
8
9
10
11
12
# 0x55fc1fcad190
def check(p):
i = 0
lst_ch = 0
for i in range(len(p)):
c = ord(p[i])
p[i] = chr(c^lst_ch ^ (i+1))
lst_ch = c

k=[int.from_bytes(i,'little') for i in [b'aaaa',b'ssss',b'dddd',b'ffff']]

assert encrypt(p,k)=='f469358b7f165145116e127ad6105917bce5225d6d62a714c390c5ed93b22d8b6b102a8813488fdb'

分析encrypt函数逻辑。这里有个非常疑惑的点,Ruby里应该是没有ord这个函数的,不知道为什么识别出来的字节码带ord函数,同时直接执行这段字节码也会因为找不到ord函数符号而报错。不知道是不是出题人故意让字节码无法运行而这样设计的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def encrypt(t, key):
c=[]
# 每4字节一组进行pack
for n in range(0, len(t),8):
num1 = int(ord(t[n])) << 24
num1 += (ord(t[n+1])) <<16
num1 += (ord(t[n+2])) <<8
num1 += (ord(t[n+3]))
num2 = (ord(t[n+4])) << 24
num2 += (ord(t[n+5])) <<16
num2 += (ord(t[n+6])) <<8
num2 += (ord(t[n+7]))

# times 为YY 即
enum1,enum2=0
for _ in range(16):
enum1,enum2=enc_one(num1,num2,key)
c.append(enum1)
c.append(enum2)
return hex(c) # [sprintf("%.8x", num) for num in c]

enc_one函数接收两个四字节数和四个四字节的key做加密。加密算法是魔改的XTEA(魔改的地方有亿点多)。并且,enc_one只执行算法的一次循环,通过times控制循环轮次(轮数为YY轮,也就是16轮),通过GETUPVAR和SETUPVAR指令获取并修改上层变量来控制XTEA的v与sum(类似于lambda的变量闭包访问)。还原得到这部分加密逻辑

1
2
3
4
5
6
7
8
9
10
11
// rounds=16
void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], sum=0, delta=0x12345678;
for (i=0; i < num_rounds; i++) {
v0 += (((v1 << 3) ^ (v1 >> 5)) + v1) ^ (sum + key[((sum>>11)+1) & 3]);
sum += delta;
v1 += (((v0 << 3) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum+1) & 3]);
}
v[0]=v0; v[1]=v1;
}

最后,exp,先逆魔改的XTEA

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
#include <stdio.h> 
#include <stdint.h>
void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], delta=0x12345678, sum=delta*num_rounds;
for (i=0; i < num_rounds; i++) {
v1 -= (((v0 << 3) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum+1) & 3]);
sum -= delta;
v0 -= (((v1 << 3) ^ (v1 >> 5)) + v1) ^ (sum + key[((sum>>11)+1) & 3]);
}
v[0]=v0; v[1]=v1;
}
int main()
{
uint32_t v[]={0xf469358b, 0x7f165145, 0x116e127a, 0xd6105917, 0xbce5225d, 0x6d62a714, 0xc390c5ed, 0x93b22d8b, 0x6b102a88, 0x13488fdb },k[4]={'aaaa','ssss','dddd','ffff'};

for(int i=0;i<5;++i){
decipher(16, &v[i*2], k);
printf("%08x%08x",v[i*2],v[i*2+1]);
}

return 0;
}
// output
// 67080e02194b500d5c585f0b5e40461511470a08154211560d47491e04031d262771217626242765

逆异或

1
2
3
4
5
6
7
cipher = bytes.fromhex('67080e02194b500d5c585f0b5e40461511470a08154211560d47491e04031d262771217626242765')
cipher = list(cipher)
lst_ch=0
for i in range(len(cipher)):
cipher[i]^=lst_ch ^ (i+1)
lst_ch = cipher[i]
print(bytes(cipher))

得到 flag{6ad1c70c-daa4-11ec-9d64-0242ac1200}

可信计算

上次高考的Final考场,接触过了以下可信计算,可惜太仓促了,没玩明白是啥意思。这次在做两篇大阅读之余研究了以下。CTF的可信计算实则就是编程题,给你一套系统(往往是登录系统),该系统存在一些不安全的地方,比如逻辑上的漏洞,需要你编写代码修改该套系统的源码,重新编译并跑测试脚本,如果测试通过即可得到flag。

基于挑战码的双向认证

添加1. term计算MB’= SM3(key, rA||rB),并与MB比较,若一致则确认服务器端正确。

基于挑战码的双向认证_1
基于挑战码的双向认证_2

基于挑战码的双向认证2

term计算MA=SM3(key,rB),并将MA发送给Server

1
2
3
4
Memset(Buf,0,DIGEST_SIZE*2);
Strncpy(Buf,client_state->key,DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE,client_state->nonceB,DIGEST_SIZE);
calculate_context_sm3(Buf,DIGEST_SIZE*2,login_info->passwd);

基于挑战码的双向认证3

笑死,和第二题一模一样,把第二题的代码贴过去就出了。后来听说hacker_server里似乎多check了一些东西但是没启动。反正就是非预期了。这波啊,是高考试卷印错了,印了两道一样的题目。




浙ICP备19044916号-1