【逆向学习】分析变种银狐学习笔记(采用匿名函数进行恶意行为)

前言

学习逆向中,有错误请指正,粗略分析

前置知识点:

  • ida9
  • 沙盒

本样本由客户中招机器中提取,和网上的其它银狐还不太一样,使用的是回调函数+函数指针表+虚函数表+匿名来加大分析难度

一、样本前置分析

先丢入虚拟机,看看基本信息

img

img

然后无脑丢入微步沙箱

https://s.threatbook.com/report/file/5d11482c2d6211f8bfb04c88657ec34806aa745e3df98b410a741ba8d90e9397

img

看得出来有反沙箱,拿不到ip

使用Detect It Easy检测加壳情况

img

无壳,使用C++编写
img

二、逆向分析

丢入ida9,小等一下。看上面的小黄条跑完就是分析好了

还在分析:
img

分析好了:

img

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

2.1 动态调试拿函数

在进行这一步前,请确认:

  1. 位于虚拟机内
  2. 虚拟机是干净的,不包含你的任何信息
  3. 从外部断开虚拟机网络设备
    img

然后选择:
img

  1. 选择本地调试器
  2. WinMain开头下一个断点
  3. 运行

img

运行起来后结束,回到静态,再次等待ida分析(可以看到左边的已经在识别函数了)

2.2 分析WinMain函数

回到WinMain,稍微看一下发现只有sub_7FF6100D1000进入下一步,那就跟上去

img

不难看出,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'

先别怕,人家就长这样,能够正常跳转的。

用这个有什么好处等会就知道了,能把逆向人恶心得不轻,直接在这一坨函数上双击,跳转到:
img

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

我们直接找第二个进去
img

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

img

2.4 如法炮制...吗?

理论上,虚函数表就放在.rdata我们只要挨个分析过去就好了。但是问题是:这样效率太低了,刚刚一个函数都有点眼花缭乱了,更别说一堆函数指针和匿名函数,那有什么办法吗?

有的,兄弟,有的。

首先,这个木马不可能从网络连接到截图都直接写吧?必然要使用dll,那么,我们打开Imports

img

这下看懂了,Win32API!我们可以直接对着API反向找函数,而没必要分析执行流,在刚刚动态调试就是为了找出dll

2.5 拿网络连接开刀

WS2_32.dll是提供了socket的一个动态库,其中要连接就一定会调用WSAStartup
img

按X查找交叉引用

img

一下就找到了目标函数,依次类推,要导入的函数基本上是有用的,你要分析什么就直接去找(例如:这个木马运行后会删除自身,你就去KERNEL32DeletFileW),这样就能快速完成任务。

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

img

开头改为C3(即retn
img

img

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

img

img

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

https://s.threatbook.com/report/file/1a58a3c099bb2de7bcde13fda246582219ce91e4cd5a8069bef18121be80e17f

img

然后就发现全部出来了

2.7 深入分析

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

img

对着这一堆按X
img

img

然后就循环起来就好

三、总结&坑点

因为是用户机器下来的,不能保存文件,可以去微步下载。

ida por 以及插件下载: https://www.123912.com/s/dlcdjv-1bHg?提取码:BRTt

  1. 实际上摸了好久才出来的,笔者真的分析了15个函数QAQ
  2. 别忘了重置虚拟机哦
  3. 其实这个是qt写的,在早期已经发现了,但是对分析用处不大就没写

img

学过一些qt,最著名的就是信号和槽,但是信号和槽反编译出来不是这样,所以就没写了。

为什么说匿名函数很恶心。因为找不到交叉引用,全是当成对象来用,传入的参数也是当成对象,统一由一个函数来启动函数,可把我恶心坏了,加上开了一堆线程,每个只能追踪到统一的启动函,这才让我放弃了追踪控制流的打法。

3.1 木马分析打法

  1. 关闭杀软
  2. 重置虚拟机
  3. 断开网络
  4. 丢沙箱
  5. 反编译+去混淆
  6. 动态调试导入dll
  7. 对着dll找函数功能
  8. 去掉反调试函数
  9. 丢入沙箱就可以交报告了,如果客户大大要什么具体行为就回到6对着找,基本能八九不离十
posted @ 2025-06-24 01:08  归海言诺  阅读(372)  评论(0)    收藏  举报