既然我们发现了指令不过就是一些字节的组合,我们可以尝试抛开C语言,自己构造指令执行。我们完全可以分配一段内存,然后将mov指令的机器码填入。但是,如何让CPU执行我们的代码呢?
假定我们构造了一段mov指令,需要用jmp语句跳转到该指令。
如果我们执行完mov指令后就不管了的话,我们就会发现程序会异常退出。比如下面的程序
#include "stdafx.h"
#include <iostream>
using namespace std;
int gi = 0;
unsigned char *code = 0;
unsigned char* BuildCode()
{
unsigned char *pCode = new unsigned char[10];
unsigned char *pMov = pCode;
pMov[0] = 0xC7;
pMov[1] = 0x05;
unsigned char *pAddress = pMov + 2;
*((int *)pAddress) = (int)(&gi);
unsigned char *pImm = pAddress + 4;
*((int *)pImm) = 18;
return pCode;
}
int _tmain(int argc, _TCHAR* argv[])
{
gi = 12;
code = BuildCode();
_asm jmp dword ptr [code]
return 0;
}
我们只是构造了一个mov指令,执行完毕后程序异常退出了。
因为我们执行完mov指令之后,如果不加处理,EIP所指内存的值是不确定的。CPU就会将该不确定的值解释为对应指令,这将导致不可预料的行为,因此,我们再mov指令之后应该放一条指令,让程序回到正常流程。Jmp指令正好可以达到这个目的。
// 1.3.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
using namespace std;
int gi = 0;
unsigned char *code = 0;
unsigned char *pReturnAddress = 0;
unsigned char* BuildCode()
{
unsigned char *pCode = new unsigned char[16];
unsigned char *pMov = pCode;
pMov[0] = 0xC7;
pMov[1] = 0x05;
unsigned char *pAddress = pMov + 2;
*((int *)pAddress) = (int)(&gi);
unsigned char *pImm = pAddress + 4;
*((int *)pImm) = 18;
unsigned char *pJmp = pImm + 4;
pJmp[0] = 0xff;
pJmp[1] = 0x25;
unsigned char *pJmpAddress = pJmp + 2;
*((int *)pJmpAddress) = (int)(&pReturnAddress);
return pCode;
}
int _tmain(int argc, _TCHAR* argv[])
{
gi = 12;
_asm
{
mov pReturnAddress, offset l
}
code = BuildCode();
_asm jmp dword ptr [code]
l:cout << gi << endl;
return 0;
}
mov 指令由三部分构成
两字节的c7 05 代表操作码
四字节的gi地址
四字节的源操作数
jmp指令由6字节构成
2字节的操作码 ff 25
4字节的目的跳转地址
我们通过调试→窗口→寄存器可以看到我们的8个通用寄存器的值
EIP(Extended Instruction Pointer)32位的指令寄存器
是将16位的IP寄存器扩展之后得到的,EIP指向哪里,CPU就将该地址作为执行指令的入口。