2019 CISCN 线上赛

国赛初赛总算打完了。刚好是西湖论剑后一天,跟队友在杭州小宾馆里肝题。成绩还算可以吧,就是时间有点不够,做题慢了,不然还能出一道的。第一天1道easyGo,第二天四道题,这谁顶得住啊。安卓题还是不会做,要赶紧学安卓咯!

下载地址

easyGo

这题是由Go语言编译成的二进制文件。之前没做过go的题目,有点无从下手。参考了一下夜影师傅的一篇文章,用文章中类似的方法能在00000000004CEFB8找到提示语字符串”Please input you flag like flag{123} to judge:“,通过查找引用能在00000000004E1130附近找到其他字符串的引用。再次查找引用定位到sub_495150,这里是主要加密逻辑。不过单纯静态做也有点乱,动调之后看到在0000000000495318对比长度0x2A,在

.text:00000000004953A1 call sub_4023F0

下断点,RDX直接指向了flag

1
2
3
000000C0000181B0  66 6C 61 67 7B 39 32 30  39 34 64 61 66 2D 33 33  flag{92094daf-33
000000C0000181C0 63 39 2D 34 33 31 65 2D 61 38 35 61 2D 38 62 66 c9-431e-a85a-8bf
000000C0000181D0 62 64 35 64 66 39 38 61 64 7D 00 00 00 00 00 00 bd5df98ad}......

之后仔细看看也能看到base64密文在00000000004CF8B6,加密表在000000C00006C580

1
2
3
4
5
6
import base64
a = "6789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
base_fix = "tGRBtXMZgD6ZhalBtCUTgWgZfnkTgqoNsnAVsmUYsGtCt9pEtDEYsql3"
table = base_fix.maketrans(a, b)
print(base64.b64decode(base_fix.translate(table)))

bbvvmm

拿flag需要解出username和password,nc上去拿flag。

username

username用国密SM4加密,其中

key = 'DA98F1DA312AB753A5703A0BFD290DD6'.decode('hex')

cipher = 'RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y='

很明显是base64,但其实加密表也被替换了,在0000000000406C20

username的加密就是标准的国密加密。踩坑警告:不要用gmssl,用pysm4

1
2
3
4
5
6
7
8
9
10
11
import base64
import string
from pysm4 import *
a = "IJLMNOPKABDEFGHCQRTUVWXSYZbcdefa45789+/6ghjklmnioprstuvqwxz0123y"
b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
base_fix = "RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y="
key = int('DA98F1DA312AB753A5703A0BFD290DD6',16)
table = string.maketrans(a, b)
cipher = int(base64.b64decode(base_fix.translate(table)),16)
plain = decrypt(cipher,key)
print(hex(plain)[2:-1].decode('hex').decode('hex'))

password

虚拟机题

vm结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00000000 vm              struc ; (sizeof=0x4D0, mappedto_10)
00000000 reg dd 12 dup(?)
00000030 bytecode_base dd ?
00000034 zero dd ?
00000038 _ip dq ? ; offset
00000040 func handle 73 dup(?)
000004D0 vm ends
000004D0
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 handle struc ; (sizeof=0x10, mappedto_11)
00000000 ; XREF: vm/r
00000000 num dd ?
00000004 zero dd ?
00000008 func dq ? ; offset
00000010 handle ends

opcode反编译:

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
B0 19 00 00 00 			push 0x19
B5 0A pop rA
B2 0B push rB
B4 09 pop r9[rA]
B0 1A 00 00 00 push 0x1A
B5 0A pop rA
04 0B 09 mov r9[rA], rB
B0 1A 00 00 00 push 0x1A
B5 0A pop rA
B2 0B push rB
B4 09 pop r9[rA]
90 C2 00 00 00 jmp short 0xC2
91 nop
loc_00:---------------------------------
01 1A 00 00 00 0A mov r5, 0x1A
02 09 00 mov r0, r9[rA]
10 09 30 00 00 00 01 lea r1, r9[0x30]
B2 01 push r1
B2 00 push r0
C0 mov arg0, arg1[arg0]
B5 00 pop r0
B0 F4 FF FF FF push 0xfffffff4 ;-12
B5 0A pop rA
B1 00 push r0[rA]
B5 01 pop r1
01 1A 00 00 00 0A mov rA, 0x1A
B1 09 push r9[rA]
B5 00 pop r0
10 00 78 00 00 00 00 lea r0, r0[0x78]
70 00 FF 00 00 00 00 and r0, 0xff
50 00 18 00 00 00 00 shl r0, r0, 0x18
B2 00 push r0
B0 18 00 00 00 push 18
C8 shr arg1,arg0
B5 00 pop r0
B2 01 push r1
B2 00 push r0
C3 xor arg0,arg1
B5 00 pop r0
50 00 18 00 00 00 00 shl r0, r0, 0x18
B2 00 push 0x18
B0 18 00 00 00 push 0x18
C8 shr arg1,arg0
B5 00 pop r0
70 00 FF 00 00 00 01 and r0, r1, 0xff
01 19 00 00 00 0A mov rA, 0x19
02 09 00 mov r0, r9[rA]
11 01 00 00 lea r0, r1[r0]
loc_01:----------------------------------
B0 19 00 00 00 push 0x19
B5 0A pop rA
B2 00 push r0
B4 09 pop rA[r9]
01 1A 00 00 00 0A mov rA, 0x1A
B1 09 push r9
B5 00 pop r0
10 00 01 00 00 00 00 lea r0, r0[1]
01 1A 00 00 00 0A mov rA, 0x1A
04 00 09 mov r9[rA], r0
B0 1A 00 00 00 push 0x1A
B5 0A pop rA
02 09 00 mov r0, r9[rA]
86 00 06 00 00 00 00 cmp r0, r0, 0x06
88 00 26 00 00 00 jb loc_00
91 nop
FF exit

这部分算法很简单,不过直接看opcode会有点晕,建议直接调试,对着主要结构体看不难发现直接异或了0x78开始的6个字节,最后相加要为0。

远程打不通,可能是read读了回车。

1
2
3
4
5
6
7
from pwn import *
p = remote('39.106.224.151','10001')
print(p.recv())
p.sendline('badrer12')
print(p.recv())
p.send('xyz{|}')
print(p.recv())

strang_int

一开始不知道时啥架构的,后面放到linux里面file一下可以看到是DOS/MBR程序,直接用默认x86打开就行了。这题如果对操作系统比较熟悉的话做起来会上手一点。

刚开始的一段代码是十六进制的,第一行的jmp其实跳的就是下一行,因为MBR加载的位置在0x7C00,之后初始化了一些东西,不用太关心。

接下来0x01FE的代码是32进制的。注意到刚开始将ds赋值成了10h,所以接下来的寻址都要加0x100。

000001FE开始的一部分初始化了一部分寄存器,也不用太关心。接下来的逻辑是关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
seg000:0000022C                 xor     ebx, ebx
seg000:0000022E loc_22E: ; CODE XREF: seg000:0000025D↓j
seg000:0000022E nop
seg000:0000022F cmp ebx, 10h
seg000:00000232 jge short loc_25F
seg000:00000234 mov eax, 80000h
seg000:00000239 lea edx, ds:0D08h[ebx*4]
seg000:00000240 mov edx, [edx]
seg000:00000242 mov ax, dx
seg000:00000245 mov dx, 8E00h
seg000:00000249 mov ecx, 21h ; '!'
seg000:0000024E add ecx, ebx
seg000:00000250 lea esi, ds:128h[ecx*8]
seg000:00000257 mov [esi], eax
seg000:00000259 mov [esi+4], edx
seg000:0000025C inc ebx
seg000:0000025D jmp short loc_22E

在第一个0x10的循环内,修改了ds:128h开始的一些数据,把ds:0D08h和8E00复制过去。去道0xE08并不能看出什么,但是如果再往下看0x100个字节,到了0xF08能看到刚好连续16个word指向了16个地址。顺着这些地址也发现不了什么,但是把这些地址都加0x100后,刚刚好能对应16个类似函数的代码。这时大概能猜到一点东西:程序里的偏移不太对。

最终0x228+0x21*8这里存放了连续16个地址,映射到0xF08也就是`00000D7C开始的16个函数。

再来看这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
seg000:0000025F ; --------------------------------------------------------------------
seg000:0000025F
seg000:0000025F loc_25F: ; CODE XREF: seg000:00000232↑j
seg000:0000025F ; seg000:00000266↓j
seg000:0000025F call sub_268
seg000:00000264 int 21h ; DOS -
seg000:00000266 jmp short loc_25F
seg000:00000268
seg000:00000268 ; =============== S U B R O U T I N E ================================
seg000:00000268
seg000:00000268
seg000:00000268 sub_268 proc near ; CODE XREF: seg000:loc_25F↑p
seg000:00000268 mov edi, large ds:0B78h
seg000:0000026E lea edi, ds:0D48h[edi*4]
seg000:00000275 mov eax, [edi]
seg000:00000277 mov large ds:65h, al
seg000:0000027C mov ecx, [edi+4]
seg000:0000027F mov eax, [edi+8]
seg000:00000282 retn
seg000:00000282 sub_268 endp

同样的,这里的0B78h和0D48h和65h应当对应地址0x0D78和0xF48和265h。在F48看到一连串相似的结构体,每个的第一个字节都是0x21到0x30中的某一个。

继续看代码,从0xF48+[0xD78]*4读取一个字节,覆盖到0x265上,刚好覆盖了int 指令的操作数那一字节,联系之前的修改函数地址,不难得出,这道题通过修改中断向量表,用中断服务程序实现了一个虚拟机功能,用ecx和eax实现操作数的传递。其中00000D64开始的5个dword是寄存器,00000D78是ip。接下来就分析00000D7C开始的每个handler了。

用下面的代码可以得到opcode对应的汇编代码:

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
opcode = [[0x21, 0x0, 0x81], [0x27, 0x1, 0x1], [0x24, 0x1, 0x1], [0x23, 0x2, 0x0], [0x22, 0x3, 0x2], [0x21, 0x4, 0x8], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x27, 0x3, 0x3], [0x23, 0x4, 0x3], [0x24, 0x3, 0x2], [0x27, 0x2, 0x4], [0x24, 0x0, 0x2], [0x21, 0x1, 0x1], [0x25, 0x0, 0x1], [0x22, 0x1, 0x0], [0x21, 0x2, 0x81], [0x26, 0x1, 0x2], [0x21, 0x2, 0x9], [0x26, 0x1, 0x2], [0x21, 0x2, 0x9], [0x2D, 0x2, 0x1], [0x21, 0x0, 0x81], [0x22, 0x1, 0x0], [0x21, 0x2, 0x9], [0x25, 0x1, 0x2], [0x23, 0x3, 0x0], [0x23, 0x4, 0x1], [0x26, 0x3, 0x4], [0x21, 0x4, 0x7E], [0x2D, 0x4, 0x3], [0x21, 0x3, 0x1], [0x25, 0x0, 0x3], [0x25, 0x1, 0x3], [0x26, 0x2, 0x3], [0x21, 0x4, 0x5A], [0x2D, 0x4, 0x2], [0x2F, 0x0, 0x0], [0x30, 0x0, 0x0]]

def ana(a):
if a[0] == 0x21:
return ("mov r%d, 0x%x"%(a[1],a[2]))
elif a[0] == 0x22:
return ("mov r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x23:
return ("mov r%d, [r%d*4 +0E48h]"%(a[1],a[2]))
elif a[0] == 0x24:
return ("mov [r%d*4 + 0E48h], r%d"%(a[1],a[2]))
elif a[0] == 0x25:
return ("add r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x26:
return ("sub r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x27:
return ("xor r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x28:
return ("shl r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x29:
return ("shr r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x2a:
return ("and r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x2b:
return ("jmp r%d"%(a[1]))
elif a[0] == 0x2c:
return ("jz r%d, r%d"%(a[2],a[1]))
elif a[0] == 0x2d:
return ("jnz r%d, r%d"%(a[1],a[2]))
elif a[0] == 0x2e:
return ("pause")
elif a[0] == 0x2f:
return ("correct!")
elif a[0] == 0x30:
return ("wrong!")
else :
return ("error!")

for i in range(len(opcode)):
print('%02d'%i, ana(opcode[i]))

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
00            mov r0, 0x81
01 xor r1, r1
02 mov [r1*4 + 0E48h], r1
02 loc_03:
03 mov r2, [r0*4 +0E48h] ;plain
04 mov r3, r2
05 mov r4, 0x8
06 shl r3, r4
07 xor r2, r3
08 shl r3, r4
09 xor r2, r3
10 shl r3, r4
11 xor r2, r3
11
12 xor r3, r3
13 mov r4, [r3*4 +0E48h]
14 mov [r3*4 + 0E48h], r2
15 xor r2, r4
16 mov [r0*4 + 0E48h], r2
16
17 mov r1, 0x1
18 add r0, r1
19 mov r1, r0
20 mov r2, 0x81
21 sub r1, r2
22 mov r2, 0x9
23 sub r1, r2
24 mov r2, 0x9
25 jnz r2, r1 ;loc_03
25
26 mov r0, 0x81 ;memcmp
27 mov r1, r0
28 mov r2, 0x9
29 add r1, r2
30 loc_30: mov r3, [r0*4 +0E48h]
31 mov r4, [r1*4 +0E48h]
32 sub r3, r4
33 mov r4, 0x7e
34 jnz r4, r3 ;loc_42
35 mov r3, 0x1
36 add r0, r3
37 add r1, r3
38 sub r2, r3
39 mov r4, 0x5a
40 jnz r4, r2 ;loc_30
41 correct!
42 loc_42: wrong!

加密算法并不难,注意一开头将opcode的前4个字节清零,然后这里放每次循环得到的结果,下一个循环继续异或。

写出解密算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cipher = [0x65, 0x55, 0x63, 0x57, 0x01, 0x04, 0x53, 0x06, 0x49, 0x49, 0x49, 0x1F, 0x1F, 0x07, 0x57, 0x51, 0x57, 0x43, 0x5F, 0x57, 0x57, 0x5E, 0x43, 0x57, 0x0A, 0x02, 0x57, 0x43, 0x5E, 0x03, 0x5E, 0x57, 0x00, 0x00, 0x59, 0x0F]
key = [0 for i in range(4)]
plain = ''
for i in range(0,36,4):
temp = cipher[i:i+4]
for j in range(4):
temp[j] ^= key[j]
key = temp[:]
for j in range(1,4)[::-1]:
temp[j]^=temp[j-1]
for j in range(4):
plain+=chr(temp[j])
print(plain)

where_u_are

环境配置

一道wasm逆向题。wasm的汇编代码比x86的恶心多了。。。

用python本地可以开个web服务:

1
2
python -m SimpleHTTPServer 8080

chrome中输入localhost:8080,点击html进入web服务。按f12,在sources界面可以调试。第一次打开并没有加载好wasm,刷新一次可以看到wasm,可以看到左侧各种函数,中间是主要的汇编代码,右侧有调用栈,断点,变量监视等。断点设好后可以在右侧Scope里看到各种全局变量和局部变量。之后静态分析不动了可以来这里动调确认猜想。

另外跑web的服务的时候不清楚为啥点了确认之后会反复弹窗,点了取消猜出结果,而且只会输出两个浮点数,结果的提示语并不会输出。

刚刚说了,wasm的汇编及其恶心。先用wabt的wasm2c将wasm转成C代码。这里的C代码仍然究极恶心。我们可以用gcc+IDA帮我们优化,然后阅读IDA反编译出来的代码。

首先是wasm2c。编译好wabt后,在bin文件夹内能找到wasm2c,使用指令

1
2
wasm2c where_u_are.wasm -o where_u_are.c

得到wabt反编译的C代码,然后把wasm2c文件夹内的wasm-rt.h复制到同目录下,使用指令

1
2
gcc -c where_u_are.c

得到只编译不连接的.o文件,这时就可以拖进ida分析了。

算法分析

经过gcc+IDA的优化,代码总算多少能看了。main函数对应wasm的第25个函数即f25。

分析算法之前,先明白wasm的字符串常量全都保存在文件末尾,我们不用太关心它是怎样使用的,只要知道哪里用到了这串字符串即可。用010editor在文件末尾我们看到了几个字符串:"%s" "%lf %lf" "Correct!!" "Wrong!"

回到main函数,可以看到几个类似偏移的数据:0x1170u 0x1173u 0x1186u 0x117Cu。如果我们稍作计算,可以发现这些偏移和这些字符串是一一对应的。可以断定这里就是对应这些字符串,也就不难猜出f98和f97是类似printf和scanf的函数。那么主要逻辑应该在f23和f24里面。分析f23中的循环,先猜测f43是strlen,在循环内的f22中,看到另一个可疑的偏移0x400,在010editor中看到是一个长度32位的字符串,对应0-9a-z,字母部分空缺了几个。回到f23的循环,每个循环内部还有大小为5的循环,结合最后一行(>>j)&1,以及刚刚的字符串,应该能想到,这里将输入的字符按照字符串映射到0到31,然后把每一个bit取出来,这样的到了n*5个bit

来到f24,稍作分析这里的代码。

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
v9 = 0.0;
v10 = 0.0;
if ( ++wasm_rt_call_stack_depth > 0x1F4u )
wasm_rt_trap(7LL, a2, a3);
v8 = g10;
g10 += 272;
if ( g10 >= g11 )
Z_envZ_abortStackOverflowZ_vi(272LL, a2);
v7 = v8 + 112;
v11 = -180.0;
v12 = 180.0;
v13 = -90.0;
v14 = 90.0;
for ( i = 0; i < 50; ++i )
{
v6 = i32_load(Z_envZ_memory, (unsigned int)(4 * i + (_DWORD)a1));
if ( (i + 1) % 2 == 1 )
i32_store(Z_envZ_memory, (unsigned int)(4 * ((i + 1) / 2) + v7), v6);
else
i32_store(Z_envZ_memory, (unsigned int)(4 * (i / 2) + v8), v6);
}
for ( j = 0; j < 20; ++j )
{
if ( (unsigned int)i32_load(Z_envZ_memory, (unsigned int)(4 * j + v7)) == 1 )
{
v11 = v10;
v10 = (v10 + v12) / 2.0;
}
else if ( (unsigned int)i32_load(Z_envZ_memory, (unsigned int)(4 * j + v7)) == 0 )
{
v12 = v10;
v10 = (v11 + v10) / 2.0;
}
if ( (unsigned int)i32_load(Z_envZ_memory, (unsigned int)(4 * j + v8)) == 1 )
{
v13 = v9;
v9 = (v9 + v14) / 2.0;
}
else if ( (unsigned int)i32_load(Z_envZ_memory, (unsigned int)(4 * j + v8)) == 0 )
{
v14 = v9;
v9 = (v13 + v9) / 2.0;
}
}

大概就是将刚刚的n*5个bit按奇偶分成两组,然后进行这里的“算法”。最终校验的代码IDA分析的不是很好,不过大概也能看出结果要为25和175。

比赛时没看出是啥算法,就爆破了:

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
for i in range(1048576):
a = "{0:020b}".format(i)
v9 = 0.0
v10 = 0.0
v11 = -180.0
v12 = 180.0
# v11 = -90.0
# v12 = 90.0
for i in range(20):
if a[i]=='0':
v12 = v10
v10 = (v11 + v10)/2.0
else:
v11 = v10
v10 = (v10 + v12) / 2.0
if (v10-175)>=-0.0001 and (v10-175)<=0.0001:
print(a)
break
for i in range(1048576):
b = "{0:020b}".format(i)
v9 = 0.0
v10 = 0.0
v11 = -90.0
v12 = 90.0
for i in range(20):
if a[i]=='0':
v12 = v10
v10 = (v11 + v10)/2.0
else:
v11 = v10
v10 = (v10 + v12) / 2.0
if (v10-25)>=-0.0001 and (v10-25)<=0.0001:
print(b)
break
# a = '11111100011100011100'
# b = '10100011100011100011'
s = ''
for i in range(len(a)):
s += a[i]
s += b[i]
print(s)
flag = ''
table = '0123456789bcdefghjkmnpqrstuvwxyz'
for i in range(0,len(s),5):
flag += table[eval('0b' + s[i:i+5])]
flag = 'flag{' + flag + '}'
print(flag)

赛后发现这其实是二分查找,奇偶两组是两个区间[-180, 180]和[-90, 90],每一个bit的0或1决定二分的方向,那只要求出查找175和25过程中的方向即可。

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
def getbit(start, result, min_, max_):
res = []
current = start
for i in range(20):
if current<result:
min_ = current
res.append(1)
else:
max_ = current
res.append(0)
current = (min_+max_)/2.0
print(current)
assert abs(current-result)<=0.0001
return res
evenbit = getbit(0.0,175.0,-180.0,180.0)
oddbit = getbit(0.0,25.0,-90.0,90.0)
table = '0123456789bcdefghjkmnpqrstuvwxyz'
flag = ''
s = ''
for i in range(20):
s+=str(evenbit[i])
s+=str(oddbit[i])
print(s)
for i in range(0,len(s),5):
flag += table[eval('0b' + s[i:i+5])]
flag = 'flag{' + flag + '}'
print(flag)