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] ==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[256] = {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 = [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[v7] >> 4;
rb = byte_405340[v7] & 0xF;
if ( !setjmp(&unk_4099E0) )
byte_405340[v7] = ra / byte_405340[v7 + 1]; //这里有0除异常,byte_405340[v7 + 1]总是等于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[ra] = context[ra] == context[rb];
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,[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[r3] + 0xCC) & 0xff) ^ 0x66
AA 05 01 mov r5,r1
A9 53 add r5,r3
A0 05 mov r5,[r5] #r5=xxx[r3]
AF 56 00 cmp r5,r6
A7 01 je s1 if(((input[r3] + 0xCC) & 0xff) ^ 0x66 != xxx[r3]) 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 = [BitVec('a%d' % i,8) for i in range(0x1A)]
xxx = [0x89, 0xC1, 0xEC, 0x50, 0x97, 0x3A, 0x57, 0x59, 0xE4, 0xE6, 0xE4, 0x42, 0xCB, 0xD9, 0x08, 0x22, 0xAE, 0x9D, 0x7C, 0x07, 0x80, 0x8F, 0x1B, 0x45, 0x04, 0xE8, 0x00, 0x00, 0x00, 0x00] #常量表xxx
k = 0x66
for r3 in range(0x1A):
s.add(((input[r3] + 0xCC) & 0xff) ^ k == xxx[r3])
k = (~k) & 0xff
s.check()
res = s.model()
flag_rcxx = []
for i in range(0x1A):
flag_rcxx.append(res[input[i]])
print flag_rcxx
# 解的flag_rcxx为如下数据
#[35, 140, 190, 253, 37, 215, 101, 244, 182, 179, 182, 15, 225, 116, 162, 239, 252, 56, 78, 210, 26, 74, 177, 16, 150, 165]
这时解出来的数据仅仅是vm_start输入的参数,还需再次调用rc44函数解密。
这里使用一个IDA的插件来替换内存数据
from idaapi import *
def PatchArr(dest, str):
for i in range(len(str)):
c = str[i]
idc.PatchByte(dest+i, ord(c));
print 'ok'
最后得到的数据实际是一部分flag:@ck_For_fun_02508iO2_2iOR}
为了得到完整的flag,将flag输入并回车就会发现有一个字符画输出,摘掉眼镜就能看出是rctf{
整理组合后就是....