Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1812827
  • 博文数量: 290
  • 博客积分: 10653
  • 博客等级: 上将
  • 技术积分: 3178
  • 用 户 组: 普通用户
  • 注册时间: 2007-10-24 23:08
文章存档

2013年(6)

2012年(15)

2011年(25)

2010年(86)

2009年(52)

2008年(66)

2007年(40)

分类: 网络与安全

2009-01-09 13:10:25

大家好,其实是前几天写简历的时候写了自己能手动脱壳,(手动脱壳,从寻找OEP->dump->IAT修复,最后成功拆除)
然而在脱壳方面的文章,一直没时间写,因为本来打算写逆向C++的,但是c++的内容绝不是三几句话能包含的,我打算自己
整理好以后会一并发出来;还忘大家谅解; 由于前不久瑞星电话面试的时候问我是不是能手动脱壳,遇到不熟悉的壳怎么办,
答: OEP-》DUMP—》IAT修复,最后问了找OEP的方法,为了证明下自己能做这个事,所以打算写篇脱壳的文章;当然也是为
了学习。技术方面的东西,我是报着学习的态度面对的;因为我知道我还是只小菜; 嘿嘿。。。废话不说;开始:
1:PE文件的装载
我们知道,一个普通的PE文件存放在磁盘中,在你不点击它之前,它其实和你电脑里的一张图片一样,形象的说来,
它成了个摆设,当鼠标双击它之后,shell调用CreateProcess函数打开一个有效的windows可执行文件,并且创建了
一个内存区对象,为的是稍后将它映射到内存空间中;然后通过调用WIndows的内部函数NtCreateProcess函数,创建了一个
windows执行体对象,以运行该映像;而创建执行体对象涉及以下几步(由创建线程来完成的):

1) 建立EPROCESS块
2)创建初始的进程地址空间
3)初始化内核进程块(KPROCESS)
4)结束地址空间的创建过程
5)建立PEB
6)完成执行体对象的创建过程 
由于,本文写的是脱壳,更多详细,请参考 Windows.Internals.Fourth.Edition
PE文件装载过程:(Undocumented Windows)

我们来看一下 loader 是如何解释 PE 文件,又是如何为执行准备内存 image 的。 loader 需要找到空闲的虚拟地址空间来将文件映射到内存。
 loader 尝试着将 image 加载在 preferred base address。成功后,loader 将 sections 映射入内存。loader 扫描 section table,
用每一个 section 的 RVA 加上基地址算出 section 的加载地址,然后将 sections 加载在相应的地址上。页属性是根据 section 的特征要求设定的。
将 section 映射入内存后,若基地址不等于 preferred base address,则 loader 开始进行基址重定位。之后检查 import table 并加载所需的 DLLs。
加载 DLL 与加载可执行文件的过程一样——映射 sections,基址重定位,解析 
imports等等。所有的 DLL 都加载了之后,就修改 IAT 使之指向实际的 imported 函数的地址。
成了! image 已准备好执行了。关于Load的文章,What Goes On Inside Windows 2000: Solving the Mysteries of the Loader 我放附件里,
由于时间爱你关系,只翻译了一小点,大家凑合看吧;

2 :壳以及壳的加载过程:
1)什么是壳?
    
我们可以把壳看成一个子程序,由它处理后的Pe文件在磁盘中一般是以加密后的形式存在的,有的壳还带有压缩功能,使得exe文件更加小巧,加壳在一定程度上可以防止破解者对程序文件的非法修改,同时可以防止程序被反编译;壳附加在原程序上通过Load载入内存后,却抢先于原程序执行,也就是在PE文件代码段执行之前抢先得到控制权; 然后在执行过程中对原PE文件加密,还原,还原后在把控制权交还给原程序;

2)壳的加载过程
 a:保存入口参数,加壳程序初始化时保存各寄存器的值,其实对windows来说,在每个子程序执行之前,总要保存ebx,edi,esi,ebp寄存器的值,而ecx,edx的值是不固定的,不能在返回时应用。特别注意:从 Windows API 函数中返回后,eax,ecx,edx 中的值和调用前不一定相同。当函数返回时,返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时,也必须尊守这一点,即在函数入口处保存段寄存器和 ebx,esp,esi,edi 的值并在函数返回时恢复。如果不这样一来的话,您的应用程序很快会崩溃。而通常,我们都用pushad/popad  pushfd/popfd指令保存和恢复现场环境,注意:上面说过,壳可以看成一子程序,它只是比没加壳的代码提前获得了控制权,所以基本在没一个壳的开头,总能看到这个指令:
00413000 >  60              pushad    

b:处理多次进入

c:模拟PE加载器完成相应的功能,处理完后将控制权交还给原程序;将控制权交给程序原入口点就是大家熟悉的(OEP)了; 在这一步中,还包括对输入表的处理,重定位表的处理,等; 由于加密加密三书上写的很详细,所以不在重复;
关于壳的加载过程,网上有篇文章:http://blog.chinaunix.net/u1/51827/showart_1757935.html 我转在我的blog上,有兴趣的可以看看;

3:手动脱壳三部曲
1)  查找程序的真正入口(oep)
2)  抓取内存映像文件(dump)
3)  Pe文件重建
到这里,我们可以开始练手了,注意: 不要在不熟悉pe文件格式的情况下,就想着手动脱壳,至少不要在还没搞清楚导入表和导出表,重定位表的情况下就想着更进一步,如果这样,无疑,你是在自找苦吃;
我用的加密解密三的例子:RebPe.exe
好了废话不说,开始三部曲第一部: 寻找OEP
这一部分涉及几种下断的方法和原理,如果不清楚的,赶紧翻开加密解密三,第二章,看吧,最近在看深入浅出MFC,记住一句话:勿在浮沙上筑高台;

寻找OEP的几种方法:
要是你对壳很熟悉,你当然可以一条指令一条指令的来,一直跟踪到代码段,兄弟我除了佩服你的技术精湛之余,还有向你学习的冲动; 但是,我们还是来用用书上的剩下的一些方法看看;

1:内存二次访问断点找OEP

   原理:外壳首先要将原来压缩的代码解压,并放到对应的区块上,处理完毕,将跳到代码段执行。这种方法的关键,要等到代码段解压完毕,再对代码段设置内存访问断点,而一般的壳,会依次对.code .data .rsrc区块进行解压处理,所以,可以现在非代码段上下内存访问断点,此时代码段已经解压,在对代码段设内存访问断点;操作如下:
a:)OD载入RebPe.exe 看到代码如下:
00413000 >  60              pushad
00413001    E8 C2000000     call    004130C8
00413006    2E:3001         xor     byte ptr cs:[ecx], al
00413009    0000            add     byte ptr [eax], al
0041300B    0000            add     byte ptr [eax], al
0041300D    0000            add     byte ptr [eax], al
0041300F    0000            add     byte ptr [eax], al
00413011    003E            add     byte ptr [esi], bh
00413013    3001            xor     byte ptr [ecx], al
00413015    002E            add     byte ptr [esi], ch
b:)Alt+M 打开内存窗口,对rdata设内存访问断点 : F2(,你也可以对其它的非代码段设断;)F9运行,暂停在:
00413145    A4              movs    byte ptr es:[edi], byte ptr [esi>
00413146    B3 02           mov     bl, 2
00413148    E8 6D000000     call    004131BA
0041314D  ^ 73 F6           jnb     short 00413145
在Alt+M ,对.Text设断 F2后,F9
003A0282    61              popad
003A0283    68 30114000     push    401130      ; OEP嘿嘿,看到了吧;
003A0288    C3              retn
003A0289    3011            xor     byte ptr [ecx], dl
我们跟踪,会发现,指令可读性较差,不用担心: ctrl +a ,OD会帮你: 此时,来到如下;也就是我们Pe的真正入口处了:
00401130  /.  55            push    ebp
00401131  |.  8BEC          mov     ebp, esp
00401133  |.  6A FF         push    -1
00401135  |.  68 B8504000   push    004050B8
0040113A  |.  68 FC1D4000   push    00401DFC                         ;  SE 处理程序安装
0040113F  |.  64:A1 0000000>mov     eax, dword ptr fs:[0]
00401145  |.  50            push    eax
00401146  |.  64:8925 00000>mov     dword ptr fs:[0], esp
0040114D  |.  83EC 58       sub     esp, 58
00401150  |.  53            push    ebx
00401151  |.  56            push    esi
00401152  |.  57            push    edi
00401153  |.  8965 E8       mov     dword ptr [ebp-18], esp
00401156  |.  FF15 28504000 call    dword ptr [405028]               ;  kernel32.GetVersion                    ;这个函数熟悉吧
0040115C  |.  33D2          xor     edx, edx
随便用PE查看器,看下入口点:00400000 
00401130 – 00400000 = 1130  (入口RVA) 
2:堆栈平衡原理找oep
 我们说过,在windows中调用子程序前,必须保存现场环境,而当子程序调用之前,必须恢复现场; 这一部分,可能你要,至少要对堆栈有个基本的了解,你可以参考我前面写的逆向C++中,函数,那一节,有一小部分介绍;而要继续深入,推荐arhat的 the shellcode handbook 一书。

我们知道,PUSHAD(Push All 32-bit General Registers)
指令格式:PUSHAD       ;80386+
其功能是把寄存器EAX、ECX、EDX、EBX、ESP、EBP、ESI和EDI等压栈。

POPAD(Pop All 32-bit General Registers)
指令格式:POPAD      ;80386+
其功能是依次把寄存器EDI、ESI、EBP、ESP、EBX、EDX、ECX和EAX等弹出栈,它与PUSHAD对称使用即可。

我们看看堆栈的变化:

Popad未执行前:  

0012FFC4   7C817067  返回到 kernel32.7C817067 ; 当前esp指向
0012FFC8   0012BBC4
0012FFCC   73FB49E4
0012FFD0   7FFD8000
0012FFD4   8054C6B8
执行后:
0012FFA4   0012BBC4    ;edi
0012FFA8   73FB49E4    ;esi
0012FFAC   0012FFF0    ;ebp
0012FFB0   0012FFC4    ;esp
0012FFB4   7FFD8000    ;ebx
0012FFB8   7C92E4F4  ntdll.KiFastSystemCallRet   ;edx
0012FFBC   0012FFB0     ;ecx
0012FFC0   00000000     ;eax
0012FFC4   7C817067  返回到 kernel32.7C817067

对0012FFA4下硬件断点: hr 0012FFA4

F9 ,程序中断在:

003A0282    61              popad
003A0283    68 30114000     push    401130  ;熟悉吧,OEP
003A0288    C3              retn  
其实堆栈平衡原理,是找第一个pushad 配对的popad,因为,在很多壳中,可能在处理数据的时候,还会调用其它子程序,这样,如果用观察法找配对 pushad 和popad
指令就会让初学者,眼花缭乱,相信我,我有过这种经历;所以,用一个硬件断点,方便了很多吧;

脱壳二部曲: dump 抓取内存映像
如果用OD里的dump插件,哈哈,那么IAT你都不用修复拉; OD真的很好用,偶喜欢;好期待有天我也能弄个这NX的插件出来,奉献给大家;不过,有什么用呢; 大牛们都写好了摆好了; 你会发现,脱壳后运行的程序,运行的非常好;
在这里,有一点补充下,有可能你加壳后,会发现文件执行的时候有错误;kanxue补充如下:
将记事本和计算器 中Directory Table 中的LoadConfig值清零,即可加。外壳程序处理时没有考虑LoadConfig。

下面用loadPE来: 右键—》完全转存; dump.Exe 双击,55555.。。。 不能用了吧;
 笑不出来了吧;没关系,今天宿舍就停电了,本来打算连IAT修复也写完的,555.。。。我不想在宿舍住拉,每次干得兴起就断电; 那就明天在来吧;今天先到这;

IAT修复:
1:)先来回顾下基础知识:
  首先,PE文件中的数据按照装入内存后的页面属性被分成多个节,并由节表中的数据来描述这些节;一个节中的数据仅仅是属性相同而已,并不一定是同一种用途;
  其次,由于不同用途的数据可能被放在同一个节中,仅仅靠节表是无法确定它的存放的位置的,PE文件中依靠可选头中的数据目录表来指出它们的位置;结构如下:typedef struct _IMAGE_OPTIONAL_HEADER {
     //
     //标准域 
     //
     USHORT  Magic;                   //魔数
     UCHAR   MajorLinkerVersion;      //链接器主版本号
     UCHAR   MinorLinkerVersion;      //链接器小版本号
     ULONG   SizeOfCode;              //代码大小
     ULONG   SizeOfInitializedData;   //已初始化数据大小
     ULONG   SizeOfUninitializedData; //未初始化数据大小 
     ULONG   AddressOfEntryPoint;     //入口点地址
     ULONG   BaseOfCode;              //代码基址
     ULONG   BaseOfData;              //数据基址
     //
     //NT增加的域
     //
     ULONG   ImageBase;                  //映像文件基址
     ULONG   SectionAlignment;           //节对齐
     ULONG   FileAlignment;              //文件对齐
     USHORT  MajorOperatingSystemVersion;//操作系统主版本号
     USHORT  MinorOperatingSystemVersion;//操作系统小版本号
     USHORT  MajorImageVersion;          //映像文件主版本号
     USHORT  MinorImageVersion;          //映像文件小版本号
     USHORT  MajorSubsystemVersion;      //子系统主版本号
     USHORT  MinorSubsystemVersion;      //子系统小版本号
     ULONG   Reserved1;                  //保留项1
     ULONG   SizeOfImage;                //映像文件大小
     ULONG   SizeOfHeaders;              //所有头的大小
     ULONG   CheckSum;                   //校验和
     USHORT  Subsystem;                  //子系统
     USHORT  DllCharacteristics;         //DLL特性
     ULONG   SizeOfStackReserve;         //保留栈的大小
     ULONG   SizeOfStackCommit;          //指定栈的大小
     ULONG   SizeOfHeapReserve;          //保留堆的大小
     ULONG   SizeOfHeapCommit;           //指定堆的大小
     ULONG   LoaderFlags;                //加载器标志
     ULONG   NumberOfRvaAndSizes;        //RVA的数量和大小
     IMAGE_DATA_DIRECTORY DataDirectory  [IMAGE_NUMBEROF_DIRECTORY_ENTRIES];   //数据目录数组
 } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

typedef struct _IMAGE_DATA_DIRECTORY {
     ULONG   VirtualAddress;       //虚拟地址
     ULONG   Size;                 //大小
 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY

// 各个目录项

 // 输出目录
 #define IMAGE_DIRECTORY_ENTRY_EXPORT         0
 // 输入目录
 #define IMAGE_DIRECTORY_ENTRY_IMPORT         1
 // 资源目录
 #define IMAGE_DIRECTORY_ENTRY_RESOURCE       2
 // 异常目录
 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION      3
 // 安全目录
 #define IMAGE_DIRECTORY_ENTRY_SECURITY       4
 // 基址重定位表
 #define IMAGE_DIRECTORY_ENTRY_BASERELOC      5
 // 调试目录
 #define IMAGE_DIRECTORY_ENTRY_DEBUG          6
 // 描述字符串
 #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT      7
 // 机器值(MIPS GP)
 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR      8
 // TLS(线程本地存储)⑥目录
 #define IMAGE_DIRECTORY_ENTRY_TLS            9
 // 载入配置目录
 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10

我们知道,从数据目录表引出的,仅仅是这些数据的RVA和数据块的尺寸,很明显,不同数据块的数据组织方式是不同的,比如导入表和导出表,描述它们的数据结构是不同的;如下:

 typedef struct _IMAGE_EXPORT_DIRECTORY {
     ULONG   Characteristics;            //特征
     ULONG   TimeDateStamp;              //时间日期戳
     USHORT  MajorVersion;               //主版本号
     USHORT  MinorVersion;               //小版本号
     ULONG   Name;                       //名字
     ULONG   Base;                       //基址
     ULONG   NumberOfFunctions;          //函数数
     ULONG   NumberOfNames;              //名字数
     PULONG  *AddressOfFunctions;        //函数的地址
     PULONG  *AddressOfNames;            //名字的地址
     PUSHORT *AddressOfNameOrdinals;     //名字序数的地址
 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                      //  in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

我们知道,PE文件在没有装入内存之前,导入表结构中由OriginalFirstThunk和FirstThunk指向的

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        PBYTE  ForwarderString;
        PDWORD Function;
        DWORD Ordinal;
        PIMAGE_IMPORT_BY_NAME  AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

结构数组中的值是相同的,注意,这个结构定义为一个联合,实际上就是一双字,这个结构用来指定一个导入函数,但双字的最高位是1时,表示函数是以序号的方式导入的,这时双字的低位字就是函数的序号,但双字的高位为0时,表示函数以字符类型的函数名方式导入,这时的双字的值就是一个RVA,指向一个结构:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中Hint表示函数的序号,name【】定义了导入函数的名称;

但是为什么要两个一模一样的IMAGE_THUNK_DATA数组呢,让我们先来回顾下调用导入函数的指令:
在RebPE中找到oep后,F8跟踪,你能看到这个函数:

00401150  |.  53            push    ebx
00401151  |.  56            push    esi
00401152  |.  57            push    edi
00401153  |.  8965 E8       mov     dword ptr [ebp-18], es>
00401156  |.  FF15 28504000 call    dword ptr [405028]     ;  kernel32.GetVersion

Call  dword ptr[405028],  直接寻址方式,将405028处的数据内存属性转换为Dword后作为函数的地址调用;
我们可以在提示窗口中:

ds:[00405028]=7C81126A (kernel32.GetVersion)   

可见,在这个地址中存放的是(kernel32.GetVersion)导入函数的入口地址;这样,call 调用将Eip压入堆栈后,程序执行流就到kernel32中了;

还有一种调用方式:
就是call  aaaaaaaa
 Aaaaaaaa   jmp dword ptr 【xxxxxxxx】,这个指令是一个间接寻址的跳转指令,如果你在16进制下查看这个ptr[xxxxxxx]的xxxxxxxx,你会发现它存放的是一个指向欲调用的导入函数名的字符串的RVA地址;当PE文件被装载时,windows装载器根据这个xxxxxxxx处的Rva得到函数名,然后用GetProcAddress函数找到内存中此导入函数的地址,并将xxxxxxxx处的内容替换成真正的函数地址;此时firstthunk指向的DWOWD数组中原本指向函数名的RVA被替换成导入函数的真正入口地址;

在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的,全部这写数组的组合也被称为导入地址表,IAT;导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址;还有就是数据目录表中的13项,可以直接用来定位IAT的地址; 不过一般还是以上一种方法为准;

到这里,我们可以来思考手动查找IAT的方法了,由于所有DLL模块的firstThunk数组一般都被放在一块,组成了输入地址表,注意: 这里是一般,不是完全,加密解密三上就有IAT被分开存放的例子,大家可以看下;
所以我们只要在找到OEP后,进入到原PE文件的代码块后,找到一个API调用的地址;如:
004011A4  |.  FF15 24504000 call    dword ptr [405024]     ; [GetCommandLineA
004011AA  |.  A3 F8894000   mov     dword ptr [4089F8], ea>

在[405024]右键-》数据窗口中跟随,内存地址;在数据窗口中数据如下:
00405024  AD 2F 81 7C 6A 12 81 7C FA CA 81 7C 1A 1E 80 7C  ?亅j 亅?亅  
阅读(11252) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~