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复制给v31v31 = 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_byteswith open('dump', 'wb') as f: f.write(idaapi.get_bytes(0x5FF060, 0x393)) |
将内存dump出来之后就直接拖入ida看看这段shellcode的作用,当然也可以直接在ida里将shellcode转为指令直接分析不需要dump!
但是我还是dump出来更加清晰:
在shellcode的最后就隐藏这flag:
