0x00 rushB
0x01 代码分析
首先进行查壳,无壳,64位程序。拖入IDA和虚拟机。
IDA分析代码发现代码存在混淆,但是还是可以捋出来逻辑。
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // eax
size_t v4; // rax
int v5; // ecx
char v6; // al
int v7; // ecx
int v9; // [rsp+3Ch] [rbp-404h]
char s[1000]; // [rsp+40h] [rbp-400h] BYREF
char **v11; // [rsp+428h] [rbp-18h]
int v12; // [rsp+434h] [rbp-Ch]
unsigned int v13; // [rsp+438h] [rbp-8h]
int v14; // [rsp+43Ch] [rbp-4h]
v13 = 0;
v12 = a1;
v11 = a2;
memset(s, 0, sizeof(s));
v14 = v12;
v9 = -1044249014;
while ( 1 )
{
while ( 1 )
{
while ( v9 == -1901981264 )
{
v6 = sub_4005A0((__int64)v11[1]); // 效验函数
v7 = -1221395144;
if ( v6 )
v7 = 1016816590;
v9 = v7;
}
if ( v9 != -1881820214 )
break;
v13 = 0;
v9 = -1503504170;
}
if ( v9 == -1503504170 )
break;
switch ( v9 )
{
case -1221395144:
printf("RUSH AGAIN!\n");
v13 = 1;
v9 = -1503504170;
break;
case -1044249014:
v3 = 197961190;
if ( v14 != 2 ) // main函数的参数必须为2
v3 = -1221395144;
v9 = v3;
break;
case 197961190:
v4 = strlen(v11[1]);
v5 = -1901981264;
if ( v4 != 68 ) // 第二个参数的长度为68
v5 = -1221395144;
v9 = v5;
break;
default:
v9 = -1881820214;
printf("Congratulations! You have rushed B! Flag is flag{md5(input)}...\n");
break;
}
}
return v13;
}
v14 = v12 = a1 既main的第一个参
v11 = a2 既main的第二个参
这里知道了input的长度,然后看效验函数 sub_4005A0
,打开是一个多层的循环,先找点有用的东西。发现两个if语句和某数组相关。
将数组的值取出来查看
根据if语句中的取值方式(18 * x + y),按照18个数据一行进行排列,得到一个18*18的迷宫(hex省略了0x)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 2 b a a 9 2 b 8 3 b a a a a 9 0
0 6 9 5 3 8 6 9 5 3 c 6 9 3 a 9 4 0
0 1 6 c 5 3 a c 6 e a 8 7 c 1 6 9 0
0 7 a b d 5 2 b a a a 9 6 a e 9 5 0
0 6 8 5 4 6 a c 3 b 8 6 a 9 3 c 5 0
0 3 9 6 a a b 9 5 4 3 9 3 c 4 3 c 0
0 5 6 a 9 3 c 5 6 9 5 5 5 3 9 6 9 0
0 5 3 9 5 5 3 c 3 d 5 6 c 5 5 3 d 0
0 5 5 4 6 c 6 a c 5 5 1 3 d 6 c 5 0
0 5 7 a 9 3 b a 8 5 5 6 c 7 9 1 5 0
0 5 6 8 6 c 6 a a c 6 a 9 4 5 7 c 0
0 5 3 a 9 3 9 3 a a b 8 5 3 c 5 1 0
0 5 5 1 5 5 6 c 3 9 6 9 6 c 1 6 d 0
0 5 6 d 5 5 1 3 d 6 8 7 a a e 8 5 0
0 7 8 5 6 c 6 c 5 3 a c 3 9 3 9 5 0
0 6 a e a a a a c 6 a a c 6 c 6 1e 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
但是和普通的迷宫不一样,继续分析代码,发现了下面的switch case部分包含了wasd,基本确定该题目为走迷宫
switch ( v12 )
{
case 464858295:
v8 = 1096245293;
if ( v20 == 'a' )
v8 = -1727954447;
v12 = v8;
break;
case 479300766:
v19 = 0;
v12 = -200831311;
break;
case 602782944:
v16 += *((_BYTE *)&v17 + 2 * (3 - v14));
v15 += *((_BYTE *)&v17 + 2 * (3 - v14) + 1);
v12 = -1747144131;
break;
case 740051567:
v3 = -1281903442;
if ( v20 < 'w' )
v3 = 1150671512;
v12 = v3;
break;
case 959594628:
v14 = 0;
v12 = -1155152745;
break;
case 1055140964:
v7 = 1096245293;
if ( v20 == 'd' )
v7 = -1849705372;
v12 = v7;
break;
case 1096245293:
v12 = -2143573666;
break;
case 1150671512:
v5 = 1096245293;
if ( v20 == 's' )
v5 = 959594628;
v12 = v5;
break;
case 1294597446:
v14 = 2;
v12 = -1155152745;
break;
default:
v12 = -1918458732;
break;
}
再看v20,v20取值来源于v18加上偏移量v13
v18则是等于传进来的参数a1,所以这里是根据input进行移动。v15,v16则是我们移动的坐标,在两个if语句可以明显的得出。初始的位置为(1,1)
现在再来分析那两个if语句
if(byte_602040[18 * v16 + v15] & 0x10) != 0
在迷宫里取值然后和0x10做与运算,所以只有个位数的值(hex模式下)是只能得到假值0的。
在迷宫的右下角存在一个1e,只有该值与0x10与运算的结果是真值1
由此猜测:1e所在的位置为终点
继续分析前后部分,发现满足条件时会触发
v10 = 0x9F36EED5;
v12 = v10;
接着就会触发
if ( v12 != 0x9F36EED5 )
break;
v19 = 1;
v12 = -200831311;
v19是返回值,在main函数中,可以分析得到,该函数必须返回真值程序才不会退出并显示"RUSH AGAIN!\n"
第二个if语句,这个语句尤为重要
if ( ((1 << v14) & byte_602040[18 * v16 + v15]) != 0 )// 也是一次效验
v9 = 602782944;
这里为真的情况下才会触发赋值,然后该值与下面的case语句对应
case 602782944:
v16 += *((_BYTE *)&v17 + 2 * (3 - v14));
v15 += *((_BYTE *)&v17 + 2 * (3 - v14) + 1);
该case语句对应为对v15和v16的赋值操作,也就是说只有前面的if语句为真,我们才能在迷宫里移动
所以该if语句是对移动操作的效验,具体逻辑如下
首先对 1 进行 v14 位的左移操作,得到值 a
然后取出迷宫当前的位置的值 b
将 a 与 b 进行运算,得到结果 c
只有当 c 为真,才能正常移动
现在分析该 a 和 b 的情况:
1. b 只能取真值,所以四周用 0 围起来的"墙"是不能踏足的
b 的值分布为 1 ~ 0xf
2. a 的值和 v14 息息相关,所以需要分析和 对 v14 复制的相关操作
在switch case发现和v14较为明显的关系
所以得出 v14 的值和移动的方向有关,详细的情况需要接着分析代码。在复杂的循环中寻找逻辑关系实在是费时费力,既然已经知道和移动的方向有关,那么可以直接使用动态调试获取对应关系即可。
现在留下的问题还有一个
v16 += *((_BYTE *)&v17 + 2 * (3 - v14));
v15 += *((_BYTE *)&v17 + 2 * (3 - v14) + 1);
在整个函数中对应坐标的操作函数只有这一条,并且v17的值也是一个较大的数 v17 = 0x1010000FFFF00
,所以这里也不是那么好分析,就都留给调试分析吧。
0x02 动态分析
动态调试只有两个目的,所以直接在对应的地方下断点即可。在Ubuntu运行IDA的远程调试程序,然后在IDA配置相关参数。
这里随意构造68位的参数即可(例如填充68个d,然后更换第一个操作数“w/a/s/d”,只需要4次调试就能完成),然后在对应的地方下断点
可以直接查看到该值
同时在下面可以看到当前的操作数,0x73(s)
F9运行到下一次断点,然后去查看v15,v16的值
可以看到这里v15没有变化,v16加1。经过多次测试发现移动方式为正常迷宫的wasd,v16为行数(y坐标),v15位列数(x坐标)。
同时得到方向与 v14 的关系如下:
hex | char | 对应的 v14 |
---|---|---|
0x73 | s | 0 |
0x64 | d | 1 |
0x77 | w | 2 |
0x61 | a | 3 |
根据与运算结果可以得到 方向 与 当前位置的值 的关系(前文中的a和b的关系),这里主要以 方向 来分(当前也可以坐标取值来分类)。
v14 | b | bin(b) | 对应的 a |
---|---|---|---|
0 | 1 | 0001 | 1,3,5,7,9,b,d,f |
1 | 2 | 0010 | 2,3,6,7,a,b,f |
2 | 4 | 0100 | 4,5,6,7,c,d,e,f |
3 | 8 | 1000 | 8,9,a,b,c,d,e,f |
将两个表连在一起即可到方向和“坐标值”的关系。例如,如果你想向下(s)移动,那么你当前所处的地方的值就必须为(1,3,5,7,9,b,d,f)其中一个;反过来,如果你所处的地方为 1 ,那么你只能向下(s)移动。
0x03 EXP
那么现在,迷宫得到了,走迷宫的规则也清楚了,那么就可以走迷宫找出路了。(这个迷宫是有很多单向的路径,叫二极管迷宫比较合适)
- 可以根据规则制作坐标值与方向的对应表,然后手动走迷宫。
- 根据规则写exp,找路径即可,算法随意。这里我使用递归寻找出路。
import hashlib
maze = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x02, 0x0B, 0x0A, 0x0A, 0x09, 0x02, 0x0B, 0x08, 0x03, 0x0B, 0x0A, 0x0A, 0x0A, 0x0A, 0x09, 0x00,
0x00, 0x06, 0x09, 0x05, 0x03, 0x08, 0x06, 0x09, 0x05, 0x03, 0x0C, 0x06, 0x09, 0x03, 0x0A, 0x09, 0x04, 0x00,
0x00, 0x01, 0x06, 0x0C, 0x05, 0x03, 0x0A, 0x0C, 0x06, 0x0E, 0x0A, 0x08, 0x07, 0x0C, 0x01, 0x06, 0x09, 0x00,
0x00, 0x07, 0x0A, 0x0B, 0x0D, 0x05, 0x02, 0x0B, 0x0A, 0x0A, 0x0A, 0x09, 0x06, 0x0A, 0x0E, 0x09, 0x05, 0x00,
0x00, 0x06, 0x08, 0x05, 0x04, 0x06, 0x0A, 0x0C, 0x03, 0x0B, 0x08, 0x06, 0x0A, 0x09, 0x03, 0x0C, 0x05, 0x00,
0x00, 0x03, 0x09, 0x06, 0x0A, 0x0A, 0x0B, 0x09, 0x05, 0x04, 0x03, 0x09, 0x03, 0x0C, 0x04, 0x03, 0x0C, 0x00,
0x00, 0x05, 0x06, 0x0A, 0x09, 0x03, 0x0C, 0x05, 0x06, 0x09, 0x05, 0x05, 0x05, 0x03, 0x09, 0x06, 0x09, 0x00,
0x00, 0x05, 0x03, 0x09, 0x05, 0x05, 0x03, 0x0C, 0x03, 0x0D, 0x05, 0x06, 0x0C, 0x05, 0x05, 0x03, 0x0D, 0x00,
0x00, 0x05, 0x05, 0x04, 0x06, 0x0C, 0x06, 0x0A, 0x0C, 0x05, 0x05, 0x01, 0x03, 0x0D, 0x06, 0x0C, 0x05, 0x00,
0x00, 0x05, 0x07, 0x0A, 0x09, 0x03, 0x0B, 0x0A, 0x08, 0x05, 0x05, 0x06, 0x0C, 0x07, 0x09, 0x01, 0x05, 0x00,
0x00, 0x05, 0x06, 0x08, 0x06, 0x0C, 0x06, 0x0A, 0x0A, 0x0C, 0x06, 0x0A, 0x09, 0x04, 0x05, 0x07, 0x0C, 0x00,
0x00, 0x05, 0x03, 0x0A, 0x09, 0x03, 0x09, 0x03, 0x0A, 0x0A, 0x0B, 0x08, 0x05, 0x03, 0x0C, 0x05, 0x01, 0x00,
0x00, 0x05, 0x05, 0x01, 0x05, 0x05, 0x06, 0x0C, 0x03, 0x09, 0x06, 0x09, 0x06, 0x0C, 0x01, 0x06, 0x0D, 0x00,
0x00, 0x05, 0x06, 0x0D, 0x05, 0x05, 0x01, 0x03, 0x0D, 0x06, 0x08, 0x07, 0x0A, 0x0A, 0x0E, 0x08, 0x05, 0x00,
0x00, 0x07, 0x08, 0x05, 0x06, 0x0C, 0x06, 0x0C, 0x05, 0x03, 0x0A, 0x0C, 0x03, 0x09, 0x03, 0x09, 0x05, 0x00,
0x00, 0x06, 0x0A, 0x0E, 0x0A, 0x0A, 0x0A, 0x0A, 0x0C, 0x06, 0x0A, 0x0A, 0x0C, 0x06, 0x0C, 0x06, 0x1E, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
# 记录路线
road = ['▩'] * len(maze)
# 行走规则
s = [1, 3, 5, 7, 9, 0xb, 0xd, 0xf]
d = [2, 3, 6, 7, 0xa, 0xb, 0xf]
w = [4, 5, 6, 7, 0xc, 0xd, 0xe, 0xf]
a = [8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf]
# 方向选择
DI = ['w', 'd', 'a', 's']
flag = ['0'] * 70 # 多出两个防止下标越界
step = 0 # 记录步数
x = 1 # 起始位置
y = 1
# 获取当前的值
def maze_num():
return maze[18 * x + y]
# 获取路径的值,为'0'则说明未走过,则可以走
def road_num():
return road[18 * x + y]
# 正向移动
def move(direction):
global x, y, road
if direction == 'w':
road[18 * x + y] = '↑'
x = x - 1
elif direction == 's':
road[18 * x + y] = '↓'
x = x + 1
elif direction == 'a':
road[18 * x + y] = '←'
y = y - 1
elif direction == 'd':
road[18 * x + y] = '→'
y = y + 1
return
# 反向移动
def remove(direction):
global x, y, road, flag
if direction == 'w':
x = x + 1
elif direction == 's':
x = x - 1
elif direction == 'a':
y = y + 1
elif direction == 'd':
y = y - 1
road[18 * x + y] = '▩'
flag[step - 1] = '0'
return
# 递归找路径
def find_road(di):
global step, flag
# 判断当前位置可以走的方向
if maze_num() in eval(di):
move(di)
# 若当前路径为重复路径,则回退
if road_num() != '▩':
remove(di)
return
step = step + 1
# 根据题意,步数不会大于68
if step > 68:
remove(di)
step = step - 1
return
flag[step - 1] = di # 记录方向
# 抵达终点
if maze_num() == 30:
print(''.join(flag[:68]))
print('flag{' + hashlib.md5(''.join(flag[:68]).encode()).hexdigest() + '}')
show_road()
exit()
# 显示当前进度和坐标
print(x, '\t', y, '\t', di, '\t', maze_num(), '\t', step)
# 在当前基础上进行下一步探索
for _di in DI:
if DI.index(di) + DI.index(_di) != 3: # 下一步的方向和当前的方向不能相反
find_road(_di)
# 前路不通,当前路径需回退
remove(di)
step = step - 1
return
# 将完整路径打印出来,↑↓←→表示方向
def show_road():
for i in range(18):
for j in range(18):
if i == x and j == y: # 打印当前位置
print('㊣', end='')
else:
print(road[18 * i + j], end='')
print()
if __name__ == "__main__":
for di in DI:
find_road(di)
print("road not find")
输出结果如下:
将路径运行程序进行效验
0x10 smail cry
吃亏啊,吃亏啊,还是对java不够了解,以及对加密算法不够熟练。
0x11 代码分析
安卓逆向,使用 jdk-gui 打开,然后找到main函数,如下
可以看到是AES加密,后面传入的参数,应该就是key,在下面的 if 语句中可以看到最后加密完成的密文。
进一步分析AES文件
可以看到有两个参数,str1为input,str2为key,接着调用了AES256,继续分析
到这一步就先分析ASE加密过程
最主要是这里有一个bytes2,它实际上是作为了 iv 参与加密。接着分析 _64esabKt
反过来就是 tKbase64_
,所以还不明白的话,那我也没办法了:base64换表编码。
整体流程分析完毕。
0x12 exp
from base64 import *
from Crypto.Cipher import AES
enc = b'mrW5Musix7LhgLhfN3tI0knyuXJu6ASsQ96HNeJcbwLYKXc9whu9PwRY1CSH+EHR'
key = b'!EuZtuVD3uUtkctMHDGRYQo.vhePTu-k'
table = b'owKhCYRZTD9EFv6M/ISj7fXzyG4AOQLr5dbkmP0xeNtVlpBJUng83ias2q+WHcu1'
TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
if __name__ == "__main__":
# base换表解密
newtable = bytes.maketrans(table, TABLE)
dec0 = b64decode(enc.translate(newtable))
print("dec0: ", dec0.hex().upper())
# 位运算,xor 4
dec1 = b''
for i in dec0:
x = i ^ 4
dec1 += x.to_bytes(1, 'little')
print("dec1: ", dec1.hex().upper())
# AES 解密
iv = key[:16] # 偏移
aes = AES.new(key, AES.MODE_CBC, iv) # 构造类
dec2 = aes.decrypt(dec1) # 解密
dec2 = dec2.rstrip(dec2[-1].to_bytes(1, 'little')) # 清除padding
print("dec2: ", dec2.hex().upper())
# 位运算,取反得flag
flag = ''.join([chr(256 + ~i) for i in dec2])
print('flag: ', flag)
#dec0: 91FEE03FEDF59D4783C9E0D5A74A919A3C58F96BFE39B4B774A3BCA68BFD881785096F4A043F8A941185FC44BCE8BF06
#dec1: 95FAE43BE9F1994387CDE4D1A34E959E385CFD6FFA3DB0B370A7B8A28FF98C13810D6B4E003B8E901581F840B8ECBB02
#dec2: 99939E98849B9AC9C69DC699CAC69DC8C9CEC7CFCB9ECACFCDCCC79E999AC89D9C9ACDCC9C82
#flag: flag{de69b9f59b761804a50238afe7bce23c}
总结,这题逻辑很简单,问题出在exp上,当时没有现成的AES解密脚本,有一个工具包但是只有ECB的加解密模式……就导致了现在只能复现,说多了都是泪啊。
未完待续……