从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup

WriteUp 2年前 (2022) admin
855 0 0

点击 / 关注我们

Brief

这题名为 Loader,其本质也是从 .data 段中加载了程序的主要逻辑并运行,使用了无文件 PE 文件加载的相关技术。

因为这道题没加反调试等 check,所以比赛时我只是略扫了一下 load 的部分,主要精力都放在关键逻辑上了。但其实这题 loader 部分的 assembly 写得很有意思,于是赛后我又着重分析了一下相关部分的代码。

题目整体可以分为两部分:

  1. Loader 首先将 .data 段的权限设置为 RWX.data 中数据的是一个进程的 dump,Loader 随后效仿 Windows 加载器来修改 IAT 并对该部分代码进行重定位,随后跳转到其中的 main 函数
  2. 关键逻辑:由 nim 语言编译,将输入解析为 big num 再 check

加载过程分析

  1. 首先,将 .data 段存放的 shellcode 记为 code 变量方便后续表示。
  2. 开头的 VirtualProtect 部分较为简单,直接略过。此时控制流来到 code 处,此处的逻辑也很简单,就是通过修改栈顶保存的 rip 让控制流来到 code+0x34000 处,我们直接从此处开始分析。

Locate kernel32.dll

InMemoryOrderModuleList

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 读取 gs:[0x60] 处的数据。如 Win32 Thread Information Block – Wikipedia 所述,该位置着指向当前进程 PEB 的指针。
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 读取 PEB:[0x18] 处的数据。偏移及字段的关系可以参考 PEB (geoffchappell.com) ,可知这里是获取的是 LDR 的指针。
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 进一步读取 LDR:[0x20] 处的数据。该结构体的细节可以参考 PEB_LDR_DATA (geoffchappell.com) ,可知这里获取的是 InMemoryOrderModuleList 的指针
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img

LDR_DATA_TABLE_ENTRY of kernel32.dll

InMemoryOrderModuleList` 是一个双向链表,链接了若干结构体,每个结构体都记录了加载进当前进程空间的一个模块。对于该进程,其链接的顺序是可执行文件,`ntdll.dll`,`kernel32.dll
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img

随后的三条指令实际上是就是沿着 FLink 遍历链表,找到第三个链表上的 node。该 node 就是 kernel32.dllLDR_DATA_TABLE_ENTRY , LDR_DATA_TABLE_ENTRY 的结构可以参考 LDR_DATA_TABLE_ENTRY (geoffchappell.com) , 此处读取了 0x10+0x20 处的 DllBase 字段并赋给 rbp 寄存器, 随后执行了 call 指令进入下一阶段

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img

Load API

这部分代码的作用是调用 kernel32.dll 中的 LoadLibraryGetProcAddress ,来加载后续关键逻辑中需要使用的 API。此时 rbp 指向了 kernel32.dll 的加载基址,也就是该 PE 文件在内存中的起始位置。

从系统里找到 kernel32.dll 文件然后放到 Detect It Easy 中可以更好的理解这部分。

Locate Export table

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 将调用处后面的地址保存到 rsi 中,看来此处的数据并非花指令
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 访问 IMAGE_DOS_HEADERe_lfanew 字段,该字段代表 IMAGE_NT_HEADERS 与文件头的偏移
  2. 访问 IMAGE_NT_HEADERS 的子结构体 IMAGE_OPTIONAL_HEADERIMAGE_DATA_DIRECTORY 字段,对于 kernel32.dll ,该字段就是导出表的 offset
  3. 此时 rbx 指向 kernel32.dll 的导出表。后面大量使用的 0x20 偏移处的字段也就是其中的AddressOfNames 字段
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img

GetProcAddress and LoadLibraryA

后面的指令稍微有些复杂,并且由于寄存器使用得非常随意,伪代码可读性也很差,还是来分析汇编

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 首先注意到注意到其中使用了 0xEDB88320 这个 constant。上网搜索发现是一个 CRC 算法中的数字,该算法如下。

“`c unsigned long int compute_crc( unsigned char *input, int len, unsigned long divisor ) { int i, k; unsigned long crc = 0xFFFFFFFF;

 for ( i = 0; i < len; i++ )
 {
   crc ^= ( input[ i ] );
   for ( k = 8; k; k-- )
   {
     crc = crc & 1 ? ( crc >> 1 ) ^ divisor : crc >> 1;
   }
 }

 return crc ^ 0xFFFFFFFF;

} … printf( “%lxn”, compute_crc( “ABC”, 3, 0xEDB88320 ); // a3830348 “`

  1. rbx+0x20 指向了 Export table 的 AddressOfNames。该字段是一个列表,每个成员是 Export function name 与 PE 文件的偏移,因此此时 rdi 指向了当前校验的函数名,eax 保存着 checksum
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. checksum[rsi] 相等时执行后续代码,否则接着去校验后面的函数名
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 动调可以发现当 function name 为 GetProcAddress 时校验通过,此时 rdx 寄存器即为 GetProcAddress 的 index,再去访问 Export table 的 AddressOfFunctions 并处理偏移即可得到该函数的绝对地址
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
  1. 此时 raxGetProcAddress 的地址,随后的 push 操作将其压入栈中,lodsd 在取值的同时会让 rsi+=4 ,即指向了下一个要定位的函数的 checksum
  2. 当再走完一遍上面的逻辑后,此时栈中已有 LoadLibraryAGetProcAddress 的绝对地址
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
img
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712204237676

Rewrite IAT

在该部分中 Loader 解析了 code 中的导入表,并利用前面定位到的 GetProcAddressLoadLibraryA 加载 code 需要用到的 API。

这些 API 的信息记录在 INT 和 IAT 表中,正常情况下 Windows 在加载一个 PE 文件时,会从 INT 表中解析到 API 所在的 dll 和名称,将 dll 加载到内存中后把 API 的绝对地址覆写入 IAT 表中,此处自定的加载器也是按照该逻辑写的。

  1. code 中的 IMAGE_NT_HEADERS 的地址保存到 rbp 寄存器中,并寻址到 IMAGE_NT_HEADERS+0x90 处的 Import Table。Import Table 由若干 IMAGE_IMPORT_DESCRIPTOR 构成,其 FirstThunk 字段指向了所有待导入的 API。
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712220902482
  1. 加载 dll 文件并获取其 IMAGE_IMPORT_DESCRIPTOR 的地址
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712221206998
  1. 通过循环来加载该 dll 中所有被使用到的 API
从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712221249276
  1. IMAGE_IMPORT_DESCRIPTOR 处理完后再加载其他需要的 dll

Relocate

由于 ASLR 的存在, .data 段的地址也会变化,因此若想让 code 可以正常的运行还需要像正常加载 PE 文件一样对代码中的一些绝对地址进行改写

PE Header 中的 Base Relocation Table 专门用于记录哪些地址需要被改写,相关细节较多不在此展开,仅对该 Loader 的加载过程进行概述

在处理完 IAT 后接着会执行这部分代码

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712223028447
  1. 获取 Base Relocation Table 的绝对地址,存放到 rdi
  2. 遍历,对于每个要改写的地址,计算其绝对地址,随后将该地址的值改为 &code + offset

Execute from entrypoint

在重定位完成后,code 便可以被当作加载到进程中的 PE 文件执行。此时 Loader 从 IMAGE_NT_HEADER 中获取了 AddressOfEntryPoint ,并跳转到此处。

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712223938442

然后其实就是正常 init 再调用 main 函数的过程了,结合一些 PE 文件分析的经验和动调,可以定位到 main 函数在 code + 0x12850

关键逻辑

nim 的内存管理并不复杂,string、big num 这种数据结构也不太复杂,都形如

struct Data{
    int64_t f0;
    int64_t f1;
    // data
}

通过动调,可以定位到 printscanf 函数,然后在此处校验输入 flag 的格式

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712224654750

然后会将输入的 flag 拆分为两个 18-bytes 的字符串

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712224747227

此处调用了多个库函数,由于是高级语言编译而来所以不太容易看懂,但其参数均为数字字符串,不难想到是一个 atoi 的操作

从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup
image-20220712224942866

此时上下文共有四个 int

big1 = 0x100000000000000
big2 = 0x1000000000000000
num1 = # input 1
num2 = # input 2

后面所调用的一些库函数也都是基于这四个 int 和其运算结果进行处理,通过黑盒和猜测,可梳理后面的逻辑如下

assert(big1 < num1)
assert(num1 < big2)
assert(num1*num1-11*num2*num2 == 9)

这里需要解一个 general pell equation,不太算是 re 的范畴了,遂交给 crypto 师傅来做

最终解题脚本

sage ./pell.sage
# pell.sage
cf = continued_fraction(sqrt(11))
cs = cf.convergents()

for each in cs:
    x1, y1 = each.numer(), each.denom()
    if x1^2 - 11*y1^2 == 1:
        break

for each in cs[1:]:
    x2, y2 = each.numer(), each.denom()
    if x2^2 - 11*y2^2 == 1:
        break

D = 11
big1 = 0x100000000000000
big2 = 0x1000000000000000

while True:
    x = (x1 * x2 + D * y1 * y2)
    y = (x1 * y2 + x2 * y1)
    if big1 < x * 3 < big2:
        break

    x2, y2 = x1, y1
    x1, y1 = x, y

num1,num2 = x*3,y*3
print('flag{%018d%018d}' % (num1, num2))

得到 flag flag{118936021352508390035860559716724409}

总结

这题咋说呢,re 套 crypto 有点捞(还是说只有我不会解 pell equation qaq),但是加载的部分还是挺有意思的,认真调一遍能对 PE 文件加载的过程有些更具体的认识。

推荐阅读:
Shiro 历史漏洞分析
浅谈pyd文件逆向
CVE-2022-23222漏洞及利用分析
Mimikatz详细使用总结

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。


跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup

版权声明:admin 发表于 2022年7月14日 下午1:28。
转载请注明:从 data 段中加载 PE Executable —— 2022-蓝帽杯-Reverse-Loader Writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...