这是2022年HGAME比赛的REVERSE复现,其中有思路借鉴于官方答案;

Week1:

easyasm

IDA:

main

可以在主要部分找到一个循环运算,而这里的 [si] 的间接地址就代表了输入flag的位置,es是保存了seg001数组的寄存器,这个循环的意思就是:循环28次,每次交换输入字符的前4位和后4位,最后异或23得到seg001的值;

按照这个思路反向运算写出的C语言:

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
#include<stdio.h>

int main()
{
unsigned int out[28] = {
0x91, 0x61, 0x01, 0xC1, 0x41, 0xA0, 0x60, 0x41, 0xD1, 0x21, 0x14, 0xC1, 0x41, 0xE2, 0x50, 0xE1,
0xE2, 0x54, 0x20, 0xC1, 0xE2, 0x60, 0x14, 0x30, 0xD1, 0x51, 0xC0, 0x17
}; //seg001数组
unsigned int i;
unsigned int in[28] = {0};
unsigned int a,b;
unsigned char an[28] = {0};
for(i = 0;i<28;i++) //运算
{
out[i] = out[i] ^ 23;
a = out[i] >> 4;
b = out[i] & 0xF;
b = b << 4;
in[i] = a + b;
}
for(i = 0;i<28;i++) //换ASCII
{
an[i] = in[i];
}
printf("%s",an);
return 0;
}

运行后得到flag:hgame{welc0me_to_4sm_w0rld}

creakme

IDA:

main

一来可以发现一个类似于base64的码表,但再看算法,发现并不是base64;

经过这个for运算后,会让运算后的值与v11进行比较;

所以解题思路就是去逆向这个for循环,但写出解题代码后发现并不正确;

最有意思的原因是仔细看v10,它的定义是_DWORD,也就是32位,运算中出现的v3也是32位;

所以让所有运算的数据成为32位的,再写代码:

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
#include<stdio.h>

int main()
{
unsigned int Table[17] = {
0x44434241, 0x48474645, 0x4C4B4A49, 0x504F4E4D, 0x54535251, 0x58575655, 0x62615A59, 0x66656463,
0x6A696867, 0x6E6D6C6B, 0x7271706F, 0x76757473, 0x7A797877, 0x33323130, 0x37363534, 0x2F2B3938,
0x0000003D //v10
};
unsigned int out[8] = {
0x48D93488, 0x030C144C, 0x52EB78C2, 0xED9CE5ED, 0xAE1FEDE6, 0xBA5A126D, 0xCF9284AA, 0x65E0F2E3 }; //v11
int i,c,j;
unsigned int a,b;

i = 0, c = 0x12345678 * 33;
for(i=0;i<7;i+=2) //运算
{
a = out[i];
b = out[i+1];
j = 32;
do
{
c -= 0x12345678;
b -= c ^ (c + a) ^ (Table[0] + 16 * a) ^ (Table[1] + (a >> 5));
a -= c ^ (c + b) ^ (Table[2] + 16 * b) ^ (Table[3] + (b >> 5));
--j;
}
while(j);
c = 0x12345678 * 33;
out[i] = a;
out[i+1] = b;
}
for(i=0;i<8;i++) //输出hex
{
printf("%X ",out[i]);
}
return 0;
}

输出后的hex转换为ascii后发现也不对,但很明显这就是flag的值:

1
2
HEX 6D616768 34487B65 5F797070 34633476 6E306974 7D21 0 0
flag 'magh4H{e_ypp4c4vn0it}!'

之后发现原因是小端序排列的原因;

调整下顺序便可以得到flag:hgame{H4ppy_v4c4ti0n!}

Flag Checker

这是个安卓apk,用jeb分析:

RC4

点进flag checker的MainActivity里,会发现有一个encrypt方法,使用了标准RC4加密;

main

之后出现了主函数,使用 ‘carol’ 做密钥来RC4加密,之后可以看到Base64的字样,那么说明使用了base64加密;

最后用两次加密的结果与 mg6CI 开头的那个字符串比较相同与否;

那么就可以反着逆,先解base64,再解RC4:

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

Sbox = [0] * 256

def rc4_init(s, key, len):
k = [0] * 256
j = 0
for i in range(len):
key[i] = ord(key[i])
for i in range(256):
s[i] = i
k[i] = key[i%len]
for i in range(256):
j = (j+s[i]+k[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp

def rc4_crypt(s, data, len):
i = 0
j = 0
t = 0
for k in range(len):
i = (i+1)%256
j = (j+s[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp
t = (s[i]+s[j])%256
data[k] = data[k] ^ s[t]

out = 'mg6CITV6GEaFDTYnObFmENOAVjKcQmGncF90WhqvCFyhhsyqq1s='
out = base64.b64decode(out)
out = list(out)
key = 'carol'
key = list(key)
rc4_init(Sbox,key,len(key))
rc4_crypt(Sbox,out,len(out))
for i in range(len(out)):
out[i] = chr(out[i])
out = ''.join(out)
print(out)

运行后得到flag:hgame{weLC0ME_To-tHE_WORLD_oF-AnDr0|D}

猫头鹰是不是猫

IDA:

function

最左边是main函数,右上是sub_55B565CA524E,右下是crypto;

一上来就会运行两次右上的函数,效果是用0,和1打印出猫和猫头鹰的样子(解题没什么用);

之后会让输入长度为64的字符串,将字符串经过crypto函数过后加密,加密后与cmp数组比较与否;

这里可以说明一下:cat,owl都是16384字节的数组,也就是4096 * 4;cmp是256字节的数组,也就是64 *4;

重点在于crypto函数,先后两次用sub_55B565CA5347函数与cat和owl两个数组参数将output变量加密;

进入sub_55B565CA5347函数:

sub_55B565CA5347

首先是经过第一个嵌套循环将输入的cat或者owl数组的每个数据都除10;

之后经过第二个嵌套函数将64个输入的字符,分别每个字符与对应的cat或者owl数相乘,再把64次运算的结果加在一起;

这样的一次总和,就成为了新的output数组里的一个字符;而注意这个函数将运算结果ans换到output里的时候,output数组下标有个4*n,这说明output最后的结果是DWORD类型的,也就是单个数据32位,一共64个数据;这也和256个字节(也就是256 * 8 = 2048 = 32 * 64(单位是位))的cmp比较数组对的上号;

所以让cmp数组变为DWORD型数据,先经过owl输入的运算,再经过cat输入的运算,最后得到输入的64个字符;

这样一看,就会发现:当cmp成为已知实数的时候,在owl输入的运算中,有64个未知数(正着运算前的output),和64组方程(64个未知数分别加上指定常数的和,一共64个);这是一个线性代数,且有唯一解;同理,在cat输入的运算中也是这样;

那么思路就是解两次线性代数,写代码的时候采用z3库解方程;

代码:

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
from z3 import *

cat = [] #cat和owl的数据过于庞大,就不显示出来了,都是DWORD型的,每个数字32位;
owl = []
data = [] #这是cmp已知数据;

for i in range(64): #owl和cat每个数除10;
for j in range(64):
cat[(64*i+j)] = cat[(64*i+j)] / 10
for i in range(64):
for j in range(64):
owl[(64*i+j)] = owl[(64*i+j)] / 10

s = Solver()
so = Solver()
out = [Int('out[%d]' % i) for i in range(64)] #z3设置未知数;
o = [Int('o[%d]' % i) for i in range(64)]

for i in range(64): #解第一组owl的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + out[j] * owl[(64*j+i)]
s.add(data[i] == sum)
if(s.check()==sat):
print(s.model())

out = [0] * 64
out = [] #输出后的数据再写入,进行第二轮运算;

for i in range(64): #解第二组cat的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + o[j] * cat[(64*j+i)]
so.add(out[i] == sum)
if(so.check()==sat):
int(so.model())

o = [0] * 64
o = [] #输出后的数据再写入,方便直接显示flag;

for i in range(64): #将运算后的数据换成ascii码输出最后的字符串;
o[i] = chr(o[i])
o = ''.join(o)
print(o)

多次运行补充数据后得到flag:hgame{100011100000110000100000000110001010110000100010011001111}

Week2:

xD MAZE

IDA:

main

一进主函数就能看见关键词的拼接:hgame{ + x + } ;可以通过v3,v4的赋值以及下面的 if 判断推测出x的长度是28;

右图是28个长度的v11循环运算,正好对应了 cin 输入的v11;有四个数字,对应四个方向,不同的方向会导致 j 的不同增长;

左下图是有个maze数组的判断; 如果不是空格(32),或者 j 超出了maze范围,就会失败,否则就成功;这个数组是由4096个 空格 和 # 符号组成;可以把空格想象成可以走的路,而 # 符号是围墙,通过 j 变量来操控人物走迷宫;

那可以写一个简单的迷宫算法,模拟 j 变量走通迷宫,并记录 j 变量如何增长,对应的 v11 输入是怎么样的情况;

代码如下:

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
maze = [] #4096个数据太多,不写出来

flag = [0] * 28 #输入的数据
j = 0
i = 0
n = [0] * 28

while(i<28):
if ((maze[j+1]==32)&(n[i]!=1)): #while是主体算法
flag[i] = 3 #每次判断四个方向,并用n变量记录这次选的方向
n[i] = 1 #当下一次这条路不通就退回去,并根据上次的n变量不选择上一次的路
j = j + n[i]
i = i + 1
continue
if ((maze[j+8]==32)&(n[i]!=8)):
flag[i] = 2
n[i] = 8
j = j + n[i]
i = i + 1
continue
if ((maze[j+64]==32)&(n[i]!=64)):
flag[i] = 1
n[i] = 64
j = j + n[i]
i = i + 1
continue
if ((maze[j+512]==32)&(n[i]!=512)):
flag[i] = 0
n[i] = 512
j = j + n[i]
i = i + 1
continue
i = i - 1
j = j - n[i]

for i in range(28): #while后就把flag都找出来了,下面在做字符串转换,以方便打印flag
if(flag[i]==0):
flag[i] = 48
if(flag[i]==1):
flag[i] = 49
if(flag[i]==2):
flag[i] = 50
if(flag[i]==3):
flag[i] = 51

for i in range(28):
flag[i] = chr(flag[i])

flag = ''.join(flag)
flag = 'hgame{' + flag + '}'

print(flag)

运行后得到flag:hgame{3120113031203203222231003011}

upx magic 0

这道题很怪,它的题目是和upx压缩保护有关,但用IDA并没有发现这是个包装程序;

IDA分析:

function

进入start函数后,可以找到一个叫 sub_400BBD 的函数,进入后,发现这就是要找的目标函数(右图);

输入32个字符,用输入的字符进行for运算,最后变成chan变量;

然后用chan变量和v14变量比较与否;那么可以用给的v14变量通过for的逆运算算回输入的字符;或者爆破;

代码如下:

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
d = [0] * 33 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<32):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[32] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(32):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-UPxmAG|C_@Nd~crC16}

fake shell

运行程序后,是一个模仿 Linux 终端的玩意儿,打开flag.txt需要sudo密码:

shell

没办法就开IDA:

function

从左往右看,第一张图是main函数,找到使用sudo命令后运行的函数:sub_559D9EE5B9E9;

第二张是进入这个函数后的内容,可以看到需要输入密码v4,32个字符长度,然后进入加密函数:rc4;

第三张就是rc4的加密了,先用rc4_init函数和已知密钥:aHappyhg4me字符串创建Sbox,再用rc4_crypto加密输入数据,最后用加密后的数据与v7变量比较与否;

因为rc4加密的异或运算,导致加密后的数据再用原函数加密一遍就可以变回加密前的数据;

所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data = [0xB6, 0x94, 0xFA, 0x8F, 0x3D, 0x5F, 0xB2, 0xE0, 0xEA, 0x0F, 0xD2, 0x66, 0x98, 0x6C, 0x9D, 0xE7, 0x1B, 0x08, 0x40, 0x71, 0xC5, 0xBE, 0x6F, 0x6D, 0x7C, 0x7B, 0x09, 0x8D, 0xA8, 0xBD, 0xF3, 0xF6]
box = [0x6C, 0xA8, 0x54, 0xD3, 0xD1, 0xFC, 0x87, 0x2F, 0xF7, 0xE4, 0x74, 0x5F, 0x1B, 0xA4, 0x22, 0x6A, 0xEF, 0x17, 0x4F, 0x04, 0xB4, 0x3D, 0x40, 0x36, 0xA0, 0x32, 0x5B, 0x1D, 0x8A, 0x57, 0xAD, 0xFD, 0x7D, 0xF6, 0x48, 0xE2, 0x7F, 0xD4, 0x1A, 0x1F, 0x15, 0x9F, 0xC0, 0x89, 0xBB, 0x3F, 0x3A, 0x73, 0x28, 0x00, 0x1C, 0xA3, 0x2E, 0x6D, 0x68, 0xC5, 0x0E, 0x18, 0x90, 0x0D, 0x0A, 0xD6, 0x4D, 0x45, 0xFF, 0xDB, 0x11, 0xDA, 0x95, 0x53, 0x5A, 0x72, 0x2D, 0xA1, 0x0F, 0x50, 0x7A, 0xB0, 0xBC, 0x8D, 0xBA, 0xCC, 0x56, 0x88, 0xCF, 0xCB, 0xC7, 0x26, 0x80, 0x42, 0x9E, 0x7C, 0x07, 0xF0, 0xE9, 0x49, 0xDF, 0x71, 0x98, 0x6B, 0xB1, 0xB5, 0xE7, 0xF8, 0x67, 0x24, 0xBF, 0x46, 0x77, 0xE5, 0x8E, 0x0B, 0x29, 0x63, 0x85, 0x34, 0x62, 0xD2, 0x4E, 0xED, 0xA7, 0x41, 0x8C, 0xD7, 0x43, 0x60, 0xD9, 0xB8, 0xEC, 0xD5, 0xB6, 0x92, 0x08, 0xC1, 0x5D, 0x86, 0x0C, 0x44, 0x7B, 0xCA, 0xAB, 0xE0, 0x96, 0x83, 0x2A, 0x3C, 0xB7, 0xDD, 0xDC, 0x3B, 0x19, 0x99, 0xD0, 0xA9, 0xDE, 0xC6, 0x02, 0xD8, 0x5C, 0xF3, 0x52, 0x9B, 0x09, 0x64, 0x30, 0x91, 0xC9, 0xC3, 0xF2, 0x2C, 0x25, 0xE6, 0x9C, 0xEE, 0x10, 0x13, 0x81, 0x20, 0x59, 0xFE, 0xFB, 0xAA, 0xCD, 0x16, 0x27, 0x76, 0xFA, 0x33, 0xB9, 0xE1, 0x1E, 0xF5, 0x4C, 0xEA, 0xF1, 0xBE, 0xF4, 0x05, 0xA2, 0x93, 0x2B, 0xA5, 0x12, 0xA6, 0x21, 0xE8, 0x51, 0xCE, 0x79, 0x6F, 0x66, 0x9D, 0x84, 0x01, 0x5E, 0x8F, 0x6E, 0x9A, 0x3E, 0xAE, 0x7E, 0x06, 0x14, 0xEB, 0x82, 0xE3, 0x97, 0x69, 0x35, 0x23, 0x61, 0xB2, 0xB3, 0x94, 0x03, 0x39, 0xC4, 0x47, 0xBD, 0xAC, 0x78, 0x55, 0xAF, 0x37, 0xC2, 0x4A, 0x70, 0x65, 0x75, 0x8B, 0x31, 0xC8, 0x4B, 0x38, 0x58, 0xF9]
#data是v7比较数据,box是偷懒调试经过rc4_init函数后直接复制出来用
v5 =0
v6 = 0

for i in range(32): #rc4加密主体
v5 = (v5+1) % 256
v6 = (v6+box[v5]) % 256
v4 = box[v5]
box[v5] = box[v6]
box[v6] = v4
data[i] = data[i] ^ box[(box[v5]+box[v6])%256]

for i in range(32): #变字符串得到sudo密码
data[i] = chr(data[i])
data = ''.join(data)
print(data)

结果发现sudo密码就是flag:hgame{s0meth1ng_run_bef0r_m4in?}

creakme2

IDA:

main

这是main函数,首先定义了一个num数组,然后输入32个字符,接着经过四次crypto加密,而从定义变量可以看出,input只有8个长度,所以这四个加密的意义就是把32个字符分成了四分,每份8个字符进入crypto单独加密,之后再拼凑在一起;最后把运算出来的字符串与cmp变量比较与否;

再来看crypto函数:

crypto

输入的a1是稳定的32,用于循环控轮次;输入的a2是8个字符;输入的a3是num数组;

这个for运算,是让前4个字符用后4个字符和num以及v4运算得到;让后4个字符通过变化后的前4个字符和v4以及不变的num运算得到;这个过程持续32轮;

那么可以倒着来算,把数据算回来;用给定的比较数组cmp当已知,先算后4个字符,因为变化后的v4和前4个字符是已知的,就可以减回去;之后减一遍v4,就可以用还原的后4个字符与v4算前4个字符了,然后持续32轮;

这个算法肯定没问题,但就是算不对,因为最终的v4应该变为0,但运算结果v4却不是0;

是什么原因呢?打开汇编层代码进行查找,于是就发现了一个神奇的东西:

asm

这一部分是翻译为了 v4 += 9E3779B1;[rsp+58h+var_38] 这个地址,代表的就是v4;

可以看出上半部分的运算是没问题的,确实翻译成伪代码就是这个意思;但之后它把eax右移31位(这个时候eax等于v4)得到符号位,然后传送给ecx,之后用ecx来做除法,如果这个数是正数,那么符号位为0,这样算肯定有问题;所以就会有异常处理;

一旦出现了异常处理,就会使得下半部分的运算进行,把v4异或上0x1234567;这就是为什么光看伪代码找不出错误原因的地方;按照它这个思路,可以写出如下代码:

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
#include<stdio.h>

unsigned int data[8] = {
0x457E62CF, 0x9537896C, 0x1F7E7F72, 0xF7A073D8, 0x8E996868, 0x40AFAF99, 0x0F990E34, 0x196F4086
}; //data是cmp比较数据
unsigned int num[10] = {1,2,3,4,5,6,7,8,9,0};
unsigned int v4;
unsigned int v5,v6;
unsigned int sign; //表示符号的数

int main()
{
int i,j;
int box[32];
unsigned int flag[32];
char ascii[33];
for(j=0;j<8;j+=2) //分四组运算
{
v5 = data[j];
v6 = data[j+1];
v4 = 0;

for(i=0;i<32;i++) //算出v4的32轮中的情况并记录,以方便倒着算;
{
v4 += 0x9E3779B1;

sign = v4 >> 31;

if(sign==0) //模拟异常处理
{
v4 ^= 0x1234567;
}
box[i] = v4;
}

for(i=31;i>=0;i--) //倒着算
{
v6 -= (num[(box[i] >> 11) & 3] + box[i]) ^ (v5 + ((v5 >> 5) ^ (16 * v5)));

v5 -= (num[box[i-1] & 3] + box[i-1]) ^ (v6 + ((v6 >> 5) ^ (16 * v6)));
}

data[j] = v5;
data[j+1] = v6;
}

for(i=0;i<32;i++) //下面在为输出flag字符串准备,因为data是DWORD型数组
{
flag[i] = (data[i/4] << 8*(4-((i+1)%4))) >> 24;
}
for(i=0;i<32;i++)
{
ascii[i] = flag[i];
}
ascii[32] = '\0';
printf("%s",ascii);
return 0;
}

运行后得到flag:hgame{SEH_s0und5_50_1ntere5ting}

upx magic 1

这个是用upx加壳的内容,用checksec就可以知道,但upx -d 命令对它没用;

upx1

用十六进制查看器搜索upx后发现原标准标记upx! 被改成了upx? 所以搜搜机器没有发现它是一个upx加壳;

将改过的标记改回来后脱壳:

upx2

之后用IDA:

function

左上是start起始位置,进入sub_400B8D函数,可以从文中知道这个就是main函数;

可以看出,输入的flag长度需要为37,然后进行for循环的运算,之后与v14进行比较与否,这系列操作就和upx0一模一样了;

解题代码:

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
d = [0] * 38 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00005ED5, 0x00003C03, 0x00007C87, 0x00002672, 0x0000AB1A, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<37):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[37] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(37):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-rea1_UPxmAG|C_@Nd~crC16}

Week3:

Answer’s Windows

这是个模拟 Windows面板,需要输入密码;

可以想到用IDA搜索显示正确或错误的句子,而IDA里搜不出中文,可以想到是用了图片;

locked

使用IDA搜索:true,right,false,lose,win,wrong 这些特殊字样,会发现两个图片叫做right和wrong,跳转之后,会发现if的比较字符串:

main

而根据静态分析和动态调试可以发现它将输入的字符串进行了base64加密,又因为反调试将调试中的base64码表换成了错误的,导致怎么也得不到比较用的字符串;但能够发现比较数据有常规base64所没有的奇怪符号;于是去字符串列表里搜索123456789(赌码表含连续数字)连着的数据,可以发现所需要的码表;

于是有了下面两组数据:

1
2
3
//  ;'>B<76\=82@-8.@=T"@-7ZU:8*F=X2J<G>@=W^@-8.@9D2T:49U@1aa  比较数据

// !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a 码表

为什么码表以a结尾呢?因为比较数据后面跟着两个a,和原base64加密结果后面跟= 异曲同工;

还有个坑,可以看到图中的比较数据和得出的比较数据有所不同,原因在于 \ 是转义符,需要消掉解密,在没消掉之前,可以得到解除的明文是hgame开头,可后面是乱码;

此时解密得到flag:hgame{qt_1s_s0_1nteresting_so_1s_b4se64}

creakme3

这是PCC架构的文件,和以往的arm,x86有所不同,由PowerPC编译,所以IDA不能分析,linux不能运行;

此题有提示,使用Ghidra分析便可得知主体逻辑;

Ghidra下载:https://github.com/NationalSecurityAgency/ghidra

此时可以看到main的逻辑:

main

可以看到最中间的while()函数,他在给关于a的偏移进行排序,点击进入a后,发现是.data节中的一些数据,它们有规律:每8个字节为一个单位,初始是ascii码范围的hex值,加上4个字节后,变为一个比较大的数字;而在中间的while()函数中,排序是乘上8加了4,所以在利用较大的数字比大小,而最后putchar()进行输出,只是乘上了8,可以想到这个main函数的逻辑便是由每个单位的较大数字排序,最后输出排序后的ascii码,这应该便是flag了;

代码:

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
#include<stdio.h>

struct ai //chascii码,index代表较大数字
{
int ch;
int index;
};
struct ai a[89] =
{
{ 48, 20093 }, { 48, 26557 }, { 48, 31304 }, { 48, 33442 }, { 48, 37694 }, { 49, 39960 }, { 50, 23295 }, { 50, 27863 }, { 50, 42698 }, { 50, 48505 }, { 50, 52925 }, { 51, 12874 }, { 51, 12946 }, { 51, 14597 }, { 51, 17041 }, { 51, 23262 }, { 51, 28319 }, { 51, 42282 }, { 51, 48693 }, { 51, 52067 }, { 53, 32571 }, { 56, 14612 }, { 56, 45741 }, { 57, 14554 }, { 57, 20048 }, { 57, 27138 }, { 57, 45327 }, { 66, 30949 }, { 95, 32502 }, { 95, 35235 }, { 95, 36541 }, { 95, 38371 }, { 97, 29658 }, { 100, 21388 }, { 100, 25403 }, { 100, 40604 }, { 100, 46987 }, { 100, 51302 }, { 101, 12974 }, { 101, 30329 }, { 102, 10983 }, { 102, 19818 }, { 102, 22280 }, { 102, 26128 }, { 102, 41560 }, { 102, 47116 }, { 102, 51333 }, { 103, 28938 }, { 103, 31988 }, { 104, 16246 }, { 104, 28715 }, { 104, 41966 }, { 104, 44368 }, { 104, 47815 }, { 105, 16420 }, { 105, 35362 }, { 105, 49237 }, { 106, 11090 }, { 106, 50823 }, { 107, 24320 }, { 107, 50199 }, { 108, 24962 }, { 109, 30171 }, { 110, 15457 }, { 110, 18838 }, { 110, 24001 }, { 111, 11638 }, { 111, 32023 }, { 111, 43291 }, { 112, 39661 }, { 114, 17872 }, { 114, 33895 }, { 114, 43869 }, { 115, 20611 }, { 115, 25122 }, { 115, 36243 }, { 115, 37434 }, { 115, 38686 }, { 115, 46266 }, { 115, 51077 }, { 116, 13656 }, { 116, 34493 }, { 116, 38712 }, { 117, 14096 }, { 117, 38777 }, { 119, 12095 }, { 119, 17629 }, { 123, 30945 }, { 125, 40770 }
};

int main()
{
for(int i=0;i<89;i++)
{
for(int j=0;j<89;j++)
{
if(a[i].index<a[j].index)
{
struct ai temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
for(int i=0;i<89;i++)
{
putchar(a[i].ch);
}
return 0;
}

运行后得到flag:hgame{B0go_50rt_is_s0_stup1d}

hardened

使用jeb分析发现只有SecShell,应该是被加壳了;

使用BlackDex进行脱壳;(下载地址:https://github.com/CodingGay/BlackDex)

脱壳后会在java代码中发现加载了 libenc.so 库,调用了两个本地方法,其中加密部分就在这个库里;

查看 AES 加密 key、iv 的引用可以发现混淆加密的部分;

unshell

字符串混淆的解密可以用frida;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//script.js
function print_string(addr) {
var base_hello_jni = Module.findBaseAddress("libenc.so");
var addr_str = base_hello_jni.add(addr);
console.log("addr:", addr, " ", ptr(addr_str).readCString());
}

/* frida -U -f com.example.secretsong -l C:\Users\chz\Desktop\script.js --no-pause
____
/ _ | Frida 14.2.12 - A world-class dynamic instrumentation toolkit | (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit . . . .
. . . . More info at https://frida.re/docs/home/
Spawned `com.example.hardened`. Resuming main thread!
[M5 Note::com.example.hardened]-> print_string(0x31070)
addr: 200816 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/= [M5 Note::com.example.hardened]-> print_string(0x31020)
addr: 200736 JUST_A_NORMAL_KEY_FOR_YOU_TO_DEC
[M5 Note::com.example.hardened]-> print_string(0x31050)
addr: 200784 you_find_me!!!!! */

之后异或回去就能解字符串;

解密得到flag:hgame{cONGraTUl4T|0N5!N0w_yoU_C4n_eN?OythEMUsIc}

decrypt

fishman

原码中用了 init 和 check 函数;

使用IDA分析fishman库,字符串搜索 init 以及 check:

init

可以找到init和check的函数入口,进入过后,可以在init函数里发现一些运算,根据搜索运算所给的数据和格式,可以知道这是blowfish加密;

此时根据加密规则可以找到,密钥就是aLetUD:LET_U_D;

而比较数据就存在于check函数里,果不其然,会输出win或者lose:

check

于是使用blowfish的解密库解密:(blowfish库下载:https://github.com/xtbanban/blowfish)

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 "blowfish.h"

/*
需要加入自带 blowfish.C 代码
*/

void main()
{
long long out[4] = {
-5409505419495256385LL,
1428749241468231806LL,
6435326525834898959LL,
2019834963917240364LL
};
BLOWFISH_CTX ctx;

int i;
Blowfish_Init(&ctx, (uint8_t *)"LET_U_D", 7); //初始密钥
for(i=0; i<4; i++)
{
Blowfish_Decrypt(&ctx, (char *)out + 8*i, (char *)out + 8*i + 4);
}
printf("%s\n",(char *)out);
}

运行后得到flag:hgame{D0_y0u_re411V_11k3_9Vthon}

Week4:

( WOW )

IDA分析:

main

可以看到main函数里先输入容纳40长度的内容,然后进行一次for循环里的加密,从input变成output;

之后与比较数据比较与否,输出错误或者正确,但后面还有个for循环,后面这个是把output输入,变成input,猜想一下它可能是解密;

尝试将output直接修改为cmp的数据,查看解密内容:

answer

由此可得flag:hgame{WOWOW_h@ppy_n3w_ye4r_2022}

补充:这个题的加密是不常规的DES加密,DES加密为对称加密,即一组密钥即可完成加解密;核心思想为扩散混淆,扩散即为将明文的1个字符扩展为密文中的多个;混淆即为算法多层,让密钥和密文的关联更难找到;

server

IDA:

main

可以看到函数列表里有个叫 main_main 的主函数,说明这是go语言;

其中还有个函数叫做 main_encrypt ,既然是加密就应该有数据才对,打开汇编模式,可以发现伪代码中看不到的数据;

根据汇编更改 math_big函数的输入参数:

1
__int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>)

可以变为:

change

也可以用同样的原理,右键其他无参函数,点 set call type ,然后更改,最后可以修复encrypt函数看到原理:

for and XOR

发现在经过之前的加密后,进行了异或;且长度为153;

总之,根据标记符号函数名称和数据判断,这是RSA加密加上异或;

代码:

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
from Crypto.Util.number import * 
#pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple
import gmpy2
#pip install gmpy2

a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]

for j in range(256): # 爆破
num = j
a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
try: # 将无法转换的情况直接丢弃,说明该情况必然不是flag
enc = int("".join(map(chr,a)))
except ValueError:
continue

p = 92582184765240663364795767694262273105045150785272129481762171937885924776597
q = 107310528658039985708896636559112400334262005367649176746429531274300859498993
e = 950501
r = (p-1)*(q-1)
d = gmpy2.invert(e,r)
m = pow(enc,d,p*q)
print(long_to_bytes(m))

运行得到flag:hgame{g0_and_g0_http_5erv3r_nb}

ezvm

IDA:

main

可以看出主函数输入后进入switch;

翻译出每个case对应操作,恢复程序逻辑:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
	VM_START

OP_PUSH_NUM |
OP_POP_EBX |'\n'

OP_PUSH_NUM |
OP_POP_ECX |-5
.input:
OP_STREAM_IN |eax
OP_PUSH_EAX |
OP_ADD |edx,1
OP_CMP |eax,ebx
OP_JNE |ecx .input

.check_len:
OP_SUB |edx,1
OP_PUSH_NUM |
OP_POP_EBX |32

OP_PUSH_NUM |
OP_POP_ECX |47

OP_MOV |eax,edx
OP_PUSH_EAX
OP_CMP |eax,ebx
OP_JNE |ecx .exit

OP_PUSH_NUM
OP_POP_ECX |-10

OP_PUSH_NUM
OP_POP_EDX |0
.enc:
OP_GET_MEM |eax,stack[edx]
OP_PUSH_NUM |
OP_POP_ESI |xor_key
OP_MUL |eax,2
OP_XOR |eax,esi
OP_SET_MEM |stack[edx],eax
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .enc

OP_PUSH_NUM
OP_POP_EDX |0

OP_PUSH_NUM |-17

OP_PUSH_NUM |21
.check
OP_PUSH_NUM |
OP_POP_EBX |cipher
OP_GET_MEM |eax,stack[edx]
OP_CMP |eax,ebx
OP_POP_EAX
OP_PUSH_EAX
OP_POP_ECX
OP_JNE |ecx .exit
OP_POP_ECX
OP_POP_EBX |
OP_PUSH_EBX |
OP_PUSH_ECX |
OP_PUSH_EAX
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .check


OP_PUSH_NUM
OP_POP_ECX |2

OP_PUSH_NUM |
OP_POP_EBX |0

OP_PUSH_NUM
OP_POP_EDX |-6
.success:
OP_PUSH_NUM |
OP_POP_EAX |str

OP_CMP |eax,ebx
OP_JE |ecx .exit
OP_STREAM_OUT |eax
OP_JMP |edx .success

.exit:
VM_EXIT

由此代码:

1
2
3
4
5
6
7
8
9
10
xor_keys = [94, 70, 97, 67, 14, 83, 73, 31, 81, 94, 54, 55, 41, 65, 99, 59, 100, 59, 21, 24, 91, 62, 34, 80, 70, 94, 53, 78, 67, 35, 96, 59]

plain_text = []

cipher = [142, 136, 163, 153, 196, 165, 195, 221, 25, 236, 108, 155, 243, 27, 139, 91, 62, 155, 241, 134, 243, 244, 164, 248, 248, 152, 171, 134, 137, 97, 34, 193]

for i in range(0,32):
plain_text.append(chr((cipher[i]^xor_keys[i])//2))

print(''.join(plain_text))

运行可得flag:hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}

hardasm

IDA:

main

可以看到伪代码的main函数显示的也是__asm,汇编指令;

根据汇编指令可以搜索出,这是AVX2 指令集;

具体思路就是给 ymm1~ymm7 寄存器赋值,然后进行运算,ymm0是输入数据;

但运算过程过于庞大,而且不会这个指令集;又因为ymm0到比较与否的区域始终没有改变,所以可以爆破;

在比较数据的地方下个断点,调试的时候输入 hgame{aaaaaaaaaaaaaaaaaaaaaaaaa} 共32个字符串(因为scanf的格式为%32s),这时候可以发现:[rsp+70h+var_50] 的地方从原来的输入数据,变为了6个0xFF;

cmp

这是因为比较后,因为前6个字符输入的都是正确的,而其他错误,所以其他的都成为了false,0;

那么就可以使用python的 subproccess 子程序模块,在运行脚本时对该程序进行操作,使其循环找到32个0xFF;

subprocess菜鸟教程:https://www.runoob.com/w3cnote/python3-subprocess.html

既然要找0xFF,那么需要从子程序中返回0xFF才行;

根据程序的输出可知:通过rcx将 error字符串或success字符串 打印出:

print

那可以patch程序,将[rsp+70h+var_50]处的内容传递给rcx,以此打印;

代码:

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
import subprocess
real_flag="hgame{" #绝对正确的前6个字符
cur_index=6 #当前爆破的位置
while cur_index<32:
for i in range(32,128): #当前爆破的位置上的字符
real_flag_arr = [0] * 32

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): #除了当前爆破的位置,其他位置 上都设置为32(空格)
real_flag_arr[j]=32

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\\hardasm.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()

if len(out)>cur_index: #判断程序打印出的0xFF的个数是否增加,增加则说明当前爆破的位置 上的字符设置的是正确的
real_flag+=chr(i)
cur_index+=1
print(real_flag)
break

运行得flag:hgame{right_your_asm_is_good!!}

总结

这次hgame的逆向之旅收获了很多,应该可以说有了叫baby和easy题的题感;更多的包括JAVA的语法,z3的模型可运算,UPX的标签规则,POWERPC的PCC架构,Blowfish标准河豚算法,RSA大素数算法,DES加密的了解;有两道题并不是特别理解,一道是第三周的hardened,另一道就是第四周的ezvm了;前一道没有可用手机或是kali虚拟机,所以无从下手;另一道虚拟机因为还不太理解虚拟机的构造,所以无法翻译;

因此接下来的任务就是抽空把虚拟机的实验完成,以及ROOT手机到手;