最近做到了这道题,感觉这题挺复杂的,做了好长时间,网上的wp也不多,看的也一知半解,今天记录下自己的解题过程和感悟
!!!希望读者在复现时跟着我的步骤动调跟进,以免漏过一些重要的步骤
本题考点
1.使用时间戳作为随机种子生成随机数
2.暴力破解时间种子(调用程序原本的函数)
3.onexit函数对地址回调
4.rc4加解密
5.vm (头疼)
首先拖入ida
观察main函数
发现只有在一定时间内打开才可以
下断点调试,发现在main函数执行之前就输出了语句,所以main函数并不是真正的main,我们需要找到真正的main函数,首先反回上级函数,观察发现可疑函数
动态后发现确实是,跟进,发现这里调用了两个关键函数(下面有用)
首先观察调用的第一个函数
在这个函数中发现,如果dword_4099D0为0那么就会错误
dword_4099D0的修改在sub_402268函数中
时间戳生成随机数
这里使用时间戳生成随机数,然后对一个数组做异或,将异或后的数据经过一系列操作后生成一个数据,这个数据要等于1792,否则会将dword_4099D0置零,也就是失败
暴力破解时间种子(调用程序原本的函数)
先讲一下本人的方法(可以跳过),观察sub_4027ed函数,发现并不算太长,将逻辑复现为c语言代码,然后进行爆破,此方法工作量大且不适合大型函数
下面讲解大佬的方法
首先本题是windows程序,所以我们可以使用loadlibrary函数调用源程序里的函数来进行爆破,这种方法工作量小,且不容易出错
下面给出调用的代码,
!!!!注意,因为程序是64位,所以在编译时请选用x64模式
!!!!注意,因为程序是64位,所以在编译时请选用x64模式
!!!!注意,因为程序是64位,所以在编译时请选用x64模式
#include <iostream>
#include<stdio.h>
#include<windows.h>
using namespace std;
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;
}
爆破后得到时间为0x5b00e398
onexit函数对地址回调
还记得上面我们调用了两个函数吗?第一个函数是时间验证,第二个函数则是下面的关键
这里是这题的精髓,观察发现这里使用了onexit函数,在msdn上查看onexit函数的作用
这里注册了程序在退出时的回调地址,动态调试可以发现函数在执行时间验证后会继续执行完main函数,而在main函数执行完毕后会有一个exit
我们跟进后发现执行完exit后程序并没有结束,而是跳转到了sub_403260函数,也就是上面通过onexit函数注册的回调函数
这就是onexit函数的作用,在linux下也有同样作用的函数atexit
rc4加解密
紧跟上面,继续执行程序,发现程序进入到了sub_4023B1函数
这里要求我们输入一个字符串,然后对输入进行rc4加密,再把输入传递给vm函数执行
vm
接下来到了最令人头痛的vm时间了,老实说每次vm都会占用大量的解题时间,所以解决vm一定要养成将opcode转换为对应的指令和寄存器,这样可以省去大量的时间,通过动调加静态分析逆向出vm逻辑,使用z3解一下,然后在对其进行rc4的解密就可以了,注意vm里面注册了一个异常,除以0会抛出异常,异常接收函数会处理异常
from z3 import *
s = Solver()
user_in = [BitVec('a%d' % i,8) for i in range(0x1A)]
re = [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]
tmp = 0x66
for i in range(0x1A):
s.add(((user_in[i] + 0xCC) & 0xff) ^ tmp == re[i])
tmp = (~tmp) & 0xff
flag=[]
if s.check() == sat:
m = s.model()
for i in range(26):
flag.append(m[user_in[i]])
print(flag)
f=[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]
key=[0xA4,0xE7,0x2C,0x32]
s=[0]*256
k=[0]*256
for i in range(256):
s[i]=i
k[i]=key[i%len(key)]
v6=0
v5=0
for i in range(256):
v6=(s[i]+v6+k[i])%256
v5=s[i]
s[i]=s[v6]
s[v6]=v5
v7=0
v6=0
for i in range(len(flag)):
v7=(v7+1)%256
v6=(v6+s[v7])%256
v4=s[v7]
s[v7]=s[v6]
s[v6]=v4
f[i]^=s[(s[v6]+s[v7])%256]
print(''.join(chr(x) for x in f))
输出为@ck_For_fun_02508iO2_2iOR}
再加上”rctf{“就是Flag了
总结
这题主要是采用了虚假的main函数,要找到真正的main函数,然后再通过调用程序本身的函数爆破时间,再通过onexit函数注册退出返回地址,在把输入做rc4加密传给vm函数,vm函数里注册了一个异常。这就是这题的主要流程,感觉学到了挺多的,希望读者在阅读文章时尽量跟着动调摸清整个流程。