HGame 逆向复现

HGame

Reverse

eazyasm

根据题目可以知道,题目由汇编编写而来,直接拖入ida

可以看到关键部分(如上图)

程序将[si]先左移4位后再右移4位,把两个得到的结果相加,最后于0x17进行异或之后与一个字符比较,如此循环28次。

在汇编里不难发现对比的字符串:

因此我们可以编写对应的解题脚本了(c++):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
#include<cstring>
using namespace std;
int main() {
char data[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
};
char a,b;
for(int i=0;i<28;i++){
data[i]^=23;
a = data[i]>>4;
b = data[i]&0xf;
b=b<<4;
cout<<(char)(a+b);
}
return 0;
}

creakme

直接拖入ida进行反编译

乍一看像是有一个Base64,但实际上是Tea加密的魔改,在每次加密过程中多异或了一次sum

同时我们在编写脚本的时候要注意端序的问题,避免出错

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<iostream>
#include<cstring>
using namespace std;
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t delta=0x12345678;
uint32_t v0=v[0], v1=v[1], sum=delta * 32, i;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<32; i++) {
v1 -= sum ^ ((v0<<4) + k0) ^ (v0 + sum) ^ ((v0>>5) + k1);
v0 -= sum ^ ((v1<<4) + k2) ^ (v1 + sum) ^ ((v1>>5) + k3);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}

int main()
{
uint32_t v[]={1222194312u, 51123276u, 1391163586u, 3986482669u, 2921328102u, 3126465133u, 3482485930u, 1709241059u};
k[4]={0x44434241,0x48474645,0x4c4b4a49,0x504f4e4d};
decrypt(v, k);
decrypt(v + 2, k);
decrypt(v + 4, k);
decrypt(v + 8, k);
printf("%s", v);
return 0;
}

Flag Checker

apk文件直接拖入Jeb

可以明显的看到函数调用了一个RC4Base64,程序将输入的字符串先以密钥carol进行RC4加密之后把加密后的数据转换为Base64与后面一个字符串进行比较。

于是我们可以写对应脚本

1
2
3
4
5
6
7
8
9
10
11
from base64 import *
from Crypto.Cipher import ARC4

cipher = b64decode(b'mg6CITV6GEaFDTYnObFmENOAVjKcQmGncF90WhqvCFyhhsyqq1s=')

key = b'carol'
rc4 = ARC4.new(key)

rc4.decrypt(cipher)
# b'hgame{weLC0ME_To-tHE_WORLD_oF-AnDr0|D}'

猫头鹰是不是猫

拖入ida

可以简单的看到其逆向关键在于sub_1537这个函数,点进去

我们发现核心函数就是sub_1347,继续深入进去

函数不加修改的话可能会有一些难看,稍微修改一下就成上图啦,不难看出来函数主要在进行矩阵的乘法运算由输入的64位字符构成的矩阵与上面的deword_4140deword_8140两个64*64的矩阵进行乘法运算,之后将得到的值与unk_4040进行比较。

因此我们可以写一个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)

xD MAZE

题目描述是走迷宫,那我们只需要找到对应地图,按照对应方式进行走动就可以了

我们导出对应地图按64*64进行输出,之后按照对应规则行走就可以啦

走出迷宫所需要的输入套上hgame{}就是flag

upx magic 0

题目描述有upx壳但是实际上并没有,直接拖入ida进行反编译

通过字符串定位我们可以找到对应加密部分

于是可以直接通过爆破的方式来解决这个题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v14 = [0x8d68, 0x9d49, 0x2a12, 0xab1a, 0xcbdc, 0xb92b, 0x2e32, 0x9f59,
0xddcd, 0x9d49, 0xa90a, 0xe70, 0xf5cf, 0xa50, 0x5af5, 0xff9f, 0x9f59, 0xbd0b,
0x58e5, 0x3823, 0xbf1b, 0x78a7, 0xab1a, 0x48c4, 0xa90a, 0x2c22, 0x9f59, 0x5cc5,
0x5ed5, 0x78a7, 0x2672, 0x5695]


for i in range(32):
for j in range(28, 128):
v12 = 0
v12 = j << 8
for x in range(8):
if (v12 & 0x8000) != 0:
v12 = (v12 * 2) ^ 0x1021
else:
v12 *= 2
if (v12 & 0xffff) == v14[i]:
print(chr(j), end='')

fakeshell

我们直接拖入ida,找到关键函数

rc4_initrc4_crypt这两个函数需要稍微修改一下,让其变得更加易于阅读

不难发现这两个函数都是标准的RC4加密,但是我们对最开始的密钥happyhg4me!进行交叉引用会发现后期会被修改成w0wy0ugot1t

所以在编写脚本时需要注意一下密钥,同时加密后的数据也要注意按小端序来进行计算。

脚本如下:

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
#include<iostream>
#include<cstring>
using namespace std;

/*初始化函数*/
void rc4_init(unsigned char*s,unsigned char*key, unsigned long Len) {
int i=0,j=0;
char k[256]= {0};
unsigned char tmp=0;
for(i=0; i<256; i++) {
s[i]=i;
k[i]=key[i%Len];
}
for(i=0; i<256; i++) {
j=(j+s[i]+k[i])%256;
tmp=s[i];
s[i]=s[j];//交换s[i]和s[j]
s[j]=tmp;
}
}

/*加解密*/
void rc4_crypt(unsigned char*s,unsigned char*Data,unsigned long Len) {
int i=0,j=0,t=0;
unsigned long k=0;
unsigned char tmp;
for(k=0; k<Len; k++) {
i=(i+1)%256;
j=(j+s[i])%256;
tmp=s[i];
s[i]=s[j];//交换s[x]和s[y]
s[j]=tmp;
t=(s[i]+s[j])%256;
Data[k]^=s[t];
}
}


int main() {

unsigned char s[256] = { 0 }, s2[256] = { 0 };//S-box
char key[] = {"w0wy0ugot1t"};//密钥
//加密后的数据
char pData[]={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};
//加密后数据长度
unsigned long len = strlen(pData);
//unsigned long len = 32;
int i;


rc4_init(s, (unsigned char*)key, 11); //初始化

for (i = 0; i<256; i++) { //用s2[i]暂时保留经过初始化的s[i]
s2[i] = s[i];
}

rc4_crypt(s2, (unsigned char*)pData, len);//解密

for(int i=0; i<len; i++) {
cout<<pData[i];
}


return 0;
}

creakme 2

将程序拖入ida,通过简易的分析程序,大致可以看出是一个xtea加密

关键函数就是xtea_crypt,key则是v110-3

进入函数也不难发现是一个标准的xtea模式下的加密,但是我们观察汇编时会发现delta有一个除 0 异常

那么我们在编写脚本时需要添加上对应的异常处理机制

脚本如下

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
#include<iostream>
#include<cstring>
#include <stdint.h>
using namespace std;
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=0x9E3779B1;
for (i=0; i < num_rounds; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
if (sum >> 31 == 0){
sum ^= 0x1234567;
}
printf("%x\n", sum);
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
printf("%x\n", sum);
}

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=0x9E3779B1, sum=0xc78e4d05;
for (i=0; i < num_rounds; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
if (sum >> 31 == 0)
sum ^= 0x1234567;
sum -= delta;
//cout<<sum<<endl;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0]=v0; v[1]=v1;
}

int main()
{
//加密数据
uint32_t v[]={0x457E62CF, 0x9537896C, 0x1F7E7F72, 0xF7A073D8, 0x8E996868, 0x40AFAF99, 0xF990E34, 0x196F4086};
//密钥
uint32_t const key[4]={1,2,3,4};
//轮数
unsigned int rounds=32;
decipher(rounds, v, key);
decipher(rounds, v + 2, key);
decipher(rounds, v + 4, key);
decipher(rounds, v + 6, key);
printf("%s\n",v);
return 0;
}

upx magic1

看到题目表述有upx壳我们去试试看,发现提示不含upx壳,但是我们拖入进010可以卡到对应的upx!的压缩标识被修改成upx?了,我们将其还原(文件末尾还有两个修改点),便可以直接脱壳。

同样的我们采用爆破的方式来解这个题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v14 = [0x8d68, 0x9d49, 0x2a12, 0xab1a, 0xcbdc, 0xb92b, 0x2e32, 0x9f59,
0xddcd, 0x9d49, 0xa90a, 0xe70, 0xf5cf, 0xa50, 0x5af5, 0xff9f, 0x9f59, 0xbd0b,
0x58e5, 0x3823, 0xbf1b, 0x78a7, 0xab1a, 0x48c4, 0xa90a, 0x2c22, 0x9f59, 0x5cc5,
0x5ed5, 0x78a7, 0x2672, 0x5695]


for i in range(32):
for j in range(28, 128):
v12 = 0
v12 = j << 8
for x in range(8):
if (v12 & 0x8000) != 0:
v12 = (v12 * 2) ^ 0x1021
else:
v12 *= 2
if (v12 & 0xffff) == v14[i]:
print(chr(j), end='')

Answer’s Windows

通过字符串定位我们可以找到如下信息

base64_crypt是修改过名称的函数

我们从加密完后的数据看得出来这个不是普通的码表,我们在字符串里面可以找到

两个类似码表的数据,base64的码表长度为64位,我们截取前 64 位进行换表解密,即可得到对应flag,不过需要注意的是在使用线上网站进行解密时需要去除部分\,因为其表示转义,避免网站将其也识别为一个加密后的数据。

creakme3

题目描述中出现了IDA is not a panacea.提示我们换一个工具,我们采用ghidra这个工具来进行逆向分析。

我们稍微修复一下符号表可以在主函数看到

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
undefined4 main(void)

{
int fs;
int randnum;
int j;
int order_cnt;
int i;
int order [89];
int canary;

canary = *(int *)(fs + -0x7008);
memset(order,0,0x164);
printf("Welcome my whitegive re task! This is your flag: ");
do {
for (j = 0; j < 0x59; j = j + 1) {
randnum = rand();
order[j] = randnum % 0x59;
}
order_cnt = 1;
while ((order_cnt < 0x59 && (a[order[order_cnt + -1] * 2 + 1] <= a[order[order_cnt] * 2 + 1])))
{
order_cnt = order_cnt + 1;
}
} while (order_cnt != 0x59);
for (i = 0; i < 0x59; i = i + 1) {
putchar(a[order[i] * 2]);
}
if (canary == *(int *)(fs + -0x7008)) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}

可以看出来主函数就是把 a 中的数据进行排序,排完 0x59个数据后然后进行输出,我们看到 a 中的数据可以发现

这样排布的一串数据,我们根据其特点发现是每 8 位一组,同时主函数中的指针指向分别对应着 8 位中的后两位,而所有数据都要按照<=的顺序来进行排列后才会实现输出,我们推断前面的 4 字节就是对应字符,我们把后两个 16 进制进行组合就可以得到一串数字,大概就是对应的序号,我们提取对应数据,进行排序后输出,即可得到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
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
90
91
92
93
94
95
96
97
98
99
100
101
a = [
(0x30, 0x4E7D),
(0x30, 0x67BD),
(0x30, 0x7A48),
(0x30, 0x82A2),
(0x30, 0x933E),
(0x31, 0x9C18),
(0x32, 0x5AFF),
(0x32, 0x6CD7),
(0x32, 0xA6CA),
(0x32, 0xBD79),
(0x32, 0xCEBD),
(0x33, 0x324A),
(0x33, 0x3292),
(0x33, 0x3905),
(0x33, 0x4291),
(0x33, 0x5ADE),
(0x33, 0x6E9F),
(0x33, 0xA52A),
(0x33, 0xBE35),
(0x33, 0xCB63),
(0x35, 0x7F3B),
(0x38, 0x3914),
(0x38, 0xB2AD),
(0x39, 0x38DA),
(0x39, 0x4E50),
(0x39, 0x6A02),
(0x39, 0xB10F),
(0x42, 0x78E5),
(0x5F, 0x7EF6),
(0x5F, 0x89A3),
(0x5F, 0x8EBD),
(0x5F, 0x95E3),
(0x61, 0x73DA),
(0x64, 0x538C),
(0x64, 0x633B),
(0x64, 0x9E9C),
(0x64, 0xB78B),
(0x64, 0xC866),
(0x65, 0x32AE),
(0x65, 0x7679),
(0x66, 0x2AE7),
(0x66, 0x4D6A),
(0x66, 0x5708),
(0x66, 0x6610),
(0x66, 0xA258),
(0x66, 0xB80C),
(0x66, 0xC885),
(0x67, 0x710A),
(0x67, 0x7CF4),
(0x68, 0x3F76),
(0x68, 0x702B),
(0x68, 0xA3EE),
(0x68, 0xAD50),
(0x68, 0xBAC7),
(0x69, 0x4024),
(0x69, 0x8A22),
(0x69, 0xC055),
(0x6A, 0x2B52),
(0x6A, 0xC687),
(0x6B, 0x5F00),
(0x6B, 0xC417),
(0x6C, 0x6182),
(0x6D, 0x75DB),
(0x6E, 0x3C61),
(0x6E, 0x4996),
(0x6E, 0x5DC1),
(0x6F, 0x2D76),
(0x6F, 0x7D17),
(0x6F, 0xA91B),
(0x70, 0x9AED),
(0x72, 0x45D0),
(0x72, 0x8467),
(0x72, 0xAB5D),
(0x73, 0x5083),
(0x73, 0x6222),
(0x73, 0x8D93),
(0x73, 0x923A),
(0x73, 0x971E),
(0x73, 0xB4BA),
(0x73, 0xC785),
(0x74, 0x3558),
(0x74, 0x86BD),
(0x74, 0x9738),
(0x75, 0x3710),
(0x75, 0x9779),
(0x77, 0x2F3F),
(0x77, 0x44DD),
(0x7B, 0x78E1),
(0x7D, 0x9F42)
]

def takeSecond(elem):
return elem[1]

a.sort(key=takeSecond)

for i in a:
print (chr(i[0]), end='')
# fjow33etu938nhi3wrnf90sdf32nklsdf0923hgame{B0go_50rt_is_s0_stup1d}fh32orh98sdfh23ikjsdf32

hardasm

程序整体是AVX2指令集逆向但是根据校外的师傅 P1umH0 的patch方式来进行解题可能会更简单一些。

通过查看汇编可以了解到对应Flag长度为 32 位

同时我们构造一个长度为 32 的伪Flag,对程序进行测试

hgame{abcdefghijklmnopqrstuvwxy}

在程序中我们可以看到每一位都是有一个cmp函数进行对比正确与否,当遇到一个数据不对时就会往下跳转到Error处,在Flaghgame{}总是正确的,同时我们在调试过程中也可以发现每当有一个输入的字符正确在栈空间内便会有一个0xFF

同时我们也观察到程序是将Error字符串通过learcx,我们可以将其修改,改为输出对应判断后的栈内[rsp+70h+var_50]那么每有一个字符输入正确就会输出一个0xFF,我们可以通过其个数来判断输入的正确性。

我们将程序中的ErrorSuccess部分修改成下图

那么我们便可以通过Pythonsubprocess库来实现爆破

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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([./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

WOW

这个题目其实算是一个自解密?

进行动调我们可以看出来我们输入的Flag是在 3 个函数下进行加密,把加密后的数据存储于Buf2中间,在后面的if语句里存在一个Buf1而这个肯定是 Flag加密后得到的,里面的语句则是跳转到B站对应的视频,继续调试我们在下面的input中可以发现我们的输入,推断出程序可能是一个自解密?因而我们可以在动调时将程序内我们输入的字符串替换成相应的Buf1内的字符串,运行程序到return 0前,在 57 行的input处便可以看到Flag了。


HGame 逆向复现
https://equinox-shame.github.io/2022/03/14/HGame/
作者
梓曰
发布于
2022年3月14日
许可协议