Archive for the ‘汇编’ Category

天书夜读2.2思考与练习

int a, b, d;
d = a + b;
int i = 1, c = 0;
while(c < 0x64)
{
	c += i;
}
switch(c)
{
	case 0:
		d = 1;
	case 1:
		d = c;
		break;
	default:
		d = 0;
}
return d;

0xCC

经常在debug模式下看到有如下汇编代码用0xCC填充栈空间:

mov ecx, 30h

mov eax, 0CCCCCCCCh

rep stos dword ptr es:[edi]

今天才知道0xCC是int 3调试中断的机器码,恍然大悟。

浅淡对assume和org的理解

MASM中有些伪指令还真是不好理解,这里谈谈我对assume和org的一点理解。如有误,还请各位告知。

伪指令很好理解,就是无法将它们汇编成对应的机器指令。这里的assume和org也是如此,它们所影响的是编译时地址偏移的计算。

众所周知,内存的访问由内存地址决定。编译后的程序运行时具体被加载到哪是不得而知的,重定位可以通过程序被加载的基址+程序内偏移来确定,因此程序内偏移的确定就尤为重要了。

Intel系列CPU访问内存采用的是段式结构,因此无论是代码还是数据,其内存地址都需要指明段地址和段内偏移,访问时就需要一个段寄存器+一个段内偏移了。assume的作用就体现在这了,比如你在data segment中定义了一个变量var dd 0,而在代码中的访问方式如mov eax, var,由于没有指明段寄存器因此汇编器在编译时就需要计算出var的内存地址。如何计算?汇编中数据寻址默认是使用的ds段寄存器,因此只需确定var在ds段寄存器指明的段的段内偏移即可。如果没有assume伪指令,很明显,第一ds本身指向段地址在哪无法确定,第二就更别提var在ds的段内偏移了。

有了assume就不一样了,它解决了第二个问题。assume翻译过来为“假设”,assume ds: data就是假设ds指向的是data segment,而var在data segment中的偏移是可以确定的,如此var的段内偏移就确定了。但是assume还没能解决第一个问题,正如assume的翻译为“假设”一样,如果ds没有真的指向data segment,那么var的地址是ds:var在data segment中的偏移,必然是错误的。因此,实际上还需要在代码里手动mov ax, data; mov ds, ax;来让ds真正的指向data segment。

var定义在data segment中,那么var在data segment中的偏移是怎么出来的呢?这是因为MASM汇编器中有个“location counter”,由它来指示指令、数据在程序中的偏移,而org伪指令就是修改location counter的初值的。因此在程序中使用org后会使地址的计算要加上这个org值。

理清一个流程

之前写MBR是先用masm/link生成exe,再通过WinHex将该exe中0x7E00以后的数据复制到bochs的floppy中去。

这次试了下bochs中的freedos来运行程序。

用masm/link生成exe后,使用system32/exe2bin将exe的PE格式转换成纯二进制com。再用Linux虚拟机mount Windows中为与Linux共享准备的共享文件夹,其中含有bochs的floppy和转换后的com。再用Linux虚拟机mount bochs的floppy,最后sudo cp /mnt/floppy/ /mnt/share/xxx.com,这样就相当于把一个com文件放进了一个1.44的磁盘。剩下的就是在bochs的freedos中运行com程序了。

若/mnt下没有floppy可以通过sudo mkdir /mnt/floppy建立一个挂载点。

一个HelloWorld

VS2003 Debug配置编译了一个HelloWorld程序,主要代码如下:
int main()
{
 printf("Hello, world!\n");
 return 0;
}

用OD调试来学习一下它的运行机理。OD载入,程序停在如下位置:
00411339 > $ /E9 F2080000   jmp     mainCRTStartup

用LordPE查看下PE文件,发现镜像基址:00400000,入口点:00011339,00400000+00011339=00411339看来OD停在了入口点。

入口点jmp mainCRTStartup以及mainCRTStartup很明显是由编译器生成的。单步跟进mainCRTStartup或者在IDA中查看mainCRTStartup可以看到,在调用用户代码前后它都做了很多工作。
用户代码的调用点如下:
.text:00411D9B                 call    j__main

j_main也是由编译器生成的一小段存根代码,如下:
.text:004113BB ; int __cdecl j__main()
.text:004113BB j__main         proc near
.text:004113BB                 jmp     _main
.text:004113BB j__main         endp

为什么会这样?应该是库代码的框架是确定的,已经生成了obj?lib?or dll?但是用户代码的位置在链接前是不确定的(因为在不同的obj模块),通过这种存根的方法就能解决问题。

库代码和用户代码最终都被链接合并到了同一个exe文件中,如果是exe和dll之间的调用关系又该如何呢?
首先必须要知道要使用哪个dll的哪个函数,然后加载dll获取函数的地址,最后call或者jmp到函数中去。
PE的文件格式已经解决了如上问题。数据目录中的输入表目录指示了要使用哪个dll、该dll中有哪些导出函数以及在哪个位置保存这些导出函数的地址(IAT)。

HelloWorld.exe的输入表如图:

注意其FirstThunk的RVA:0002A17C,和数据目录的IAT RVA一致。打开IAT也可以发现其大小为4*62(kernel32.dll中有62个导出函数)用于保存函数地址。Windows Loader在加载dll的时候会负责正确地填充。

这样一来,可以说PE文件已经准备好了一切,调用dll变得简单call or jmp [IAT中对应函数的地址的偏移]。但这里依然有两种方式可选,一种是高效率的call [],另一种是低效率的存根方法(call 存根; 存根: jmp [];)。

IDA中可以发现类似如下代码:
.text:00411C79                 call    ds:__imp__GetVersionExA@4 ; GetVersionExA(x)
.text:004112D0 ; [00000005 BYTES: COLLAPSED FUNCTION GetVersionExA(x). PRESS KEYPAD "+" TO EXPAND]

它们分别对应着前面提到的两种选择,可见对一个导出函数的调用,编译器生成了两种可选方案,只是HelloWorld中最终没有发现有调用.text:004112D0处的代码。
虽然编译器生成了两种可选方案,但是可以对输入的dll导出函数加上__desclspec(dllexport)修饰符,就能在调用时使用高效率的方案。

main中的代码没有什么好解释的,提一下在main恢复堆栈后切换栈帧前,有如下代码:
.text:00411A60                 add     esp, 0C0h
.text:00411A66                 cmp     ebp, esp
.text:00411A68                 call    j___RTC_CheckEsp
.text:00411A6D                 mov     esp, ebp
.text:00411A6F                 pop     ebp
很明显j___RTC_CheckEsp堆栈平衡检查,但好像与securty cookie不同,并不会影响缓冲区溢出漏洞。

无觅相关文章插件,快速提升流量