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 的关系如下:

hexchar对应的 v14
0x73s0
0x64d1
0x77w2
0x61a3

根据与运算结果可以得到 方向 与 当前位置的值 的关系(前文中的a和b的关系),这里主要以 方向 来分(当前也可以坐标取值来分类)。

v14bbin(b)对应的 a
0100011,3,5,7,9,b,d,f
1200102,3,6,7,a,b,f
2401004,5,6,7,c,d,e,f
3810008,9,a,b,c,d,e,f

将两个表连在一起即可到方向和“坐标值”的关系。例如,如果你想向下(s)移动,那么你当前所处的地方的值就必须为(1,3,5,7,9,b,d,f)其中一个;反过来,如果你所处的地方为 1 ,那么你只能向下(s)移动。

0x03 EXP

那么现在,迷宫得到了,走迷宫的规则也清楚了,那么就可以走迷宫找出路了。(这个迷宫是有很多单向的路径,叫二极管迷宫比较合适)

  1. 可以根据规则制作坐标值与方向的对应表,然后手动走迷宫。
  2. 根据规则写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的加解密模式……就导致了现在只能复现,说多了都是泪啊。


未完待续……