| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 8503 人关注过本帖, 2 人收藏
标题:【疑问】使用GetProcAddress 获取到的函数地址是函数真正的入口地址吗?
只看楼主 加入收藏
redice
Rank: 3Rank: 3
等 级:新手上路
威 望:6
帖 子:902
专家分:0
注 册:2006-12-11
结帖率:72.73%
收藏(2)
 问题点数:0 回复次数:11 
【疑问】使用GetProcAddress 获取到的函数地址是函数真正的入口地址吗?
疑问:在动态加载DLL,使用其中导出的函数时,我们常会用GetProcAddress获取DLL中函数的地址。我有个疑问,这里获取到的函数地址
      是函数真正的入口地址吗?如果不是这个地址是什么呢?
      
我做了一个测试,貌似可以证明这个地址不是真正的函数入口地址,那这个地址与真正的函数的入口地址有什么关系呢?

//在下面的实验中,我先获取到MessageBoxA的地址,然后修改这个地址指向内存处的前6个字节为:
//"jmp dword ptr[MessageBox的入口地址]"
//这样下MessageBox调用就陷入死循环中。
//在另一个程序中再次调用MessageBox函数,发现调用是正常的。说明上面的修改仅对当前进程有效。
#include <windows.h>
int main()
{
    HANDLE hDll=NULL;
    void * pAddr=NULL;
    unsigned int uiAddr=0;
    DWORD lpflOldProtect;
    hDll=LoadLibrary("user32.dll");
    pAddr=GetProcAddress(hDll,"MessageBoxA");
    uiAddr=(unsigned int)pAddr;
    if(VirtualProtect(pAddr,100,PAGE_EXECUTE_READWRITE,&lpflOldProtect))
    {
        //将MessageBox入口的6个字节修改为指令:
        //jmp dword ptr[MessageBoxA的入口地址]
        (unsigned char)*((unsigned char*)pAddr)=0xff;
        ((unsigned char*)pAddr)++;
        (unsigned char)*((unsigned char*)pAddr)=0x25;
        ((unsigned char*)pAddr)++;
        (unsigned int)*((unsigned int *)pAddr)=(unsigned int)&uiAddr;
        MessageBox(0,"test","test",MB_OK);
    }
    return 0;
}
搜索更多相关主题的帖子: 入口地址 函数地址 疑问 获取 int 
2008-08-03 18:03
redice
Rank: 3Rank: 3
等 级:新手上路
威 望:6
帖 子:902
专家分:0
注 册:2006-12-11
收藏
得分:0 
补充:
反汇编,看到MessageBox如下:

20:           MessageBox(0,"test","test",MB_OK);
004010BA   mov         esi,esp
004010BC   push        0
004010BE   push        offset string "test" (0042201c)
004010C3   push        offset string "test" (0042201c)
004010C8   push        0
004010CA   call        dword ptr [__imp__MessageBoxA@16 (0042a2b4)]
这里的0x0042a2b4 是什么呢?
使用GetProcAddress  获取到的值是 :0x775d5058a  也是call 最终转向的地址

鲲鹏数据 - 专业Web数据采集服务提供者
http://www.
2008-08-03 18:09
zklhp
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:china
等 级:贵宾
威 望:254
帖 子:11485
专家分:33241
注 册:2007-7-10
收藏
得分:0 
17.2.1  导入表简介

在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。

1. 调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?先将第3章中那个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下:

  invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK

  invoke  ExitProcess,NULL

当使用W32Dasm反汇编以后,这两句代码变成了以下的指令:

:00401000 6A00     push 00000000

:00401002 6800304000     push 00403000

:00401007 680F304000     push 0040300F

:0040100C 6A00   push 00000000

:0040100E E807000000     Call 0040101A ;MessageBox

:00401013 6A00   push 00000000

:00401015 E806000000     Call 00401020 ;ExitProcess

:0040101A FF2508204000   Jmp dword ptr [00402008]

:00401020 FF2500204000   Jmp dword ptr [00402000]

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?使用在17.1.4节中了解的方法来查看一下。

首先,使用17.1.4节的例子文件PEInfo.exe去查看一下Hello.exe文件,会得到以下的信息:

文件名:C:\Documents and Settings\Administrator\桌面\Hello.exe

----------------------------------------------------------

运行平台:   0x014C

节区数量:   3

文件标记:   0x010F

建议装入地址:   0x00400000

----------------------------------------------------------

节区名称  节区大小  虚拟地址  Raw_尺寸  Raw_偏移  节区属性

----------------------------------------------------------

.text 00000026  00001000  00000200  00000400  60000020

.rdata   00000092  00002000  00000200  00000600  40000040

.data 00000022  00003000  00000200  00000800  C0000040

由于建议装入地址是00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址的内容实际上对应PE文件中偏移600h处的数据。

现在随便找一个16进制编辑器来看看文件0600h处的内容是什么:

0600  76 20 00 00 00 00 00 00-5C 20 00 00 00 00 00 00 v ......\ ......

0610  54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00 T ..........j ..

0620  08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00 . ..L ..........

0630  84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00 . ... ..........

0640  00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00 ............v ..

0650  00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65 ....\ ........Me

0660  73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32 ssageBoxA.USER32

0670  2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63 .dll..u.ExitProc

0680  65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C ess.KERNEL32.dll

0690  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。

接下来看看如何去获取导入表的位置,以及导入表中的数据是如何组织以便Windows装载器能够顺利地进行上面的转换工作的。
2008-08-03 18:58
zklhp
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:china
等 级:贵宾
威 望:254
帖 子:11485
专家分:33241
注 册:2007-7-10
收藏
得分:0 
[bo][un]redice[/un] 在 2008-8-3 18:03 的发言:[/bo]

疑问:在动态加载DLL,使用其中导出的函数时,我们常会用GetProcAddress获取DLL中函数的地址。我有个疑问,这里获取到的函数地址
      是函数真正的入口地址吗?如果不是这个地址是什么呢?
      
我做了一个测 ...


偶感觉 这种测试肯定只会影响当前进程  要是改了后 整个系统的MessageBox 全变了 那不天下大乱了

只是对映射到进程的进行了改动 不知道是不是和CopyOnWrite之类的有关

个人意见 偶是大菜鸟 呵呵

[[it] 本帖最后由 zklhp 于 2008-8-3 19:10 编辑 [/it]]
2008-08-03 19:03
zklhp
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:china
等 级:贵宾
威 望:254
帖 子:11485
专家分:33241
注 册:2007-7-10
收藏
得分:0 
17.1.1 可执行文件或DLL的多个实例不能共享静态数据

当为正在运行的应用程序创建新进程时,系统将打开用于标识可执行文件映像的文件映射对象的另一个内存映射视图,并创建一个新进程对象和(为主线程创建)一个新线程对象。系统还要将新的进程I D和线程I D赋予这些对象。通过使用内存映射文件,同一个应用程序的多个正在运行的实例就能够共享R A M中的相同代码和数据。

这里有一个小问题需要注意。进程使用的是一个平面地址空间。当编译和链接你的程序时,所有的代码和数据都被合并在一起,组成一个很大的结构。数据与代码被分开,但仅限于跟在. e x e文件中的代码后面的数据而已。(实际上,文件的内容被分割为不同的节。代码放在一个节中,全局变量放在另一个节中。各个节按照页面边界来对齐。通过调用Get SystemInfo 函数,应用程序可以确定正在使用的页面的大小。在. e x e或D L L文件中,代码节通常位于数据数据节的前面。)图1 7 - 1简单说明了应用程序的代码和数据究竟是如何加载到虚拟内存中,然后又被映射到应用程序的地址空间中的。

作为一个例子,假设应用程序的第二个实例正在运行。系统只是将包含文件的代码和数据的虚拟内存页面映射到第二个应用程序的地址空间,如图1 7 - 2所示。

如果应用程序的一个实例改变了驻留在数据页面中的某些全局变量,那么该应用程序的所有实例的内存内容都将改变。这种类型的改变可能带来灾难性的后果,因此是决不允许的。

系统运用内存管理系统的c o p y - o n - w r i t e(写入时拷贝)特性来防止进行这种改变。每当应用程序尝试将数据写入它的内存映射文件时,系统就会抓住这种尝试,为包含应用程序尝试写入数据的内存页面分配一个新内存块,再拷贝该页面的内容,并允许该应用程序将数据写入这个新分配的内存块。结果,同一个应用程序的所有其他实例的运行都不会受到影响。图1 7 - 3显示了当应用程序的第一个实例尝试改变数据页面2时出现的情况。


 
图17-1 应用程序的代码和数据加载及映射示意图


 
图17-2 应用程序与虚拟内存地址空间之间的关系示意图


 
图17-3 应用程序的第一个实例尝试改变数据页面2时的情况

系统分配一个新的虚拟内存页面,并且将数据页面2的内容拷贝到新页面中。第一个实例的地址空间发生了变更,这样,新数据页面就被映射到与原始地址页面相同位置上的地址空间中。这时系统就可以让进程修改全局变量,而不必担心改变同一个应用程序的另一个实例的数据。

当应用程序被调试时,将会发生类似的事件。比如说,你正在运行一个应用程序的多个实例,并且只想调试其中的一个实例。你访问调试程序,在一行源代码中设置一个断点。调试程序修改了你的代码,将你的一个汇编语言指令改为能使调试程序自行激活的指令。因此你再次遇到了同样的问题。当调试程序修改代码时,它将导致应用程序的所有实例在修改后的汇编语言指令运行时激活该调试程序。为了解决这个问题,系统再次使用c o p y - o n - w r i t e内存。当系统发现调试程序试图修改代码时,它就分配一个新内存块,将包含该指令的页面拷贝到新的内存页面中,并且允许调试程序修改页面拷贝中的代码。

Windows 98 当一个进程被加载时,系统要查看文件映像的所有页面。系统立即为通常用c o p y - o n - w r i t e属性保护的那些页面提交页文件中的存储器。这些页面只是被提交而已,它们并不被访问。当文件映像中的页面被访问时,系统就加载相应的页面。如果该页面从来没有被修改,它就可以从内存中删除,并在必要时重新加载。但是,如果文件的页面被修改了,系统就将修改过的页面转到页文件中以前被提交的页面之一。

Windows 2000与Windows 98之间的行为特性的唯一差别,是在你加载一个模块的两个拷贝并且可写入的数据尚未被修改的时候显示出来的。在这种情况下,在Windows 2000下运行的进程能够共享数据,而在Windows 98下,每个进程都可以得到它自己的数据拷贝。如果只加载模块的一个拷贝,或者可写入的数据已经被修改(这是通常的情况),那么Windows 2000与Windows 98的行为特性是完全相同的。
2008-08-03 19:13
redice
Rank: 3Rank: 3
等 级:新手上路
威 望:6
帖 子:902
专家分:0
注 册:2006-12-11
收藏
得分:0 
[bo][un]zklhp[/un] 在 2008-8-3 19:03 的发言:[/bo]



偶感觉 这种测试肯定只会影响当前进程  要是改了后 整个系统的MessageBox 全变了 那不天下大乱了

只是对映射到进程的进行了改动 不知道是不是和CopyOnWrite之类的有关

个人意见 偶是大菜鸟 呵呵



感谢 你的关注  你觉得 GetProcAddress获取到的函数地址是不是动态链接库内存映射中函数的入口地址呢?

我是想看看能不能通过修改这个地址指向的函数劫持这个 API, 类似于在核心态 (Ring 0)的挂钩 SSDT 表实现主动防御。

[[it] 本帖最后由 redice 于 2008-8-3 21:07 编辑 [/it]]

鲲鹏数据 - 专业Web数据采集服务提供者
http://www.
2008-08-03 20:55
redice
Rank: 3Rank: 3
等 级:新手上路
威 望:6
帖 子:902
专家分:0
注 册:2006-12-11
收藏
得分:0 
hDll=LoadLibrary("user32.dll");
 pAddr=GetProcAddress(hDll,"MessageBoxA");
 通俗一点说,我就是想问pAddr是不是 user32.dll映射到内存后MessageBoxA的入口地址?

鲲鹏数据 - 专业Web数据采集服务提供者
http://www.
2008-08-03 21:03
你们都要疼我哦
Rank: 11Rank: 11Rank: 11Rank: 11
来 自:火星
等 级:贵宾
威 望:49
帖 子:1296
专家分:2746
注 册:2008-7-13
收藏
得分:0 
都搞的这么复杂干嘛捏.

一般系统的DLL文件都预先被装载到内存固定位置,根据系统的不同,每次装载的地址有可能不同,但是一般情况下,每次都是固定的.

程序的代码经过编译构建变成PE文件,里面有个导入表,用来导入程序里使用到的函数. 关于导入表,结构稍微有些多,不过程序在装载进内存后, 操作系统自动根据你的导入表,帮你定位了DLL中的API实际地址, 就是系统帮你填充了导入表中的IAT部分.IAT结构比较简单,就是你用的函数和函数对应的实际内存地址. 2楼所得到的0x0042a2b4地址,即是这个函数在导入表的IAT结构中的函数名,而0x775d5058a这个地址,就是系统自动免费帮你填充的该函数在内存中的实际地址.

还个情况,就是如果你是动态装入的DLL,这个装入位置是不固定的,你在使用其中的函数时,要用GetProcAddress指明DLL句柄和API名,返回的是该API的实际地址.  
win32中是用的平坦内存模式,一个进程和它所需要的DLL,是个整体,DLL属于调用它的进程,不会影响到其他同样调用该DLL的进程的.DLL物理上只存在一份, 采用映射加部分拷贝代码的形式提供给每个进程使用.互不影响.
2008-08-03 21:11
redice
Rank: 3Rank: 3
等 级:新手上路
威 望:6
帖 子:902
专家分:0
注 册:2006-12-11
收藏
得分:0 
回复 8# 你们都要疼我哦 的帖子
谢谢  

现在我知道了 就是使用 GetProcAddress 得到的函数地址是函数的实际地址

为什么修改了这个地址下对应的代码对其它程序没有影响呢?

“win32中是用的平坦内存模式,一个进程和它所需要的DLL是个整体,DLL属于调用它的进程,不会影响到其他同样调用该DLL的进程的.DLL物理上只存在一份, 采用映射加部分拷贝代码的形式提供给每个进程使用.互不影响.”

“映射加部分拷贝代码”,哪部分代码系统会采用直接映射,哪部分要用拷贝代码呢?

鲲鹏数据 - 专业Web数据采集服务提供者
http://www.
2008-08-03 21:27
你们都要疼我哦
Rank: 11Rank: 11Rank: 11Rank: 11
来 自:火星
等 级:贵宾
威 望:49
帖 子:1296
专家分:2746
注 册:2008-7-13
收藏
得分:0 
短消息.


因为DLL属于调用它的进程,不会影响到其他同样调用该DLL的进程的.

所以修改了这个地址下对应的代码对其它程序没有影响.

自己修改自己的东西,对其他程序不会有影响.

他修改的只是自己的一份映射,没影响到实际的物理内存中的DLL吧. 系统应该不会那么轻易的就允许应用程序更改物理内存中的那份系统DLL的.     我也是大菜鸟,个人意见
2008-08-03 22:22
快速回复:【疑问】使用GetProcAddress 获取到的函数地址是函数真正的入口地址吗 ...
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.012897 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved