RWhackA远程线程注入式病毒分析(H&NCTF2024)
2024-6-27 17:55:17 Author: mp.weixin.qq.com(查看原文) 阅读量:6 收藏

wp拜读:[原创]H&NCTF RE 部分题解(https://bbs.kanxue.com/thread-281801.htm#msg_header_h2_0)

涉及到的知识点:
IDA动态调试
IDAPython脚本
用快照对进程、模块、线程进行遍历(代码段)
病毒感染实现(PE infector)
DLL文件分析


发现保护器:保护器: Enigma Virtual Box
脱壳失败:

开始手动静态分析:



静态分析发现有很多內存访问异常点,均出自函数调用上,可能是程序重定位时产生的错误,没办法只能动态调试了。

动调验证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文件里面的函数了!
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函数:



这里会检测程序是否运行再虚拟机上!
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参数,实际的批处理文件逻辑开始执行。

继续分析又发现虚拟机检测(进程快照检查)

//使用 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必备进程!来检查是否是虚拟机!
    
v40 = "Vmtoolsd.exe";
v41 = "Vmwaretrat.exe";
v42 = "Vmwareuser.exe";
v43 = "Vmacthlp.exe";
v44 = "vboxservice.exe";
v45 = "vboxtray.exe";

遍历PCB进程块查找特定模块

直接手动绕过前面的虚拟机检查就来到了下面代码的位置!
// 获取当前进程的环境块(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)。
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;

检查系统中是否存在某些安全软件或者虚拟机相关的进程

继续向下分析。
// 获取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 函数所返回的结构体:
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()

解析一下代码:
// 加载 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);

这段代码的主要功能是:
1.加载一些 Windows API 函数的地址。
2.从内存中读取并处理一些数据。
3.枚举系统中正在运行的进程,并找到名为 "exp10rer.exe" 的进程。
4.将处理后的数据注入到目标进程的内存中。
5.在目标进程中创建一个新的远程线程,并执行注入的代码。
但是电脑中并没有这个程序或进程exp10rer.exe,这个进程大概率在源码中是存在的但是被本题魔改掉了,因为这是ctf比赛,写成这样已经将整个病毒架构写出来了!

分析最终注入其他程序的shellcode

// 输出处理后的数据
v26 = &v41;
for (i = 0; i < 0x393; ++i)
printf("%c ", *v26++);
这段代码会输出最终的shellcode,也就是我们最终要分析的目标!直接在这个位置下个断点,dump一下数据就好了。



利用idapython将数据dump出来。
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:

看雪ID:Loserme

https://bbs.kanxue.com/user-home-944427.htm

*本文为看雪论坛优秀文章,由 Loserme 原创,转载请注明来自看雪社区

# 往期推荐

1、嵌入Python解释器的程序逆向

2、记由长城杯初赛Time_Machine掌握父子进程并出题

3、从Clang到Pass加载与执行的流程

4、OLLVM混淆源码解读

5、VMProtect保护壳爆破步骤详解(入门级)

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458560341&idx=1&sn=4609b58b2137286a578d1c90a1210a1e&chksm=b18d97df86fa1ec992bbd3d38009fc406ef05fc33cd12995231d8bf4a6a6f1a1037a5451d035&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh