8句代码实现win10下Delphi 10.3 inline hook之MessageBoxA
本帖最后由 bester 于 2019-7-8 11:33 编辑首先感谢Delphi学习交流群的myy,昨天调试了6个小时,终于完成了,记录一下,以免有爱好者走弯路,某度的资料我等菜鸟看不懂,思来想去还是自己挖坑自己填。
win10下注意两个问题:
1.OpenProcess无法获取进程句柄,一般此问题在Dll中调用此函数出现,用GetLastError返回5,意思是拒绝访问,DLL暂时没试验以下三种办法
解决方法:
1.EXE用管理员运行再用OD附加
2.用提权函数提升至DEBUG权限
3.DELPHI自身IDE运行EXE不会出现此问题
2.WriteProcessMemory的第5个参数,IpNumberOfByteWritten(表示:实际写入字节),他的类型不是Cardinal类型,是NativeUInt类型,这里参考写错了,如果写Cardinal类型会提示实参和形参不一致。也可能是NativeUInt类型是Cardinal类型的分支,这里不做探究,反正Delphi的参考进去了winapi.windows看了源码以后,他的参考又不一样了,坑。
代码如下,会逐句解释
function lens(x,y:Integer):Integer; //声明一个函数lens,lens函数是计算JMP距离的,也就是从api函数跳转到新函数的距离
begin
Result:=x-y-5; // X=新函数地址,Y=api函数地址,由于Jmp(16进制E9占1个字节)+距离(占4个字节),实际上跳转是从api地址+5开始起跳,如果不减5,实际上会跳到新函数的地址+5
end; //例:如果新函数地址=00000006,api地址=00000000,那么起跳位置实际是00000006-00000000-5,如果不减5,将会跳转到00000006+5的位置
function MyMessages(hWnd: Cardinal; lpText, lpCaption: PAnsiChar; uType: Cardinal):Integer; stdcall; //声明一个和MessageBoxA函数一样类型的函数,命名为MyMessages,注意一定要用stdcall约定,否则会调用出错
var
text: PAnsiChar; //定义一个变量为text,类型和形式参数的类型一样为PAnsiChar,便于操作形式参数
begin //注意声明一个函数,不论函数是否有代码,都应该用begin end括起来,否则IDE无限提示出错
text := lpText; //令ipText的值=text,这样的话,我就能获取到MessageBoxA的参数的内容了
ShowMessage('我获取到的信息框标题是: ' + text); //此时调用ShowMessage,调试输出MessageBoxA的IpText参数的值
end;
procedure TForm1.Button2Click(Sender: TObject); //这里是一个按钮的按钮事件,我添加了两个按钮,一个是调用原始信息框的按钮,一个是Hook按钮,这里是Hook按钮
var
hp: Integer; //定义一个变量为HP,用来保存进程句柄
apiaddr: Integer; //定义一个变量为apiaddr,用来保存获取到的api地址,获取到的api的地址为10进制,但我们无需转成OD的16进制,为了方便查看可以手动调用calc计算器进行转换或者ctrl+F5添加监视
read: array of Byte; //定义一个元素序号为1-5的数组,数组名为read,类型为byte类型,表示保存5个字节,用来保存api函数的5个字节,因为我们需要修改这5个字节跳转到新函数,到时候可便于恢复Hook
rd: NativeUInt; //定义一个变量为rd,类型为NativeUInt,这个变量是函数保存实际操作了几个字节,比如我读取或者写入了5个字节,如果操作成功,这里的值就是5,否则就是0,上面有作说明,类型必须为NativeUInt,
jmps: Integer; //定义一个变量为Jmps,这里用来保存Jmp的机器码(E9)的十进制,因为我们操作的是10进制,上面说了无需转换成OD的16进制
tiaoshi,tiaoshi1: Integer; //这里用来调试输出内容,方便我查找问题,可以不要。
begin
hp := OpenProcess(PROCESS_ALL_ACCESS, False, GetCurrentProcessId); //OpenProcess函数,如果操作成功,那么hp变量将保存进程句柄,第三个参数用GetCurrentProcessId,表示获取自身进程的进程ID,如果是操作别的进程,这里要填需要操作的进程的进程ID,但注意要用DLL形式,EXE无法操作
apiaddr := Integer(GetProcAddress(LoadLibrary('user32.dll'), 'MessageBoxA')); //这里用LoadLibrary获取user32.dll的模块地址,也可以说是模块句柄,再用GetProcessAddress获取MessageBoxA的api地址,GetProcessAddress返回的是Pointer类型,所以用Integer转成整数型,也就是10进制
ReadProcessMemory(hp, Pointer(apiaddr), Pointer(@read), 5, rd); //用ReadProcessMemory读取api函数的头5个字节,保存在read数组当中,ReadProcessMemory(进程句柄:Cradinal类型,读取地址:pointer类型,保存读取的数据:pointer类型(加@表示保存的数据的地址,也就是所谓的传址),读取数据的大小:Cradinal类型,实际大小:NativeUInt类型)
jmps := 233; //JMP的机器码(E9)的十进制
WriteProcessMemory(hp, Pointer(apiaddr), Pointer(@jmps), 1, rd); //将E9写入到首地址的第一个字节
tiaoshi:=Integer(@MyMessages) ; //这里我调试输出查看一下是否获取到新函数的地址,加@表示取新函数的实际地址,跟上面的readprocessmemory的第三个参数是一样的意思。
tiaoshi1 := lens(Integer(@MyMessages), apiaddr); //这里我调试输出查看距离(占用4个字节),也就是JMP后面的4个字节的机器码,实际上转换成16进制是这样的形式,假设调试输出的值是01020304,那么在OD中查看需要倒转一下,也就是04030201,这样的形式
WriteProcessMemory(hp, Pointer(apiaddr + 1), Pointer(@tiaoshi1), 4, rd); //这里将距离写入,因为获取到的api的地址是第一个字节我们写入了E9,所以要从第二个字节开始写入4个字节的大小,所以apiaddr+1
CloseHandle(hp) ; //这里关闭我们打开的进程句柄,虽然我也不知道是为什么,但是好像就是要关闭。
end;
procedure TForm1.Button1Click(Sender: TObject); //这里就是我说的第一个按钮,调用原始的MessageBoxA
begin
MessageBoxA(0, '我是标题', '我是内容', 0)
end;
下期讲注入器部分以及怎么把上面的代码在DLL中使用,这里用EXE是为了方便我调试,有错误欢迎指正,欢迎交流Delphi,我的博客园里面会写一些文章,因为菜所以菜。 本帖最后由 朱朱你堕落了 于 2019-7-8 18:42 编辑
bester 发表于 2019-7-8 12:21
@朱朱你堕落了 看了一下你说的这个,但是我还是不明白,说一下我的理解,其实不管是5字节还是7字节,都改变 ...
不好意思,哥们,有事没及时回。
一般的5字节的inline hook都是修改API的函数头,我们知道一般的API的函数头就是mov edi, mdi。
那么我写的跳到我自己DLL里面的自定义函数时,如下图
很明显,那么原始的API函数是如下的
很明显,如果API执行是从push ebp执行,会不会影响呢,当然是不影响的,这就是热补丁调用API的方法。
因为mov edi,edi,EDI自身MOV给自身,无任意意义的。
那么问题MS会这么设计呢,因为就是为了给程序打热补丁。
热补丁实现如下图
优点就是稳定,在多线程中也稳定。为什么稳定。这样说吧。
当API函数时被触发时,那么调用的是API的函数头,API的函数头这里是一个71A24A07jmp 71A24A02
那么这个跳就跳到了上面的那个,原来5个NOP处,5个NOP已经被修改为了跳到我自定义的函数。
原来MOV EID,EID是两个字节,上面的NOP是五个字节,一共是7个字节,两个连跳就构成了7字节的热补丁HOOK
那么你一定很好奇,那怎么调用API呢,答案就是调用API函数+2的地方,这样从push ebp开始走,是不影响API的调用的。
而一般的5字节的inline hook,你可以看到,它从mov edi,edi就开始占用5个字节,push ebp等代码被覆盖,如果要调用API时,一定得先脱钩。
即先得恢复过来,才能调用API,要不,不崩溃才怪呢,脱钩后,调用API函数后,还要再重新挂钩,不重新挂钩,当下次有API调用时,还怎么
玩?
我们看到inline hook的方式就是这样
myfun()
{
//1
脱钩
//2
调用原始API
//3
再重新挂钩
}
这种很频繁的脱钩和挂钩很影响系统性能,自然也影响程序稳定性。
而7字节热补丁方式就不存在这个问题,因为一旦挂上,那么API头部就是两个连跳。。我们在自己的自定义函数中,就不需要脱钩,挂钩,
调用API时,使用的是函数头+2调用的方式。函数头+2就是push ebp这里开始,这样就根本不影响调用原API的功能。
哥们理解一下。 bester 发表于 2019-7-8 18:50
懂意思了,但是我这边有几个问题:
1.热补丁脱钩要是直接把EBF9还原为mov edi,edi吗?
2.如果我挂钩了 ...
1 脱钩当然是恢复到最原始的状态。即还原mov edi,edi,还有上面的5个NOP
2. 我不知道delphi中是如何写的,C++中是用的函数指针的方式。
举个例子
typedef int (WINAPI* PFNconnect)(SOCKET s, const struct sockaddr* name, int namelen);
int WINAPI My_connect(SOCKET s, const struct sockaddr* name, int namelen)
{
//......其他一些代码
//就是下面这样调用API的。
int bRet;
FARPROC pFunc;
pFunc = GetProcAddress(GetModuleHandle("ws2_32.dll"), "connect");//获取函数地址,这时获取到的是jmp处地址即原来mov edi,edi这个地方
pFunc = (FARPROC)((DWORD)pFunc+2); //地址+2
bRet = ((PFNconnect)pFunc)(s, name, namelen); //函数指针方式调用
return bRet;
} @朱朱你堕落了 看了一下你说的这个,但是我还是不明白,说一下我的理解,其实不管是5字节还是7字节,都改变了函数,7字节热补丁说是多线程下inline hook的解决办法,但是我想不明白就是
假设有两个线程同时访问MessageBoxA,5字节和7字节其实都是改变了原来的API的位置,所以这两个线程同时会执行到我自己定义的函数,如果我只想要其中一个线程执行到我的函数,好像也不可能实现吧?
我不明白的就是这句话:即使Hook失败,也不影响函数的继续执行
怎么会hook失败呢?如果hook住了,那么这两个线程同时会访问我的函数,如果没hook成功,那么相当于我这个writeprocessmemory也写入失败,如果是像文章所说,构造死循环的话,那么这两个线程也同时会暂停在这个死循环上面啊,我也不可能让这两个线程排好队一个一个访问我的函数 朱朱你堕落了 发表于 2019-7-8 18:13
不好意思,哥们,有事没及时回。
一般的5字节的inline hook都是修改API的函数头,我们知道一般的API的函 ...
懂意思了,但是我这边有几个问题:
1.热补丁脱钩要是直接把EBF9还原为mov edi,edi吗?
2.如果我挂钩了,想调用API函数的话,怎么能从API地址+2处开始调用? 朱朱你堕落了 发表于 2019-7-8 18:13
不好意思,哥们,有事没及时回。
一般的5字节的inline hook都是修改API的函数头,我们知道一般的API的函 ...
5字节也是可以的,只不过调用原函数需要自己把hook修改的代码补上就可以了,这样就不需要频繁的恢复了。当然有热补丁位置的话还是热补丁要方便一点 朱朱你堕落了 发表于 2019-7-8 19:11
1 脱钩当然是恢复到最原始的状态。即还原mov edi,edi,还有上面的5个NOP
2. 我不知道delphi中是如何写的 ...
我试一下 看看你的这个方式能不能使用,我能不能这样理解,就是7字节比5字节稳定的好处就在于,可以不用恢复钩子,或者说只需恢复2个字节,就能调用原函数?因为除了这点,我还是没能理解到7字节比5字节更好的地方,都涉及到了内存操作,并不是7字节就不用内存操作,只是操作的字节数的多少的问题 苏紫方璇 发表于 2019-7-8 18:51
5字节也是可以的,只不过调用原函数需要自己把hook修改的代码补上就可以了,这样就不需要频繁的恢复了。 ...
你说的那个把HOOK修改的代码补上来,我突然想到,好像见过代码用的是内联汇编实现为补的。如果我没有记错的话、 bester 发表于 2019-7-8 19:32
我试一下 看看你的这个方式能不能使用,我能不能这样理解,就是7字节比5字节稳定的好处就在于,可以不用 ...
哥们,你先把inline hook彻底玩透了,加深了理解,彻底明白了,再了解热补丁,就知道了好处了。5字节HOOK是最常用的,热补丁有一个局限性就是不是所有的API函数的开头都是movedi,eid,上面还有5个NOP的,但是5字节这种可以通杀所有的API,普通函数也可以HOOK,也是最常用的。慢慢来。 苏紫方璇 发表于 2019-7-8 18:51
5字节也是可以的,只不过调用原函数需要自己把hook修改的代码补上就可以了,这样就不需要频繁的恢复了。 ...
咦,这怎么补?不恢复难道在新函数下面先asm把hook的那5个字节补上去?可是这样确定不会出错吗?
页:
[1]
2