1 . havetea

IDA:

main

左边是主函数里可以找到的,可以发现首先让输入key,且为16个长度,并且把输入的key分成两段进行了两次加密;在加密函数crypto里,是一个简单TEA运算,只不过IDA抽风把 +=delta 翻译成了 -=补码;crypto又把输入的数据截成两段进行运算,使用的key可以在程序里找到;之后用加密数据进行比较;

知晓key和加密后的数据使用对应解密方式解密:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= ((l<<4) + c) ^ (l + sum) ^ ((l>>5) + d);
l -= ((r<<4) + a) ^ (r + sum) ^ ((r>>5) + b);
sum -= delta;
}

解密后得到输入为:please_drink_tea

end

之后又让输入32长度的内容进行加密,并且用之前输入的16长度作为第二次加密cry的密钥;可以看出这次32长度的内容被分成了4段进行cry加密,而cry其实是和第一次的crypto差不多的TEA运算;加密完之后进行数据比较;

对应解密方式:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= (sum + key[(sum >> 11) & 3]) ^ (l + ((16 * l) ^ (l >> 5)));
sum -= delta;
l -= (sum + key[sum & 3]) ^ (r + ((16 * r) ^ (r >> 5)));
}

解密后得到:flag{c616454f52a6334273b5f455a10ef818}

2.maze

IDA:

main

通过字符串搜索,找到主要函数,可以看到通过输入的v2来与v3进行 domaze 函数运算;右图为 domaze 函数,可以看出这是个三线迷宫,迷宫整体由v3控制,输入的v2代表玩家移动方向;

maze

通过调试可知,这是个指针制作的迷宫,前24位每8位代表一个方向,第25位开始往后是控制数;比如地址0x112E86A 为1,对应0x112E860处的方向的控制数,当这个控制数为 1 的时候,根据 domaze 函数的计算规则可知,会触发 sub_4C6470 结束函数;而最后一个 0 是代表是否走过这个路口,走过之后会变成 1;

之后通过这个规则去逆推回去:(这些是地址低三位)

test

正着写回去便是:rrrrtltltlllltlltrtrrr

md5之后得到:flag{988b0f23719099efcbd66586a168bab9}

3.rota

IDA:

main

最上面图展示了最终的比较数据;中间左边的图则是一开始的base64编码,下面的图展示了base64的变种码表;中间右边的图展示了最后是生成了一个BOX,用BOX与base64编码后的内容进行加密;

中间有BOX的生成内容,但是无关紧要,因为生成的数据和输入的内容无关,所以是固定的,BOX也就是固定的;

所以只需要破解这个加密就能够得出最终结果;

crypto

以上为crypt函数的内容;

调试加分析加软磨硬泡得出爆破代码的核心内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for v10 in range(66):
v11 = v5
v12 = (v5 + BOX[(v3 + v10) & 0x3F]) & 0x3F
v13 = (v3 + 1) & 0x3F
v14 = BOX[((result + BOX[v12 + 64]) & 0x3F) + 128]
if(ans[j] == b64box[v14]):
print(chr(b64box[v10]))
BOX[192] = v13
break
elif(v10 == 65):
print('erorr')
exit(0)
if(not v13):
v5 = (v5 + 1) & 0x3F
BOX[193] = v5
if(((v11 + 1) & 0x3F) == 0):
result = (result + 1) & 0x3F
BOX[194] = result

然后和原代码一样,该循环几次循环几次,该有几个有几个;

得出base64编码后的内容为:cAJ7BzX+6zHrHwnTc/i7Bz6f6t6EBQDvc/xfHt9d6S9XX

再base64解码一遍:

base64

得到:flag{8cdd01062b7e90dd372c3ea9977be53e}

4.gocode

IDA:

main

gocode提示了这是go语言写的,所以搜索函数main_main找到主函数,通过右上角的图可知输入总长度为37,且是由 PCL{} 括起来的;

看到while(1)和switch 再根据题目名称,可知道这是个类似VM的东西,而根据docode变量可知第一站经过的便是右下角图中的函数,作用是把flag括起来的32个长度内容两两拼接成十六进制数,一共变成16个;

然后便是对不同指令码对应操作进行翻译:

do

逻辑就是每次经过AA开始判断,如果错误就退出程序,直到走完全部的code码就算成功;

把翻译的写成代码然后用z3来解:(重要代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s = Solver()
flag = [BitVec('flag[%d]' % i,64) for i in range(16)]

def check():
global ip
global code
global ex
if ip + 2 >= 374 :
print('ip out of range')
exit(0)
s.add(ex[code[ip+1]] == ex[code[ip+2]])

if ip >= 374:
if s.check() == sat:
print(s.model())
break
else:
print('no')
break

解出数字后换成十六进制再拼写在一起便得到:PCL{bdcc4f46d73ec09ee628633d2f227b47}

5.analgo

IDA:

main

第一眼看去会和上道题很像,也是类似虚拟机的构造,v23是指令,main_anal函数是虚拟机函数,下面的十六进制数是比较数据,判断输入的长度是42;

但是由于这个VM反编译出很多控制数不好分析各个指令码在做什么,同时发现输入是包含flag{}的,且每输入一个,比较结果也对应的变换一个,称之为一一对应;(蓝线是对应关系)

company

可以看到随着 flag{ 的输入,每输入一个,RCX 和 RDX 就相同一个字节;

那么可以使用之前hgame中 hardasm 题目的解法,将加密后的RCX值输出,与比较数据判断从而爆破;

patch0

先把判断搞掉,全nop,直接进入输出 wrong(SecondBC) 的地方;之后修改原程序比较的地方,改为将加密数据放到 SecondBC 这个地方:

patch1

但因为 SecondBC 是 rdata段的,拥有只读权限,所以要修改权限:

change

搜索 .rdata,将 40 00 00 40 改为 40 00 00 C0;

之后写代码爆破:(使用subprocess模组)

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
import subprocess

ans = 'this is answer' '''比较数据'''
hexs = '0123456789abcdef-' '''约束范围,输入其他的程序会提前退出'''
hexs = list(hexs)
for i in range(17):
hexs[i] = ord(hexs[i])

real_flag="flag{"
cur_index = 5 '''当前位置'''
k = 0

while cur_index < 42:
for i in hexs:
real_flag_arr = [0] * 42

for j in range(len(real_flag)): '''爆破储存位置'''
real_flag_arr[j] = ord(real_flag[j])
real_flag_arr[len(real_flag_arr)-1] = ord("}")

for j in range(len(real_flag_arr)-2,cur_index,-1):
real_flag_arr[j] = 48 '''未知位填充0'''

real_flag_arr[cur_index] = i
real_flag_arr_s = ''.join(chr(k) for k in real_flag_arr)
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\analgo.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode()) '''输入程序'''
p.stdin.close()
out = p.stdout.read() '''读取输出'''
out = list(out)

if (out[k] == ans[k] ):
real_flag += chr(i)
k += 1
cur_index += 1
print(real_flag)
break

由于这个程序是 8字节 8字节来比较的,所以手动多调几次,传入之前先 add rsi 8 获取之后的加密数据;

然后每轮都改下cur_index += 8;

然后得到:flag{568a3cdd-77e1-4c42-9fee-127e27a5744e}

6.puzzle

一开始发现是个加壳程序,跑一遍发现和UPX加壳很像,一开始是循环解码,然后进入原入口;用PE也显示其为UPX加壳,但是提示不能用指令脱壳;

使用十六进制查看器发现原UPX标记处被改为了vmp,将其改回并用指令对其脱壳;

unpack

之后进去过后看IDA:

main

在scanf之后的是一段循环,通过调试可以知道,这里允许通过 0 ~ 9 字符,并且一共输入56个,否则失败;

这段循环将输入的56个数字放到一些地址里,而地址原来就有些数据;填完之后一共是 9*9 = 81个数据;

然后来到判断 judge 函数,这里它将这81个内容作为参数传入;

经过调试呢,可以发现,输入的内容中,有些是不能重复的,而且不能有 0 ;这可以让想起数独游戏;

把里面给的数据拿出来做成 9 * 9 的数独表,然后进行求解:

solve

解出输入的56个内容为:76135283549798674164925733849217386455934161872359295314

输入源程序之后,便得到: flag{23c3cb3aedbbfdd009d1bf52e530676a}