带 rep 的一些函数的研究
rep stosb
该函数等效代码
for(size_t i = 0;i < rcx;i++)
{
*(i + (char*)rdi) = al;
}
该函数的等效代码也写作
memset(rdi, al, rcx);
该函数等效代码
for(size_t i = 0;i < rcx;i++)
{
*(i + (char*)rdi) = al;
}
该函数的等效代码也写作
memset(rdi, al, rcx);
IDA 载入分析,发现是VM
抄成python出来,与z3打组合技解
from z3 import *
s = Solver()
x = [BitVec(f"x{i}", 8) for i in range(15)]
vmIns = open('signal.exe', 'rb').read()[0x1e40:0x1e40+456]
Str = [0] * 1000
end = 114
v9 = 0
v4 = 0
v8 = 0
v7 = 0
v6 = 0
v5 = 0
while v9 < end:
ins = vmIns[v9 * 4]
if ins == 1:
Str[v6 + 100] = v4
v9 += 1
v6 += 1
v8 += 1
elif ins == 2:
v4 = vmIns[(v9 + 1) * 4] + Str[v8]
v9 += 2
elif ins == 3:
v4 = Str[v8] - (vmIns[(v9 + 1) * 4] & 0xff)
v9 += 2
elif ins == 4:
v4 = vmIns[(v9 + 1) * 4] ^ Str[v8]
v9 += 2
elif ins == 5:
v4 = vmIns[(v9 + 1) * 4] * Str[v8]
v9 += 2
elif ins == 6:
v9+=1
elif ins == 7:
print(Str[v7 + 100])
s.add(Str[v7 + 100] == vmIns[(v9 + 1) * 4])
v7 += 1
v9 += 2
elif ins == 8:
Str[v5] = v4
v9 += 1
v5 += 1
elif ins == 10:
print("Read")
for i, v in enumerate(x):
# s.add(v > 0x20, v < 0x7f)
Str[i] = v
v9 += 1
elif ins == 11:
v4 = Str[v8] - 1
v9 += 1
elif ins == 12:
v4 = Str[v8] + 1
v9 += 1
else:
print(f"unknown insn {ins}")
print(s.check())
m = s.model()
for i in x:
print(chr(m[i].as_long()), end ='')
'''
Read
(16 ^ x0) - 5
3*(32 ^ x1)
x2 - 2 - 1
4 ^ x3 + 1
3*x4 - 33
x5 - 1 - 1
(9 ^ x6) - 32
36 ^ 81 + x7
x8 + 1 - 1
37 + 2*x9
65 ^ 54 + x10
1*(32 + x11)
37 + 3*x12
(9 ^ x13) - 32
65 + x14 + 1
sat
757515121f3d478
'''
flag为flag{757515121f3d478}
把exe载入IDA,发现这些函数与反调试有关
完整的检测函数
NTSTATUS sub_401990()
{
unsigned __int8 v0; // al
int v1; // edx
int v2; // ecx
NTSTATUS result; // eax
HANDLE ProcessId; // eax
HANDLE CurrentProcess; // eax
HANDLE v6; // eax
int v7; // [esp-1Ch] [ebp-44h]
int v8; // [esp-18h] [ebp-40h]
ULONG ReturnLength; // [esp+Ch] [ebp-1Ch] BYREF
int ProcessInformation; // [esp+14h] [ebp-14h] BYREF
int v11; // [esp+18h] [ebp-10h]
WINBOOL pbDebuggerPresent; // [esp+1Ch] [ebp-Ch] BYREF
int v13; // [esp+20h] [ebp-8h]
int v14; // [esp+24h] [ebp-4h]
if ( (unsigned __int8)sub_401770()
|| (unsigned __int8)sub_401820()
|| (unsigned __int8)sub_401840()
|| (unsigned __int8)sub_401860()
|| (v0 = sub_401890(), (v2 = v0) != 0) )
{
v14 = sub_402120();
}
else
{
v14 = 0;
}
v8 = v2;
v7 = v1;
if ( NtCurrentPeb()->BeingDebugged )
return dword_446030(v8, v7);
if ( NtCurrentPeb()->NtGlobalFlag )
return dword_446030(v8, v7);
ProcessId = CsrGetProcessId();
if ( OpenProcess(0x400u, 0, (DWORD)ProcessId) )
return dword_446030(v8, v7);
CurrentProcess = GetCurrentProcess();
if ( CheckRemoteDebuggerPresent(CurrentProcess, &pbDebuggerPresent) )
v13 = 0;
else
v13 = sub_402120();
if ( pbDebuggerPresent == 1 )
v11 = sub_402120();
else
v11 = 0;
v6 = GetCurrentProcess();
result = NtQueryInformationProcess(v6, ProcessDebugPort, &ProcessInformation, 4u, &ReturnLength);
if ( ProcessInformation )
return sub_402120();
return result;
}
分别检测下面的NtCurrentPeb()->BeingDebugged
NtCurrentPeb()->NtGlobalFlag
CheckRemoteDebuggerPresent
NtQueryInformationProcess
除了这个,还检测VMWaresub_401890()
代码如下
char sub_401890()
{
DWORD cbData; // [esp+0h] [ebp-4Ch] BYREF
HKEY phkResult; // [esp+4h] [ebp-48h] BYREF
BYTE Data[64]; // [esp+8h] [ebp-44h] BYREF
cbData = 63;
if ( !RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\BIOS\\", 0, 0x20019u, &phkResult) )
{
RegQueryValueExA(phkResult, "SystemManufacturer", 0, 0, Data, &cbData);
if ( strstr((const char *)Data, "VMWARE") )
{
RegCloseKey(phkResult);
return 1;
}
RegCloseKey(phkResult);
}
return 0;
}
除了上面的,还有最开始的四个函数
第一个会使用特权指令in
来制造异常,不过try{}catch{}并没有检测到什么。我们先不看
接下来我们分析第二个:sub_401930()
分别会记录rdtsc的高32位和低32位
因为__rdtsc()返回一个64位整数,分别高位:edx,低位:eax
了解一下rdtsc
rdtsc指令, 该指令返回CPU自启动以来的时钟周期数;该时钟周期数,即处理器的时间戳。
在CPU通电启动后,首先会重置EDX和EAX,在每个时钟周期上升或下降沿到来时,会自动累计周期数,并被记录到EDX和EAX寄存器中,EDX是高位,EAX是低位。
rdtsc指令就是从该寄存器中进行获取的。
周期和频率的关系公式:T(周期)=1/f(频率)
如CPU频率f为1GHz,则其时钟周期T=1/1GHz秒,意味着每隔T秒(1ns),CPU完成一个最基本的动作,并在寄存器中,对周期数加1。
所以我们可以知道rdtsc的递增是非常快的,按照我的电脑的运行时CPU频率2.35GHz,可以大概知道每秒这个数字会增加2.35*10^9。
不过不是很懂cpuid在rdtsc之前运行的意思❓
下面我们看sub_401950()
函数
显然是接着上一个函数进行的基于时间的检测,如果单步调试就爆了。
因为检测时间差是40000000,也就是4*10^7,也就是 1/100
秒内必须执行完,单步显然会不行。
对于这些函数,我们直接在函数头ret就好了,不过要注意,这些函数并不是所有都是检测函数。
继续分析,下面就是一堆简单的逻辑了
注意到是把自己读入到刚刚allocate好的内存里面,然后解密关键内容,这里直接上动态调试就OK了,静态分析没必要,因为你不需要了解算法内容(可能是AES❓)。
我们可以把解密后的东西给dump出来,是一个dll,下面的v13 = v14();
调用getflag
函数。
如果函数返回是1,则success,否则执行__asm { syscall; Low latency system call }
这个显然会造成异常。
下面开始分析DLL
找到DLL关键函数,这个前面还有几个check❓,出题人这么和反调试、虚拟机调试过不去吗😰。
其实我们只要动态调试,屏蔽掉所有异常,然后运行到程序中的v13 = v14();
这个位置就好了(记得使用反反调试插件)
我们终于找到了scanf的真正位置,下面是初始化了一堆数据,然后进虚拟机。
动态调试,加硬件断点于第一个输入的值,这个scanf有一点奇怪,你如果输入的太长了,好像第一个位置就变成0了😰,简单打断点分析了一下,好像被填0了,不过我们不管这个,我们把第一个0改成a继续下断点就行。
跑起来,第一次断在strlen,第二次就断在vm了
我们把stack下硬件断点,发现对输入先进行xor 0x33
继续跑,发现再进行 add 0x2
继续跑,发现存在了这里
然后把这个store的地方进行软件断点,把之前的硬件断点去掉,这样可以看到加密的全过程,也可以清楚知道都是xor 0x33并且之后add 0x2,对了,一定要注意,把第一个store的地方下硬件断点,因为加密后必定就是cmp检测,或者是继续加密...。
果不其然,它在处理完后面的字符串后,很快地,又把刚刚第一个字符串push到了vStack上。
这时我们继续传统艺能:对栈下断点,发现对第一个字节进行xor 0x36,第二个字符xor 0x48...,我们直接可以断xorTable的生成点,注意到getXorTable的地方其实也是拿字符串的地方
最后我们继续下断点,找到了cmp table,写出z3脚本
from z3 import *
s = Solver()
a = bytes.fromhex("36 48 0D 23 0C 25 09 17 53 06 10 1A 24 2E 38 42".replace(' ',''))
b = bytes.fromhex("51 5f 72 69 6a 32 89 79 2b 52 52 56 4a 4d 60 12".replace(' ',''))
x = [BitVec(f"x{i}", 8) for i in range(len(a))]
for i in range(len(a)):
s.add(((x[i] ^ 0x33) + 0x2 ) ^ a[i] == b[i])
print(s.check())
m = s.model()
for i in range(len(a)):
print(chr(m[x[i]].as_long()), end='')
# V&N{W&M_Easy_Re}