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


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

  1. 读取 gs:[0x60] 处的数据。如 Win32 Thread Information Block - Wikipedia 所述,该位置着指向当前进程 PEB 的指针。

  1. 读取 PEB:[0x18] 处的数据。偏移及字段的关系可以参考 PEB (geoffchappell.com) ,可知这里是获取的是 LDR 的指针。

  1. 进一步读取 LDR:[0x20] 处的数据。该结构体的细节可以参考 PEB_LDR_DATA (geoffchappell.com) ,可知这里获取的是 InMemoryOrderModuleList 的指针

LDR_DATA_TABLE_ENTRY of kernel32.dll

InMemoryOrderModuleList 是一个双向链表,链接了若干结构体,每个结构体都记录了加载进当前进程空间的一个模块。对于该进程,其链接的顺序是可执行文件,ntdll.dllkernel32.dll

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

Load API

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

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

Locate Export table

  1. 将调用处后面的地址保存到 rsi 中,看来此处的数据并非花指令

  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 字段

GetProcAddress and LoadLibraryA

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

  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( "%lx\n", compute_crc( "ABC", 3, 0xEDB88320 ); // a3830348
```

  1. rbx+0x20 指向了 Export table 的 AddressOfNames。该字段是一个列表,每个成员是 Export function name 与 PE 文件的偏移,因此此时 rdi 指向了当前校验的函数名,eax 保存着 checksum

  1. checksum[rsi] 相等时执行后续代码,否则接着去校验后面的函数名

  1. 动调可以发现当 function name 为 GetProcAddress 时校验通过,此时 rdx 寄存器即为 GetProcAddress 的 index,再去访问 Export table 的 AddressOfFunctions 并处理偏移即可得到该函数的绝对地址

  1. 此时 raxGetProcAddress 的地址,随后的 push 操作将其压入栈中,lodsd 在取值的同时会让 rsi+=4 ,即指向了下一个要定位的函数的 checksum

  2. 当再走完一遍上面的逻辑后,此时栈中已有 LoadLibraryAGetProcAddress 的绝对地址

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。

  1. 加载 dll 文件并获取其 IMAGE_IMPORT_DESCRIPTOR 的地址

  1. 通过循环来加载该 dll 中所有被使用到的 API

  1. IMAGE_IMPORT_DESCRIPTOR 处理完后再加载其他需要的 dll

Relocate

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

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

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

  1. 获取 Base Relocation Table 的绝对地址,存放到 rdi
  2. 遍历,对于每个要改写的地址,计算其绝对地址,随后将该地址的值改为 &code + offset

Execute from entrypoint

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

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

关键逻辑

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

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

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

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

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

此时上下文共有四个 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 文件加载的过程有些更具体的认识。

评论

J

JANlittle 2022-07-14 21:17:04

(图似乎裂了

R

r4ve1 2022-07-15 09:02:57

现在应该行了,昨天迁移了一下图床

R

r4ve1

这个人很懒,没有留下任何介绍

随机分类

企业安全 文章:40 篇
CTF 文章:62 篇
逻辑漏洞 文章:15 篇
硬件与物联网 文章:40 篇
memcache安全 文章:1 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录