ollydbg 中汇编指令基本原理
本阶段目标:
1 加深阅读调试器中的汇编代码的能力
2 加深对Windows底层理解
3 解决下层bug的能力.
1 第一个实例:
int main(int argc, char* argv[])
{
return 0;
}
反汇编后:
00401010 >|> 55 PUSH EBP
00401011 |. 8BEC MOV EBP,ESP
00401013 |. 83EC 40 SUB ESP,40
00401016 |. 53 PUSH EBX
00401017 |. 56 PUSH ESI
00401018 |. 57 PUSH EDI
00401019 |. 8D7D C0 LEA EDI,DWORD PTR SS:[EBP-40]
0040101C |. B9 10000000 MOV ECX,10
00401021 |. B8 CCCCCCCC MOV EAX,CCCCCCCC
00401026 |. F3:AB REP STOS DWORD PTR ES:[EDI]
00401028 |. 33C0 XOR EAX,EAX
0040102A |. 5F POP EDI
0040102B |. 5E POP ESI
0040102C |. 5B POP EBX
0040102D |. 8BE5 MOV ESP,EBP
0040102F |. 5D POP EBP
00401030 \. C3 RETN
指令说明:
STOS 是串存储指令, STOS DWORD PTR ES:[EDI]的功能是将EAX的值放入EDI中,同时EDI自动加4个字节. REP 重复执行ECX所指的数.
2 使用LIST_ENTRY
LIST_ENTRY是一个双向链表结构,它总是在使用的时候被插入到已有的数据结构中.
例:保存文件的文件名和长度
typedef struct _FILE_INFOR{
LIST_ENTRY list_object;
PFILE_OBJECT file_object;
UNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;
//如果作为链表头,使用时必须初始化
LIST_ENTRY my_list_head;
void MyFileInforInit()
{
InitializeListHead(&my_list_head);
}
typedef struct _FILE_INFOR{
LIST_ENTRY list_object;
PFILE_OBJECT file_object;
UNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;
NTSTATUS MyFileInforAppendNode(PFILE_OBJECT file_object,
PUNICODE_STRING file_name,
PLARGE_INTEGER file_length)
{
PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(
PagedPool, sizeof(PMY_FILE_INFOR), MEM_TAG);
if(MY_FILE_INFOR == NULL)
return STATUS_INSUFFICIENT_RESOURES;
//填写数据
my_file_infor -> file_object = file_object;
my_file_infor -> file_name = file_name;
my_file_infor -> file_length = file_length;
InsertheadList(&my_file_infor, (PLIST_ENTRY)& my_file_infor);
return STATUS_SUCCESS;
}
3 打开文件
//文件句柄
HANDLE file_handle = NULL;
NTSTATUS status;
//初始化含有文件路径的OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTES object_attributes;
UNICODE_STRING ufile_name = RTL_CONST_STRING(L”\\??\\C:\\a.dat”);
InitializeObjectAttributes(&object_attributes,
& ufile_name,
OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
NULL,
NULL);
//以OPEN_IF方式打开文件
status = ZwCreateFile(&file_handle, GENRIC_READ|GENERIC_WRITE,
&object_attributes, & io_status, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL,
0);
4 单条指令的组成
前缀 (1个字节) (可选) |
操作数
|
MOD-REG-R/M (可选) |
SIB (可选) |
地址偏移 (可选) |
立即数 (可选) |
(1)前缀:普通前缀
* 重复前缀
与锁同样的类型,f2h 条件: 当0标记为1 && rec!= 0重复
f3h 条件: 当0标记为0 && rec!= 0重复
* 锁前缀 f0h
用来锁总线的
* 超前前缀
指示性前缀
重复前缀也可以是指示性前缀用于SSE
* 寄存器超越前缀 66h
如果在32位模式下使用16位寄存器,那么这条指令将出现66h前缀
如果在32位模式下使用16位地址,那么将出现67h前缀
* 地址超越前缀 67h
64 位扩展前缀REX
REX前缀只在64位模式下有效,这个前缀必须紧靠操作码,不然将被忽略,REX的范围40h~4Fh.
7~4位 |
3位 |
2位 |
1位 |
0位 |
REX,必须为0100h |
W(Width) |
R(Register) 用于扩展MOD-REG-R/M |
X(indeX) 扩展SIB索引字节 |
B(Base) 扩展MOD-REG-R/M或者SIB基域 |
(2)操作码被分成1-2个字节,一般在一个字节内,被分为几个部分.(不是全部操作码都是如此)
7~2位,操作码 |
D |
W |
W = 1,传送多少个字节由寄存器前缀决定,W=0,传送都是1个字节.
D: 传输的方向 D = 1时数据从R/M传输到REG,
D =0 时数据将从REG传输到R/M
(3) MOD – REG – R/M的组成
这个字节的作用则是确定指令是否需要SIB字节,地址偏移和立即数
7~6位MOD |
5~3位REG |
2~0位R/M |
MOD模式
MOD的值 |
意义 |
00 |
没有使用地址偏移 |
01 |
使用8位的地址偏移 |
10 |
使用16位或32位的地址偏移当前模式决定是16位还是32位,可以被超前模式改 |
11 |
仅仅使用通用寄存器,此时R/M一定是寄存器,此外的情况是地址 |
REG-R/M:当R/M表示寄存器的时候,和REG表示寄存器部分表示相同都是3个位,表示0~7的时候分别表示了8种寄存器.
REG或R/M |
字节 |
字 |
双字 |
SSE |
MMX |
000 |
a1 |
ax |
eax |
ss0 |
mm0 |
001 |
c1 |
cx |
ecx |
ss1 |
mm1 |
010 |
dl |
dx |
edx |
ss2 |
mm2 |
011 |
bl |
bx |
ebx |
ss3 |
mm3 |
100 |
ah |
sp |
esp |
ss4 |
mm4 |
101 |
ch |
bp |
ebp |
ss5 |
mm5 |
110 |
dh |
si |
esi |
ss6 |
mm6 |
111 |
ah |
di |
edi |
ss7 |
mm7 |
考虑64位扩展前缀时的情况,REX.R为1,MOD-REG-R/M的REG域扩展成了4位,可以索引16个寄存器.如果REX.B 为1,MOD-REG-R/M扩展成了4位,同样可以索引16个寄存器.
当R/M是内存地址时,需要知道的是表示那个地址.
R/M的值 |
表示的地址 |
000 |
ds:[eax] |
001 |
ds:[ecx] |
010 |
ds:[edx] |
011 |
ds:[ebx] |
100 |
使用SIB字节 |
101 |
ss:[ebp]使用特殊寻址 |
110 |
ds:[esi] |
111 |
ds:[esi] |
当MOD为00并且R/M为5时(表示ebp)时表示MOD-REG-R/M字节后还有地址偏移,即[ebp+00]
当R/M为5时,也可以使用指令访问内存地址.mov [0x12345678],ebx
64位模式下,使用mov [rip + 0x12345678],ebx
(4)SIB意义Scale – index – Base占用一个字节
7~6位 |
5~3位 |
2~0位 |
Scale |
Index |
Base |
SIB是可选的,在MOD-REG-R/M后面用于R/M部分使用比例变址寻址方式,SIB表示一个地址.计算方法:
地址 = Base(表示寄存器内容) + Index(表示寄存器内容)*(2^Scale)
REX.X为1时,Index被扩展为4个位
REX.B为1时Base扩展为4个位,此时表示16位寄存器
(5)地址偏移:是否使用依赖于MOD-REG-R/M,可能是 1. 2. 4字节如果存在,则位于MOD-REG-R/M或者SIB后面
(6)立即数:取决于寻址方式的需要,为有符号数可能1,2,4个字节
5 分页内存不可执行保护
31 – 12位地址 |
11 – 6位保留 |
D |
A |
PCD |
PWT |
U位 |
W位 |
P位 |
P位: 表示这个页是否存在.
W位:表示是否可写.
U位: 是一个用户自定义的属性,如果为1,表示这是用户项.可以被R0和R3级的代码访问,如果为0,是系统页,只能被R0级别的代码访问.
页面只有读写的控制,并没有可执行的控制.可以通过简单的跳转指令,跳转到某一个地址.
注意;Windows留下了关闭NX保护开关,在boot.ini中,windows内核启动参数增加/EXECUTE即可关闭此功能.
6 sysenter虽然在R3下执行,但是跳转到目的地址却一定要在R0下制定,换句话说就是,可以再R3下调用sysenter,却无法控制跳转的地址.
跳转的地址是由windows预先设置的,地址的设置方法,这些地址保存子啊3个特殊的寄存器中.
寄存器 |
功能 |
代号 |
SYSENTER_CS_MSR |
保存跳转之后cs的内容 |
174h |
SYSENTER_CS_ESP |
保存跳转之后esp的内容 |
175h |
SYSENTER_CS_EIP |
保存目标地址 |
176h |
CS : 选择一个GDT中段描述符,R3不能修改
esp:堆栈指针.
eip: 跳转之后执行的第一条指令的目标地址,这个被windows固定死了.
设置方法:调用wrmsr指令,这是特权指令.
mov ecx, 0x8 ; windowsxp下代码段选择子
mov dword ptr eds:[eax], 0x8??????? ;固定跳到某个内核地址
wrmsr ;写入
值得注意的是:SYSENTER_CS_MSR总是被设置为8,windows中所有的内核代码段都共用这个描述符-实际计算的段基地址总是0,这是为何windows下虚拟地址总是等于线性地址的原因
此外进入sysenter执行后的堆栈段,以及sysexit返回后的代码段和堆栈段也需要确定段选择字.只需设置一个SYSENTER_CS_MSR就可以
段选择子 |
功能 |
SYSENTER_CS_MSR值 |
用于sysenter执行后代码段 |
SYSENTER_CS_MSR值+8 |
用于sysenter执行后堆栈段 |
SYSENTER_CS_MSR值+16 |
用于sysexit执行后代码段 |
SYSENTER_CS_MSR值+24 |
用于sysexit执行后堆栈段 |
从上表可以看出,既然内核代码段的选择子总是8h,那么堆栈段的选择子只能是10h而R3代码段的选择子就只能是1bh
总结:sysenter
(1)装载SYSENTER_CS_MSR到cs寄存器
(2)装载SYSENTER_EIP_MSR到eip寄存器
(3)装载SYSENTER_CS_MSR+8到ss寄存器
(4)装载SYSENTER_ESP_MSR到esp寄存器
(5)切换到R0层
(6)清除eflags寄存器中vm标志
(7)执行eip开始的R0例程.
sysexit
(1) SYSENTER_CS_MSR值+16装载到cs寄存器
(2)将edx的值送入eip
(3) SYSENTER_CS_MSR值+24装载到ss寄存器
(4)将ecx的送入ESP
(5)切换回 R3
(6)执行eip处的R3指令
经验:
1. 如果使用RtlCopyUncicodeString拷贝字符串,必须保证拷贝的字符串有足够的空间,如果没有调用RtlinitEmptyString让字符串为空,可能会失败.
2. DbgPrint 应用中, #define KdPrint DbgPrint,由于KdPrint只接受1个参数,使用时必须将参数用括号抱起来.
3.