上篇说到
段寄存器一共有96位,可见部分只有16位,也就是我们在od看到的段选择子,还有80位是不可见的,我们可以通过mov指令进行读写[LDTR和TR除外]
段寄存器具有以下结构
struct SegMent
{
WORD Selector; // 段选择子 16位 可见
WORD Attributes; // 段属性 16位 不可见 只有少数几个属性位是有效的
DWORD Base // 段起始地址 32位 不可见 仅对于FS和GS有效
DWORD Limit // 段大小 32位 不可见 仅对于FS和GS有效
}
怎么证明这些属性是存在的呢,先抛开上回说的GDT(Global Descriptor Table 全局描述符表)概念,我们来写程序做个实验来探测一下这些属性。
下面先看一张表
相同的属性我不再写出来,至于这些属性怎么来的,先不用管,以后会提到。
探测Attributes
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, ss;
mov ds, ax;
mov dword ptr ds : [var], eax;
}
printf("%x\n", var);
system("pause");
return 0;
}
我们来执行一下
成功执行而且并没有报错,这是因为ds和ss都是可读可写的基址和长度都是一致的,所以并不会报错,这时候我们换成cs试一下
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, cs;
mov ds, ax;
mov dword ptr ds : [var], eax;
}
printf("%x\n", var);
system("pause");
return 0;
}
执行
可以看到这里直接产生了一个内存读写异常的崩溃,cs修饰的内存只有读和执行属性,并没有可写的属性,所以在写入的时候产生异常并崩溃,这也印证了段的Attributes
是确实是存在的,并且修饰了段的属性。
探测Base
从我们上面那张表看,只有fs寄存器存在Base,其他的段寄存器的Base全是0,所以我们用fs测试
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, fs;
mov ds, ax;
mov eax, ds:[0];
mov dword ptr ds : [var], eax;
}
printf("%x\n", var);
system("pause");
return 0;
}
这样写其实是会崩溃的,而且有两处错误
1.我们前面说过 MOV DWORD PTR DS:[0x132B2C4],EAX
这句指令真正写入的地址是ds.base+0x132B2C4
而现在可以通过下面的图看到ds已经被我们改变了,所以这时候引用ds修饰符其实是引用的fs的修饰符而fs.base+0x132B2C4
这个地址是不正确的,是无效的,因为我们ds的base是0,根本不会寻址到我们的参数var
进行赋值,所以我们可以将代码改为mov dword ptr ss : [var], eax;
因为ss和ds段修饰符是一致的。
2.虽然我们临时将ds改成了ss ,但是还是会造成崩溃,我们从下面的图可以看到,在跳出我们的汇编代码后,后续的代码也使用了ds,所以我们要将ds赋值回来才可以成功运行。
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, fs;
mov ds, ax;
mov eax, ds:[0];
mov dword ptr ss : [var], eax;
xor eax, eax;
mov ax, ss;
mov ds, ax;
}
printf("%x\n", var);
system("pause");
return 0;
}
代码成功执行,我们都知道,如果段中的base修饰如果不存在的话,直接读取0地址是肯定会产生异常,而我们代码中mov eax, ds:[0];
其实是mov eax,7FFDF000+0
,并不是真正的0地址,所以这里也验证了段寄存器中Base属性的存在。
探测Limit
还是使用上面的代码来进行测试
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, fs;
mov ds, ax;
mov eax, ds:[0x1000];
mov dword ptr ss : [var], eax;
xor eax, eax;
mov ax, ss;
mov ds, ax;
}
printf("%x\n", var);
system("pause");
return 0;
}
我们把mov eax, ds:[0];
改成mov eax, ds:[0x1000];
编译执行,产生了访问异常
而我们通过观察上面的表我们得知fs的段大小为0xfff,范围在0x7FFDF000~0x7FFDF000+0xFFF
,而我们读取的值在0x7FFDF000+0x1000
,已经超出了范围,我们反过来验证一下
#include <iostream>
#include <windows.h>
int var = 0;
int main()
{
__asm
{
xor eax, eax;
mov ax, fs;
mov ds, ax;
mov eax, ss:[0x7FFDF000 + 0x1000];
mov eax, ds:[0x1000];
mov dword ptr ss : [var], eax;
xor eax, eax;
mov ax, ss;
mov ds, ax;
}
printf("%x\n", var);
system("pause");
return 0;
}
我们可以看到mov eax, ss:[0x7FFDF000 + 0x1000];
已经成功执行eax
被成功赋值,如果再执行下一句mov eax, ds:[0x1000];
就会产生读写异常,为什么同样的地址通过不同的段寄存器去读取会产生这种差异,原因就是fs的Limit 为0xfff,如果读取fs.base+0x1000
就会产生越界读写,产生访问异常。
通过这几个简单的小实验,我们可以得知,段寄存器中不可见的80位是真实存在且有效的,只是我们看不见而已。
下篇来解决以前提过的问题,写入段寄存器的时候我们只好像给了16位,剩下的80位从哪里来。