软件保护技术复习笔记

引言:

23-24秋冬学期小白软件保护技术课程复习笔记,内容包括各种古早..呸..经典的逆向分析工具的使用方法,shellcode分析,EXE文件脱壳,DOS文件头,PE文件头分析,输入表,输出表,重定位表等。

1 工具使用

1-1 IDA Pro

IDA Pro是静态反编译软件(被exe转化成.asm甚至.c)

大部分快捷键与Ollydbg通用

1-1-1 常用命令

  • c命令可以把某部分机器码强行翻译成指令,纠错。

  • d命令可以把某部分机器码强行翻译成变量,还可修改变量宽度(连按),db,dw,dd。ALT+D还可设置更多数据类型。

  • u命令可以把某部分机器码强行转化为未定义状态,unknown。

  • n命令可以更改变量名,函数名,标号等

  • 同样用分号进行注释(汇编界面非C语言)

  • 按shift+f5可查看程序所调用的特征库。

  • ESC回退,Ctrl Enter前进,OD里面按Esc回退,`或~前进。

  • Tab 跳转到伪代码对应的汇编代码(已经打开伪代码窗口时,也可由汇编代码转到伪代码,即双向)

  • 定义数组 *

  • 定义字符串 a

1-1-2交叉引用(Cross Reference)

通过交叉引用,可以直到某变量/函数在程序中被使用/调用的位置及次数。

  • 双击变量
  • 在地址处ctrl+x
  • 弹出窗口,即为调用位置,双击即可跳转。

也可以用菜单栏实现:

  • 找到变量双击
  • 定位到地址处
  • 光标移动到分号后面,后面有一些引用
  • 选择view-open subview-cross referrence
  • 弹出窗口

1-1-3 字符串搜索

  • 在反编译窗口,ALT+T搜索字符串,不可加引号,对反编译的内容进行检索,速度较慢
  • 在反编译窗口,ALT+B搜索字节数据字符串需加引号,搜到的是字符串本身的内容,用来对机器码进行搜索,默认十六进制。

1-1-4 IDA 调试

  • F2 打断点

  • CTRL+ALT+B 打开断点列表

  • F4 运行到光标所在位置

  • F7单步步入,F8单步步过

  • F9 run

1-2 OllyDebug

在调试时OD(Ollydebug)的函数跟随:

  • 选中一条call 敲回车
  • 输入冒号,输入想定义的标签名。
  • 按esc退回。按~前进。call后变为标签

(Ctrl + F9快速运行完该函数体,Alt + F9可以直接跳过多层内核函数返回到当前运行的程序函数当中)

在调试时OD的注释

  • 选中一条指令
  • 输入分号,输入想添加的注释
  • 从右边把白线拉进,即可看到。

在调试时OD更改寄存器的值(包括EFL)。(EIP)除外

  • 选中寄存器
  • 双击(EIP的双击只能定位到EIP处,可起到goto的作用)
  • 输入想改变的数字。

在调试时OD改变EIP

  • 光标直到目标地址处
  • 点右键
  • 选择菜单
  • 选择此处为*EIP
  • EIP则跳转到目标地址处。

在调试时修改汇编语言

  • 选中指令
  • 直接输入更改
  • 若要修改机器码,可在数据窗中修改。
  • 也可在代码窗中输入db+后续机器码,将指令直接修改掉。

修改某个变量的值

  • 先Ctrl + g定位到变量的地址
  • 选中首字节,敲键盘数字键或A-F字母弹出修改对话框
  • 输入要修改的内容即可

(如果改坏了内存信息,可以选中这块内容按 Alt + Backspace恢复)

  • F2 设置软件断点,F7跟踪进入,F9运行到断点处

软件断点:把要设断点的指令的首字节改成0xCC,对应的*汇编指令为int 3。也就是说会直接修改代码段,此时如果有shellcode对代码段进行解密就会解密出错误的结果。

在指令中打硬件断点:

  • 选中某条指令右键->断点->硬件执行

  • 如何查看已设好的硬件断点: 调试->硬件断点硬件断点的触发条件除了执行(eXecute)外,还有读(Read)、写(Write)两种。

在内存中打硬件断点:

  • 选中某块内存的首字节->右键->断点->硬件写入-> Dword(宽度,也可以是Byte,Word等)
  • 运行,如果有对该内存的访问就会断住。
  • 对函数,标号,变量重命名:冒号
  • Alt + Enter 自动跳到指针所指的位置
  • OD点菜单-查看-查看内存-Ctrl+B-ASCII码搜索字符串:查找字符串在内存中的位置。

1-3 Turbo Debug

  • F8 Setup over为应用程序单步执行。

  • F2 Toggle为插入断点(CCh)

  • F4 Go to cursor运行至光标

  • F7 Trace into跟踪进入函数

  • F9 Run全程运行程序。

  • Ctrl+F2 program reset

  • Ctrl+o 回到当前将要执行的指令处

  • Ctrl+g 定位到某个地址

1-4 Soft-Ice

soft-ice比debug及turbo debugger更强大的最主要特性是其支持硬件断点:

硬件断点:

bpmb 内存地址 r; 读。此处的地址是变量地址

bpmb 内存地址 w; 写。此处的地址是变量地址

bpmb 内存地址 x; 执行。此处的地址是指令地址

x指令与F2的区别:

x断点并不改变指令中的任何机器码,只是把该地址存放在硬件调试寄存器中; 而F2断点则是把指令的首地址改成机器码0CCh,这个CCh对应的汇编指令为int 3,当调试器遇到int 3指令时会自动停住。

  • u 地址; 表示反汇编, 帮助里面对应的键是c

  • e 地址; 表示修改内存

  • d 地址; 查看内存变量

  • F8 跟踪一步

  • F5 继续运行

  • Alt+Pause 切换到debug窗口

  • dos mcbs 查看进程的psp

  • F2 设断点

  • bpm 地址; 设硬件断点

  • bc 清除断点

  • bl 查看断点

  • exit rd 强制结束对程序的运行

1-5 Bochs

双击里面的bochsdbg.exe,点Load,选择dos.bxrc,再点start,在调试窗口中输入c,会起来一个dos系统,选择NO_soft-ice,再输入:

**cd *

masm pro286x;

link pro286x;

pro286x

自动断住,回到调试窗,点view->stack在右侧显示堆栈。

  • 输入n单步执行,以后按F8重复n命令。

  • s命令表示trace into。

  • "x/64bx 地址"命令用来查看该地址指向的数据。

  • c命令表示继续运行。

  • "pb 地址"用来设置断点。

  • blist用来查看断点。

  • delete n用来删除断点。

1-1-5 QuickView

qv hello.exe打开文件

  • 不选中任何东西敲回车/F2实现反汇编界面,十六进制编辑界面,文本界面的切换。
  • F1 弹出帮助窗口

反汇编界面:

  • F5 输入地址 跳转到地址

  • F8 显示exe文件信息

  • F8 + F5 跳转到程序入口地址处

  • F8 + F3 快捷编辑文件信息

  • F7 搜索十六进制串(HEX)

  • F6 汇编代码搜索 直接跳转到代码所在位置

  • shift + f5 插入代码块

  • Insert 选中,上下左右选中数据区块 + F9 在制定区块内写入代码 再次 F9运行此代码

2 16位EXE脱壳

  • Process State Page(进程状态页):在操作系统中,PSP是进程控制块(PCB)中的一部分,用于存储进程的状态信息。PSP包含了进程的程序计数器、寄存器值、堆栈指针等信息,以便在进程切换时保存和恢复进程的状态。
1
2
3
4
5
6
7
8
一般来说,PSP是256个字节,当程序生成了可执行文件以后,在执行的时候,
先将程序调入专属内存,这个时候DS中存入程序在内存中的段地址,紧接着是程序的一些说明,
比如说程序占用多大空间等等,这就是PSP,一般PSP占256个字节,然后才是真正的程序地址,
CS指向这里,IP设为0000,为什么一般CS要比DS10H,就是因为这个原因。
简单说:DS存放的是程序段地址,由于PSP的存在,真正要执行的地址是DS再加上256个字节,
真正的地址是DS*16+256化简一下:DS*16+0+16*16=16*(DS+16
真正的地址又可以写成:cs*16+0
所以CS相当于DS+16,化成十六制是DS+10

程序首段地址= cs = ds/es + 0x10

2-1 Dos 文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // +0 Magic number //EXE标志 "MZ"
WORD e_cblp; // +2 Bytes on last page of file //最后部分(页中)的字节数
WORD e_cp; // +4 Pages in file //文件中的全部和部分页数
WORD e_crlc; // +6 Relocations //重定位表中的指针数
WORD e_cparhdr; // +8 Size of header in paragraphs //头部尺寸,以段落为单位
WORD e_minalloc; // +A Minimum extra paragraphs needed //所需的最大附加段
WORD e_maxalloc; // +C Minimum extra paragraphs needed
WORD e_ss; // +E Initial (relative) SS value //初始的SS值
WORD e_sp; // +10 Initial SP value //初始的SP值
WORD e_csum; // +12 Checksum //补码校检值
WORD e_ip; // +14 Initial IP value //初始的IP值
WORD e_cs; // +16 Initial (relative) CS value //初始的CS值
WORD e_lfarlc; // +18 File address of relocation table //重定位表的字节偏移量
WORD e_ovno; // +20 Overlay number //覆盖号
WORD e_res[4]; // +22 Reserved words //保留字
WORD e_oemid; // +24 OEM identifier (for e_oeminfo) //OEM标识符
WORD e_oeminfo; // +26 OEM information; e_oemid specific //OEM信息
WORD e_res2[10]; // +28 Reserved words //保留字
LONG e_lfanew; // +3C File address of new exe header //PE头相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

exe末尾并不载入内存的部分称为覆盖(overlay)

2-2 16位Dos文件手动脱壳

用Soft-Ice打开文件:

ldr hello.exe

运行到shellcode中解密语句的loop之后,一定不要走到重定位的部分

同时记录下在loop之前解密的段地址和解密长度:0x67c0(将ds的段地址067c转化成真实地址67c0),EDX: 0X2023C

在右侧Bochs虚拟机中 点Break断住。

在下面输入框中输入:

writemem "d:\K.dat" 0x67c0 0x2023c

把解密后的内容dump下来

之后打开010 editor,将原exe文件后面的旧头截取出来,扩展到200h字节,将刚才dump下来的东西放到旧头的下面,保存为exe,即脱壳。

2-3 自动脱壳

C代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

int get_file_size(FILE *fp)
{
int old_pos, length;
old_pos = ftell(fp);
fseek(fp, 0, SEEK_END);
length = ftell(fp);
fseek(fp, old_pos, SEEK_SET);
return length;
}

// int main(int argc, char* argv[])
int main()
{
FILE *infile, *outfile;
unsigned char *inbuf, *outbuf;
int infile_len, outfile_len;
// infile = fopen(argv[1], "rb");
// outfile = fopen(argv[2], "wb");
infile = fopen("hello2", "rb");
outfile = fopen("result.exe", "wb");
if(infile == NULL || outfile == NULL)
{
printf("NULL");
return 0;
}
infile_len = get_file_size(infile);
inbuf = (unsigned char *)malloc(infile_len);
fread(inbuf, 1, infile_len, infile);
fclose(infile);
/*
Write your code here
*/

unsigned char *p;
p = inbuf;
int flag = 0;
while(!flag){
if(*(p) == 0xe8 && *(p+1) == 0x00 && *(p+2) == 0x00 && *(p+3) == 0x5b) flag = 1;
p++;
}
p--;
// 定位shellcode位置

unsigned char* new_header = p + 0x196; //定位头位置
unsigned short header_len = (new_header[8] + (new_header[9] << 8)) * 0x10;
unsigned short offset_len = *(unsigned short *)(new_header + 2);
unsigned short num_len = *(unsigned short *)(new_header + 4);
if(offset_len == 0) outfile_len = (num_len) * 0x200;
else outfile_len = (num_len - 1) * 0x200 + offset_len;
// outfile_len = (num_len - 1) * 0x200 + offset_len;
// 计算长度
outbuf = (unsigned char *)malloc(outfile_len);
memset(outbuf, 0, outfile_len);

int a = (inbuf[8] + (inbuf[9] << 8)) * 0x10;
int i ;
int j = header_len;
for(i = a; i < p - inbuf ;i++){
outbuf[j++] = ( ((inbuf[i] >> 4))|((inbuf[i] << 4)) ) ^ 0x57;
}
outfile_len = j;
// printf("%x", outfile_len);
// printf("%x %x %x", *new_header ,offset_len, num_len);
memcpy(outbuf, new_header, header_len);
// printf("%x", infile_len - (new_header - inbuf));
if(outfile_len % 0x200 == 0)
{
outbuf[4] = (outfile_len / 0x200) % 0x100; outbuf[5] = (outfile_len / 0x200) / 0x100;
outbuf[2] = 0x00; outbuf[3] = 0x00;
}
else
{
outbuf[4] = ((outfile_len / 0x200)+1) % 0x100; outbuf[5] = ((outfile_len / 0x200)+1) / 0x100;
outbuf[2] = (outfile_len % 0x200) % 0x100;
outbuf[3] = (outfile_len % 0x200) / 0x100;
}
//--------------------------
fwrite(outbuf, 1, j, outfile);
fclose(outfile);
return 0;
}



也可以在exe文件内加代码实现自动脱壳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
.386
code segment use16
shell:
call next
next:
pop bx ; bx=next的运行时的偏移地址
; 现在想知道shell的地址
sub bx, offset next-offset shell
cli
mov ax, cs
mov ss, ax
; mov sp, offset stk + 100h是错误的,默认shell的地址为0了
lea sp, stk[bx + 100h] ; 正确的
sti

push ds
push es
mov ax, ds
add ax, 10h
mov ds, ax
mov es, ax
mov dx, cs
sub dx, ax
movzx edx, dx ; 0扩充
shl edx, 4
add edx, ebx
add edx, ebx; edx = len of code to be decoded
xor si, si
xor di, di
decode_next:
losb
xor al, 42h
stosb
dec edx
cmp si, 0 ; 判断si已经解密了10000h个字节
jne skip
mov ax. ds
add ax, 1000h
mov ds, ax
mov es,ax
skip:
dec edx
jnz decode_next
mov cx, [bx + reloc_count]
pop es
pop ds
lea si, [bx +reloc_table] ; si = bx + reloc_table
reloc_next:
mov di, ds:[si] ; 先取偏移地址
mov dx, ds:[si+2] ; 再取段地址,此地址为相对的段地址
add dx, bp ; 加上bp得到真实的段地址
mov es, dx
add es:[di], bp
add si, 4
loop reloc_next
; 将堆栈切换为老堆栈
add [bx+delta_ss], bp
add [bx+delta_cs], bp
pop es
pop ds
cli
mov ss, cs:[cx+delta_ss]
mov sp, cs:[bx_old_sp] ; 激活用户堆栈
sti
jmp dword ptr cs:[old_ip]



; 下面进行一些原始数据的定义和保存
delta_ss dw 0
old_sp dw 0
old_ip dw 0
delta_cs dw 0
reloc_count dw 0 ; 重定位项
reloc_table db 200h dup(0) ; 重定位表
; 按照exe里的数据把这些东西抄下来
stk db 100h dup(0) ; 加标号
code ends
end

3 PE文件格式

3-1 PE文件头

  • 如何判断某个exe文件PE格式的 文件
    • 用qv打开exe,定位到3C,读取3C中的值,再定位到这个值,若为50, 45,即为PE

windwos的exe是从cs:400000h处开始载入的,其中400000h称为base addr

PE+28处查到的32位值是delta_eip,此值需要跟base addr相加才能得到真实的eip的值。

像PE+28处得到的这种相对地址称为RVA(Relative Virtual Address)

如果RVA = 1000h,那么程序真正的入口地址为401000h

PE +:(以下偏移以PE为基址)

+6:count of sections,节的个数

+28:程序的32位入口地址RVA。也称为OEP,即Original Entry Point。

+34:程序载入内存的基址,EXE一般都等于400000h

+38:内存地址对其标准,例如1000h对齐

+3c:文件对齐,如200h

+50:EXE文件载入内存后的总长度

+54 EXE文件头的文件长度,载入内存后会按内存对齐长度拉伸

EXE载入内存总长度的计算方法:最后一个节的偏移地址+该节拉伸后的长度

文件末尾EOF字节的下标等于文件的总长度

windows中,在将exe文件加载进内存时,把PE文件头当做一个节,同样做0x1000拉伸。此时.text段从0x1000开始,不是从0000开始的。

3-2 动态库与静态库

对于动态链接产生的exe,所调用的函数/API的函数体本身并不在exe中,在exe执行并载入内存时编译器到指定的dll中将调用到的函数加载进虚拟内存中。

对于静态链接产生的exe,所有调用的函数的函数体本身都会被直接编译到exe里。

总而言之,动态链接所调用的某些API的代码在exe文件中是找不到的,而静态链接所调用的API的代码在exe文件中总能找到。

  • 动态链接:

Windows下的动态链接库为xx.dll(dynamic linked library)

在windows中有两个函数调用用来返回某个API的地址,例如:

1
2
h=LoadLibrary("user32.dll");
p=GetProcAddress(h, "MessageBoxA");

其中user32.dll是一个动态链接库,他是多个API函数体的集合,Windows启动时它会自动载入内存

  • 静态链接:

windows下静态库为XXX.lib

由于printf源代码没有开源,所以对printf的机器语言代码是一个obj文件,设为printf.obj,而这个obj文件又包含在一个静态库文件cs.lib中

编译main.c时,编译器会打开cs.lib并搜索printf.obj,最后把printf.obj和main.obj整合起来生成main.exe

main.c -> compile -> main.obj /*中间产物*/

在编译时,发现printf函数在该程序中不存在,编译器会自动推断它是一个外部函数,到默认的库中去寻找(cs.lib),找到后仅仅把printf函数的段复制出来,和原先的main.obj合并在一起(link)

拼接成功后会产生一个main.exe

4 输入表,输出表,重定位表

EXE文件中通常只包含输入表(用于API地址重定位);

DLL文件中一般包含:输入表(用于API地址重定位)、输出表、重定位表(用于变量地址的重定位);

EXE及DLL通常都包含资源表(用来定义菜单、对话框、图标等);

4-1 输入表

PE + 80 输入表的内存偏移

PE + 84 输入表的内存长度

  • 一个exe里面可以包含多份输入表,输入表不止有一份,一份输入表对应一个DLL及该DLL包含的函数。比如某个exe既调用了user32.dll!MesssageBoxA,又调用了kernel32.dll!GetProcAddress, 那么这个exe的输入表就有两个。
  • 输入表在文件中结束的标志是有连续的0x14个连续的0,这样操作系统可以识别输入表在哪里结束。

输入表可以有多项,每一项的组成如下:

+C 指向DLL名

在文件中,动态库名称后面必须紧跟0

+0 指向API名字指针表

指针表即指针数组,即每一项都是指针。

其中指针指向地址的前两位xx,xx是非法的字符

将指针+2,可以得到真正的API的名字。

前两个非法字符是API的序号

  • windows通过判断指针带入的数值的大小判断是API的序号还是名字。其中API的序号一定是一个16位的数。

+10 指向API地址表

在文件中查看其和API名字指针表一模一样

他指向API的名字的地址。但在程序运行过程中,该表可以被改变,而+0处的指针所指向的表不会被改变。

1
call GetProcAddress //此时call的是+10处指向的的地址表

输入表的每一项有14h个字节,里面包含了上述3个指针。

4-2 输出表

PE + 78 输出表的内存偏移

PE + 7C 输出表的内存长度

  • 输出表一般存在于动态链接文件DLL中,用于储存API的地址和名字

  • 输出表仅有一份,而输入表可以有多份

输出表的组成:

+0c DLL名 (同输入表)

+1c API地址表 (同输入表)

+20 API名字指针表 (同输入表)

+24 API序号表

+14 API个数

+18 API个数

+14和+18两处的count的含义略有不同

+14表示有名字的API的个数+没有名字只有序号的API的个数

+18表示有名字的API的个数

+10 API序号的基数

windows中,在将exe文件加载进内存时,把文件头当做一个节,同样做0x1000拉伸。此时.text段从0x1000开始,不是从0000开始的。

4-3 重定位表

PE + A0 重定位表的内存偏移

PE + A4 重定位表的内存长度

  • 计算重定位项数: (重定位表长度 - 8)/ 2

重定位表的组成:

+00 重定位项基地址

+04 当前重定位表长度

+08 每两个字节构成一个重定位项

重定位表表示某个变量的地址需要做修正。重定位的过程在exe载入内存时,由操作系统完成。

由于每次程序载入的程序首段地址都不同,我们在编写代码的时候可能会将一些地址写成绝对地址而不是相对地址。这是就需要根据实际情况对一些取地址或地址访问类的指令进行重定位,也就是直接修改指令的机器码。

重定位表的作用就是让操作系统能够知道由多少个指令需要重定位,这些指令都在哪等信息,操作系统就是根据这些信息自动修改机器码了。

需要注意:

  • 并不是所有的exe程序都有重定位表,但是DLL却是必须需要重定位信息。

4-3-1 提取重定位项

+08 按照小端规则提取前两个字节,最高位3忽略掉,+重定位项基地址,得到重定位项的内存偏移。将内存偏移转化为文件偏移,得到重定位项。

计算重定位项数: (重定位表长度 - 8)/ 2

转到:(直接指向code段)

1
1034: FF1504500010 CALL DOWRD PTR [10005004]

在exe开始执行时windows操作系统会对该条指令方括号内的数字进行修正。

1
1034: FF1504500030 CALL DOWRD PTR [30005004]

不论该数字是否外面有括号,windows操作系统都需要对该数字进行重定位,因为该数字是地址。

4-3-2 对变量地址的重定位

设DLL载入内存的基址为100000h, 则需要重定位的变量地址位于100000h+1B87h=101B87h

**设DLL编译时设定的基址为a(位于PE+34),

该DLL实际载入内存的基址为b, 则计算Δ=b-a

再add dword ptr [101B87h],Δ就完成一项重定位。

4-3-2-1 Dos 16位文件

重定位表的内存偏移:+18位置

重定位项数 :+6位置

重定位表的长度= +6处描述的重定位项数 * 4

  • 如何定位重定位项在文件中的地址?

假设重定位项的段地址为 2, 偏移地址为 D

其在文件中的位置为:

200h + 2 * 10h + D = 221h;

  • 如何防止操作系统进行重定位干扰壳代码的加密?

将原exe头的重定位项改为0,在shellcode里自己负责重定位。

4-3-2-2 PE文件

PE + A0 重定位表的内存偏移

PE + A4 重定位表的内存长度

  • 重定位项数 = (重定位表的长度 - 8 )/ 2