首页
论坛
专栏
课程

[原创]【2019看雪CTF】Q2赛季 第七题 部落冲突 脱壳小记

2019-7-3 00:24 3126

[原创]【2019看雪CTF】Q2赛季 第七题 部落冲突 脱壳小记

2019-7-3 00:24
3126

【2019看雪CTF】Q2赛季 第七题 部落冲突 脱壳小记

这一题真是不会做。只能做做前面的准备工作,脱下壳什么的。

壳简要分析

一下载文件就被杀软杀了,可能有壳。
先拖进ida看了眼,果然有壳。目前入口处的部分伪代码如下(asm代码量有点大):

 get_api_addr_by_base_and_name_4D0564(l_kernel_base, &v141);// get LoadLibraryA
  p_GetProcAddress = (DWORD (__stdcall *)(int, char *))get_api_addr_by_base_and_name_4D0564(l_kernel_base, &v113);// GetProcAddress
  p_GetProcAddress(l_kernel_base, &v154);       // VirtualAlloc
  p_CheckRemoteDebuggerPresent = (void (__stdcall *)(signed int, int *, int))p_GetProcAddress(l_kernel_base, &v50);// CheckRemoteDebuggerPresent
  p_GetProcAddress(l_kernel_base, &v77);        // GetCurrentProcess
  p_IsDebuggerPresent = (int (*)(void))p_GetProcAddress(l_kernel_base, &v95);// IsDebuggerPresent
  p_ExitProcess = (void (__cdecl *)(_DWORD))p_GetProcAddress(l_kernel_base, &v167);// ExitProcess
  p_CreateFileA = (int (__cdecl *)(char *, unsigned int, _DWORD, _DWORD, signed int, signed int, _DWORD))p_GetProcAddress(l_kernel_base, &v179);// CreateFileA
  p_CreateMutexA = (int (__cdecl *)(_DWORD, _DWORD, char *))p_GetProcAddress(l_kernel_base, &v128);// CreateMutexA
  p_DeleteFileA = (void (__cdecl *)(char *))p_GetProcAddress(l_kernel_base, &v191);// DeleteFileA
  createMutex_lyd_4CE0C4(p_CreateMutexA);
  if ( caller_none_param(p_IsDebuggerPresent) )
    bug_4CE0A4();
  v9 = 0;
  ((void (__stdcall *)(signed int, int *))p_CheckRemoteDebuggerPresent)(-1, &v9);
  if ( v9 )
    p_ExitProcess(0);
  v204 = get_base_4D0304();                     // get program base
  v1 = v204;
  v6 = *(_DWORD *)(v204 + 60) + v204 + 24 + *(unsigned __int16 *)(*(_DWORD *)(v204 + 60) + v204 + 20);
  createfile_4CE0F4(p_CreateFileA, p_DeleteFileA);
  if ( p_IsDebuggerPresent() )
    p_ExitProcess(0);
  v49 = (_DWORD *)(v204 + *(_DWORD *)(v6 + 52));
  v203 = v49;
  v4 = (int *)(v1 + *(_DWORD *)(v6 + 92));
  v48 = (char *)v49 + *v49;
  for ( i = v49[2]; i < v203[1]; ++i )
    v48[i] ^= 0x55u;
  if ( CheckRemoteDebuggerPresent_4CE044(p_CheckRemoteDebuggerPresent) )
    bug_4CE0A4();
  write_data_and_api_4CE864((int)v48, v4, v203, (int)v49);

主要流程如下:

  1. 取API
  2. 创建名为lyd的互斥体
  3. 创建名为'-'的文件
  4. 解码代码数据
  5. 将代码数据恢复到程序相应地址(类似原PE的载入),并构建api间接调用机制(write_data_and_api_4CE864)

期间还有IsDebuggerPresent(strong OD无影响)、CheckRemoteDebuggerPr等反调试手段。

 

此处取的API是壳运行所用,创建互斥体及创建文件是与原程序代码遥相呼应起到反调试作用(更改数据)。

 

解码的数据为原PE4个Section的数据,偏移为0x400-0x9CA00,解码操作为与0x55异或。其实原PE被删了IMPORT TABLE,编码了Section后存放于地址0x4D0824,即文件偏移0x2C24。

 

进入write_data_and_api_4CE864函数后,先分别copy了原PE的头及4个Section到原虚拟地址处,再开始构建api调用机制。

 

原PE的IAT在虚拟地址0x421000,IT在0x42E034(基本被删完了)。为了阻止脱壳运行,在IAT部分也进行了点动作,壳不再在跳到OEP之前恢复IAT,而是构造了一种API间接调用的机制。此机制将call api变成了call 堆地址,IAT中存放的是堆地址,各堆中预设了代码,进行某些其它操作后,跳到API执行。预设代码有6种,随机分配,这样脚本恢复IAT就比较麻烦了,得先确定各代码堆对应的预设代码种类。此处用到了由原IT转换而来的表,位于虚拟地址0x59B000,文件偏移0xCC800,结构与IT有点类似,库文件名和API hash异或编码了。

 

此机制的构建过程比较清晰,不多说。组装好的中转代码其中一种典型样式如下:

006C0000    60              pushad
006C0001    9C              pushfd
006C0002    68 24A05900     push 0x59A024
006C0007    68 00004000     push 0x400000
006C000C    68 940A4D00     push 0x4D0A94                            ; ASCII ".rsrc"
006C0011    68 00E04C00     push 0x4CE000                            ; ASCII "$("
006C0016    E8 A9FCE0FF     call Tribalco.004CFCC4
006C001B    83C4 10         add esp,0x10
006C001E    9D              popfd
006C001F    61              popad
006C0020    68 70671C76     push kernel32.VirtualProtect
006C0025    50              push eax                                 ; Tribalco.00421004
006C0026    53              push ebx
006C0027    5B              pop ebx                                  ; Tribalco.<ModuleEntryPoint>
006C0028    58              pop eax                                  ; Tribalco.<ModuleEntryPoint>
006C0029    C3              retn
006C002A    C3              retn

6种代码样式中,跳到api均使用了push ;ret的方式。

 

构建完API中转调用后,会调用sub_4CFD74,实际操作为与0异或,开始地址为0x401000,大小为0x1D600,操作完后与改写0x4CE010处的参数,这一次操作实际是为后面解码作准备。此处也有反调试,如检测到调试,会改变异或值(加上0x85)。

 

sub_4CFD74还会在间接调用API时被调用,进行解码,具体情况如下:

  addr      size   op v
 0x416e00 0x1000  xor 2
 0x41f600 0x800   xor 4
 0x41fe00 0x400   xor 6
 0x420200 0x200   xor 8
 0x420400 0x100   xor 10
 0x420500 0x100   xor 12

write_data_and_api_4CE864函数返回后就直接jmp到了OEP。

脱壳

做题时看得没这么细致,看了下壳的大概结构,直接在jmp oep处patch成了死循环,运行程序再attach,然后dump。然后走一步看一步。
先发现IAT有猫腻,尝试多次终于用OD脚本获得IAT表;然后又发现dump下来的代码中还有SMC,IAT修复后(增加区段,改了IAT地址),SMC之后的代码call api就出问题了。

 

后来一想,其实可以直接从文件中把编码后的原PE分离开,然后解码,最后再修复IAT应该好点,至少文件小一半。

 

我当时的完整做法是:

  1. 跳转OEP前,patch程序成死循环
  2. 运行程序并attach,然后dump bin
  3. 用OD脚本改写IAT中的堆地址为API地址,得到IAT表
  4. 半手动修复dump_bin的IAT表(增加区段写IMPORT TABLE)
  5. 010 editor实现sub_4CFD74进行的6段解码
  6. 发现SMC会检查区段,直接去除SMC
  7. 去反调试(比壳中多了花样,除了壳中用的三种,前呼后应的两种,还有检查窗口标题和检查窗体风格)

最后附上脱壳之后的文件。不知是否还有问题。



[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

上传的附件:
最新回复 (3)
上海刘一刀 2 2019-7-4 10:45
2
0
看场雪 3 2019-7-4 17:03
3
0
大佬风范!
看场雪 3 2019-7-4 17:13
4
0
以后防守方出题,应该避免“攻击方看清了题目之后就放弃了”。这样不好玩
游客
登录 | 注册 方可回帖
返回