无名侠 发表于 2018-5-21 22:47

RCTF - magic writeup

本帖最后由 无名侠 于 2018-8-4 21:15 编辑

#RCTF - magic


## 题目考点

- 入口函数位置混淆
- 数据爆破
- 原始代码调用
- vm还原
- 简单方程求解
- Linux 异常处理

## 概述

运行题目给定的二进制文件,有如下输出:

```
flag only appears at a specific time, range [2018-05-19 09:00, 2018-05-21 09:00)
Better luck next time :)
```

该题目有许多坑,比如IDA函数表中的main函数并不是真正的main函数、运行时间限制、vm逆向还原、类rc4算法的处理等等。



## 0x1 寻找入口函数

使用IDA分析该二进制文件,在函数表中可以找到main函数,在main函数的头部打下断点并调试运行,这时能够发现在该断点命中的时候屏幕上已经有信息输出,说明在main函数之前还执行了大量的代码。如何寻找真正的入口函数呢?方法有如下:main函数栈回溯、puts栈回溯。

使用上面两种方法,可以大概确定真实入口函数的位置。

通过调试发现,这程序有两步验证:1、时间验证,2、输入信息验证。



## 0x2 时间验证



time64函数返回当前时间戳,根据这段代码,返回的时间戳应该在(0x5AFFE78F,0x5B028A8F]这个范围内程序才会继续执行。

其后以时间戳做为随机数种子,取随机数对E1表异或运算,再通过4027ED这个函数对E1表进行一些变换,最终得到a3的值,并判断a3的值是否等于0x700.

在后面的代码中可知,如果time_data ==0,那么程序将直接退出。因此,必须要寻找一个值使得a3满足a3==0x700。

我们观察满足题意的time返回值,0x5B028A8F - 0x5AFFE78F = 0x2A300,可以说是非常小的数字了,只需要遍历测试(0x5AFFE78F,0x5B028A8F]区间的所有值即可寻找到正确的time值。

由于a3的求值过程过于复杂,还原a3的算法是不可行的,可以采取调用原程序的方法,下面是暴力求解time的代码

```
typedef unsigned int(*test)();
static UINT time = 0x5AFFE78F + 1;
UINT myfun(int) { //通过这种形式遍历每一个time
      return time++;
}
char e1_copy = {0};
int main()
{
      UINT64 * ptr1 = (UINT64 *)0x40A38C;//time64
      UINT64 * ptr2 = (UINT64 *)0x40A414; //srand
      UINT64 * ptr3 = (UINT64 *)0x40A3FC;//rand
      UINT64 * ptr4 = (UINT64 *)0x40A3DC;//memset
      HMODULE h = LoadLibraryA("D:\\magic.exe");
      memcpy(e1_copy, (void *)0x405020, 256); //备份E1表,重新运算的时候需要还原E1表
      test test1 = (test)0x402268;
      *ptr1 = (UINT64)myfun;
      *ptr2 = (UINT64)srand;
      *ptr3 = (UINT64)rand;
      *ptr4 = (UINT64)memset;
      UINT val;
      while (true)
      {
                memcpy((void *)0x405020, e1_copy, 256); // 重置E1表
                val =test1();
                if (val != 0)
                {
                        printf("time:%x\nkey:%x", time -1 , val); //0x322ce7a4
                        //time:5b00e398
                        //key: 322ce7a4
                        break;
                }
      }
    return 0;
}
```


最终测得满足条件的time为:0x5b00e398

为方便调试第二阶段,在time64函数返回后的位置处下断点,每次停下来后手工修改RAX寄存器(此时保存的是time64函数的返回值)的值为0x5b00e398,之后即可顺利进入第二阶段的调试。

## 输入验证



rc44 是加解密同函数的算法,可直接跳过不看,其密钥为第一步中求解的time运算所得。

vm_start是进入vm虚拟机,Count是rc44对输入数据加密后的数据,也就是说输入数据加密后经过虚拟机运算要满足一定条件才行。

正向思维:vm_start(rc44(input)) == 1

逆向求解input :vm_start ===》 解出 参数count ===》rc44解密count

虚拟机典型特征是switch结构 + bytecode读取,vm_start完全符合。

通过单步调试分析出如下指令集:

```
high4:rd
low4:rs
0xAB :3
      AB 03 00
      Rb = p2
0xAA :3
      Rb = Rc
0xA9 :2
      Ra += Rb
0xA0 :2
      Rb =
0xAC :2
      Ra &= Rb;
0xAE :2
      Ra ^= Rb;
0xAD :2
      Rb = ~LOBYTE(Rb);
0xAF : 3
      Ra = Ra == Rb;
0xA6 : 2
      jz rb
               
0xA7 : 2 jnz +Rb
      if r5:
                ip += Rb
0xCC
```

其中0xAF指令的代码并不在vm_start中,而是在异常处理函数中。

```
if ( v1 != 0xAF )
goto LABEL_43;
ra = byte_405340 >> 4;
rb = byte_405340 & 0xF;
if ( !setjmp(&unk_4099E0) )
byte_405340 = ra / byte_405340;//这里有0除异常,byte_405340总是等于0
v7 += 2;
```

vm_start的开头有注册异常处理的代码

```
signal(8, sub_402930);
```

分析sub_402930处的代码

```
void __fastcall __noreturn sub_402930(int a1)
{
if ( a1 == 8 )
{
    signal(8, sub_402930);
    context = context == context;
    sub_404716(&unk_4099E0, 0);
}
exit(1);
}
```

整理翻译

```
init:
r1 = xxx   常量数组
r2 = &input #vm_start参数

AB 03 00                mov r3,00
AB 04 1A            mov r4,0x1A
AB 00 66                mov r0,0x66
s2:
AA 05 02                  mov r5,r2            
A9 53                         add r5,r3
A0 05                        mov r5,   
AB 06 CC               mov r6,0xCC   
A9 56                         add r5,r6      
AB 06 FF                  mov r6,0xff
AC 56                         and r5,r6
AE 50                         xor r5,r0                  
AD 00                         mov r0,~LOW8(r0)      #r0 = ~0x66
AA 06 05               mov r6,r5                        #r6 = ((input + 0xCC) & 0xff) ^ 0x66
AA 05 01               mov r5,r1                        
A9 53                         add r5,r3
A0 05                        mov r5,         #r5=xxx
AF 56 00               cmp r5,r6                        
A7 01                         je s1               if(((input + 0xCC) & 0xff) ^ 0x66 != xxx) break
CC            stop

s1:
A9 35         add r3,r5#r5一定等于1   r3=r3+1
AA 05 03                mov r5,r3#r5 = r3
AF 54 00               cmp r5,r4#size
A6 D1         jne s2            
CC
```



根据这些代码写z3求解脚本:

```
from z3 import *
s = Solver()
input =
xxx = #常量表xxx
k = 0x66
for r3 in range(0x1A):
      s.add(((input + 0xCC) & 0xff) ^ k == xxx)
      k = (~k) & 0xff
s.check()
res = s.model()
flag_rcxx = []
for i in range(0x1A):
      flag_rcxx.append(res])
print flag_rcxx

# 解的flag_rcxx为如下数据
#

```

这时解出来的数据仅仅是vm_start输入的参数,还需再次调用rc44函数解密。

这里使用一个IDA的插件来替换内存数据

```
from idaapi import *
def PatchArr(dest, str):
    for i in range(len(str)):
      c = str
      idc.PatchByte(dest+i, ord(c));
    print 'ok'
```

最后得到的数据实际是一部分flag:@ck_For_fun_02508iO2_2iOR}

为了得到完整的flag,将flag输入并回车就会发现有一个字符画输出,摘掉眼镜就能看出是rctf{

整理组合后就是....

Max_Wang 发表于 2018-7-10 20:16

想请教大大关于0x2时间验证部分,爆破得到时间戳的程序中,LoadLibrary真的可以加载exe文件嘛,为什么我运行这个爆破程序的时候会报错呢,我使用的编辑器是VS,win10_64位操作系统,排除路径不正确导致的LoadLibrary加载程序失败。

Setsuro 发表于 2018-5-21 23:15

沙发 楼主辛苦了

wisoft 发表于 2018-5-22 09:08

学习,感谢分享

yssun 发表于 2018-5-22 09:10


学习,感谢分享

小权666 发表于 2018-5-22 10:37

楼主你辛苦了

七八鼓捣A 发表于 2020-3-31 15:09

大佬请问您有资料吗,那些逆向课,比如“链家apk调试+so签名验证专题+调试后门”
请问在哪看,感谢
页: [1]
查看完整版本: RCTF - magic writeup