Dual personality
天堂之门
在开始分析这个题之前,先介绍一个知识点:天堂之门
天堂之门是一种利用Windows的WoW机制,在32位的进程中执行64位程序代码,达到反检测的目的(反静动态分析)。
那么什么是WoW?他的全称是Windows 32 on Windows 64。众所周知,win64平台向下兼容,可以在x64平台上运行x86程序,而Windows之所以可以做到这点,就是因为使用了wow作为翻译层。
而天堂之门就是利用了这个机制,在32位程序中插入64位程序代码,让逆向分析工具不能正常分析成功。同理如果我们想要让他能够被正常分析,我们也只需要找到程序中被插入的64位代码,然后将其dump出来单独分析即可。
题目分析
重新回到这个题目,其中需要重点分析的函数是
这个函数的功能很简单,接收两个参数,一个是地址,我们将其称为add,另一个是长度,我们将其称为len。然后将调用函数的后len个字节的机器码修改为0xEA + add + 0x33 。并将未修改前的指令拷贝至新申请的内存中,然后再追加一条跳转回原地址的指令。
这个过程中,很明显关键的步骤是修改的机器码,其中有一个关键:0x33 。再看这个指令0xEA是jmp的机器码,在ida上面修改完之后会发现这是一个远跳
jmp far ptr 该指令会同时修改cs和ip的值,即将cs修改为0x33,ip修改为407060 。
在x86程序中,cs段的值为0x23,而在x64程序中cs的值为0x33 。
所以这段代码的作用其实就是跳转到指定地址后使用x64格式去执行该地址的代码。在ida中无法直接分析这段程序,我们可以直接将这段内存dump下来,然后重新使用ida64打开分析
![[Pasted image 20230213093430.png]]
这段代码首先读取了gs段的第62字节位置的数据,并将其存入40705c处。然后检测此值是否为0,如果不为0则跳转。如果没跳转,407058位置的变量会被赋值为5df966ae,然后返回407000中所存的地址处,如果跳转了,则直接返回407000中所存的地址处。
在x64环境下,gs寄存器存储了一些一些关于进程和线程环境的关键信息 例如:
gs:[0x30] TEB 线程信息块,保存线程信息的基本数据结构
gs:[0x40] Pid 进程id
gs:[0x48] Tid 线程id
gs:[0x60] PEB 进程信息块
而题目中使用的就是gs:[0x60]
,PEB进程信息块。这个结构的结构体在win xp版本中如下:
typedef struct _PEB { // Size: 0x1D8
000h UCHAR InheritedAddressSpace;
001h UCHAR ReadImageFileExecOptions;
002h UCHAR BeingDebugged; //Debug运行标志
003h UCHAR SpareBool;
004h HANDLE Mutant;
008h HINSTANCE ImageBaseAddress; //程序加载的基地址
00Ch struct _PEB_LDR_DATA *Ldr //Ptr32 _PEB_LDR_DATA
010h struct _RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
014h ULONG SubSystemData;
018h HANDLE DefaultHeap;
01Ch KSPIN_LOCK FastPebLock;
020h ULONG FastPebLockRoutine;
024h ULONG FastPebUnlockRoutine;
028h ULONG EnvironmentUpdateCount;
02Ch ULONG KernelCallbackTable;
030h LARGE_INTEGER SystemReserved;
038h struct _PEB_FREE_BLOCK *FreeList
03Ch ULONG TlsExpansionCounter;
040h ULONG TlsBitmap;
044h LARGE_INTEGER TlsBitmapBits;
04Ch ULONG ReadOnlySharedMemoryBase;
050h ULONG ReadOnlySharedMemoryHeap;
054h ULONG ReadOnlyStaticServerData;
058h ULONG AnsiCodePageData;
05Ch ULONG OemCodePageData;
060h ULONG UnicodeCaseTableData;
064h ULONG NumberOfProcessors;
068h LARGE_INTEGER NtGlobalFlag; // Address of a local copy
070h LARGE_INTEGER CriticalSectionTimeout;
078h ULONG HeapSegmentReserve;
07Ch ULONG HeapSegmentCommit;
080h ULONG HeapDeCommitTotalFreeThreshold;
084h ULONG HeapDeCommitFreeBlockThreshold;
088h ULONG NumberOfHeaps;
08Ch ULONG MaximumNumberOfHeaps;
090h ULONG ProcessHeaps;
094h ULONG GdiSharedHandleTable;
098h ULONG ProcessStarterHelper;
09Ch ULONG GdiDCAttributeList;
0A0h KSPIN_LOCK LoaderLock;
0A4h ULONG OSMajorVersion;
0A8h ULONG OSMinorVersion;
0ACh USHORT OSBuildNumber;
0AEh USHORT OSCSDVersion;
0B0h ULONG OSPlatformId;
0B4h ULONG ImageSubsystem;
0B8h ULONG ImageSubsystemMajorVersion;
0BCh ULONG ImageSubsystemMinorVersion;
0C0h ULONG ImageProcessAffinityMask;
0C4h ULONG GdiHandleBuffer[0x22];
14Ch ULONG PostProcessInitRoutine;
150h ULONG TlsExpansionBitmap;
154h UCHAR TlsExpansionBitmapBits[0x80];
1D4h ULONG SessionId;
1d8h AppCompatFlags : _ULARGE_INTEGER
1e0h AppCompatFlagsUser : _ULARGE_INTEGER
1e8h pShimData : Ptr32 Void
1ech AppCompatInfo : Ptr32 Void
1f0h CSDVersion : _UNICODE_STRING
1f8h ActivationContextData : Ptr32 Void
1fch ProcessAssemblyStorageMap :Ptr32 Void
200h SystemDefaultActivationContextData : Ptr32 Void
204h SystemAssemblyStorageMap : Ptr32 Void
208h MinimumStackCommit : Uint4B
} PEB, *PPEB;
win 7版本有所增加但关键部分大体相同,不再展示。在这个题目中我们主要关注偏移为2位置的BeingDebugged参数,当此参数为1时则表示该程序正在被调试器调试,否则值为0 。
所以在本题中mov rax,gs:[60h] mov al,[rax+2]
的操作即为取出BeingDebugged参数,以便在后续判断程序是否在被调试。
注:在x86环境下,使用fs:[30h] 字段取出PEB结构
继续回到题目,在了解了以上信息之后,就能够理解这段代码的功能:检测程序是否在被调试,如果在调试就直接返回,否则为407058h处的变量赋值。最后返回407000中存储的地址处,并调整为32位模式执行。而在sub_401120这个函数中,这个地址的值指向了新开辟出的内存。新开辟出的内存我们已经分析出来了。所以接下来的操作便是执行未被修改前的指令并跳转回原地址继续执行程序。
然后继续向下分析,注意这里的call fword ptr,这是一个远跳,会同时修改cs和ip的值,而byte_40700c中存储的值中cs的位置是0x0033所以,在这里又进入了64位程序。
将这个地址的指令dump出来,很显然,这里是将flag进行加密的位置,如果正在进行调试则用if分支进行加密,否则使用else分支进行加密,所以正确的加密是else分支中的部分
该函数执行结束后通过retf跳转回原位置,并将cs还原为0x23,转为32位。
然后下面通过sub_401120实现第三次跳转,转为64位程序执行,dump出其中的字节码
在这个函数中,对加密使用的key进行了处理,最后直接跳转到了sub_401120中保存的未修改前的代码,然后重新跳转回主函数继续执行,注意,由于这次没有切换回32位模式,所以主函数下面的代码全部都是64位格式指令。这也是下图位置ida分析出错的原因。
从dec eax
这里开始,下面的部分全部都是x64格式的指令,并不是存在花指令,想要分析还需要将其dump出来。
这个函数直接分析汇编可以看出他是一个循环,一共循环32(0x20)次,每次都将407014指向的字符串与407060指向的字符串取出一位进行异或,注意这里有一个div的操作,余数保存在dx中,所以这段代码的功能也就是 flag^key[i%4]
现在,整个程序还没有分析清楚的加密只剩下了一段
.text:004013E8 83 C4 08 add esp, 8
.text:004013EB 85 C0 test eax, eax
.text:004013ED 74 05 jz short loc_4013F4
.text:004013ED
.text:004013EF A1 58 70 40 00 mov eax, dword_407058
.text:004013EF
.text:004013F4
.text:004013F4 loc_4013F4: ; CODE XREF: .text:004013ED↑j
.text:004013F4 2D 11 41 52 21 sub eax, 21524111h
.text:004013F9 A3 58 70 40 00 mov dword_407058, eax
.text:004013FE C7 45 F4 60 70 40 00 mov dword ptr [ebp-0Ch], offset unk_407060
.text:00401405 C7 45 E8 00 00 00 00 mov dword ptr [ebp-18h], 0
.text:0040140C EB 09 jmp short loc_401417
.text:0040140C
.text:0040140E ; ---------------------------------------------------------------------------
.text:0040140E
.text:0040140E loc_40140E: ; CODE XREF: .text:0040144A↓j
.text:0040140E 8B 45 E8 mov eax, [ebp-18h]
.text:00401411 83 C0 01 add eax, 1
.text:00401414 89 45 E8 mov [ebp-18h], eax
.text:00401414
.text:00401417
.text:00401417 loc_401417: ; CODE XREF: .text:0040140C↑j
.text:00401417 83 7D E8 08 cmp dword ptr [ebp-18h], 8
.text:0040141B 7D 2F jge short loc_40144C
.text:0040141B
.text:0040141D 8B 45 E8 mov eax, [ebp-18h]
.text:00401420 8B 4D F4 mov ecx, [ebp-0Ch]
.text:00401423 8B 14 81 mov edx, [ecx+eax*4]
.text:00401426 03 15 58 70 40 00 add edx, dword_407058
.text:0040142C 8B 45 E8 mov eax, [ebp-18h]
.text:0040142F 8B 4D F4 mov ecx, [ebp-0Ch]
.text:00401432 89 14 81 mov [ecx+eax*4], edx
.text:00401435 8B 45 E8 mov eax, [ebp-18h]
.text:00401438 8B 4D F4 mov ecx, [ebp-0Ch]
.text:0040143B 8B 15 58 70 40 00 mov edx, dword_407058
.text:00401441 33 14 81 xor edx, [ecx+eax*4]
.text:00401444 89 15 58 70 40 00 mov dword_407058, edx
.text:0040144A EB C2 jmp short loc_40140E
这段代码并不复杂,设dword_407058中的值为x,则上式等价于
for(int i = 0; i < 8; i ++)
{
flag[i] += x;
x ^= flag[i]
}
综上所述,我们可以还原出整个加密逻辑
#include<iostream>
using namesapce std;
#define rol(x,i) ((x<<i)|(x>>(64-i)))
#define ror(x,i) ((x>>i)|(x<<(64-i)))
int main()
{
string flag = "******************************";
int x = 0x5DF966AE;
x -= 0x21524111;
for(int i = 0; i < 8; i ++)
{
flag[i] += x;
x ^= flag[i];
}
unsigned long long* pQwordFlag = (unsigned long long*)flag;
rol(pQwordFlag,12);
rol(pQwordFlag,34);
rol(pQwordFlag,56);
rol(pQwordFlag,14);
int key[] = { 0x9D, 0x44, 0x37, 0xB5 };
key[0] &= key[1];
key[1] |= key[2];
key[2] ^= key[3];
key[3] = ~key[3];
for(int i = 0; i < 32; i ++)
{
flag[i] ^= key[i%4];
}
}
所以我们可以写出解密脚本
for (int i = 0; i < 32; i++)
{
flag[i] ^= key[i % 4];
}
pQwordFlag = (unsigned long long*)flag;
*(pQwordFlag + 0) = (*(pQwordFlag + 0) >> 12) | (*(pQwordFlag + 0) << (64 - 12));
*(pQwordFlag + 1) = (*(pQwordFlag + 1) >> 34) | (*(pQwordFlag + 1) << (64 - 34));
*(pQwordFlag + 2) = (*(pQwordFlag + 2) >> 56) | (*(pQwordFlag + 2) << (64 - 56));
*(pQwordFlag + 3) = (*(pQwordFlag + 3) >> 14) | (*(pQwordFlag + 3) << (64 - 14));
delta = 0x5df966ae;
delta += 0xdeadbeef; //0x3CA7259D
for (int i = 0; i < 8; i++)
{
DWORD tmp = pDwordFlag[i] ^ delta;
pDwordFlag[i] -= delta;
delta = tmp;
}
printf("flag:DASCTF{%s}", flag);
Comments NOTHING