首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
88 阅读
2
nvim番外之将配置的插件管理器更新为lazy
73 阅读
3
2018总结与2019规划
55 阅读
4
PDF标准详解(五)——图形状态
37 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
311
篇文章
累计收到
27
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
页面
归档
友情链接
关于
搜索到
190
篇与
的结果
2017-06-01
使用MSHTML解析HTML页面
最近在写一个爬虫项目,本来打算用C/C++来实现,在网上查找有关资料的时候发现了微软的这个MSHTML库,最后发现在解析动态页面的时候它的表现实在是太差:在项目中需要像浏览器那样,执行JavaScript等脚本然后形成静态的HTML页面,最后才分析这个静态页面。但是MSHTML在执行JavaScript等脚本时需要配合WebBroswer这个ActiveX控件,这个控件又必须在GUI程序中使用,但是我做的这个功能最终是嵌入到公司产品中发布,不可能为它专门生成一个GUI页面,所以这个方案就作废了。虽然最终没有采用这个方案,但是我在开始学习MSHTML并写Demo的过程中还是收益匪浅,所以在这记录下我的成果解析Html页面MSHTML是一个典型的DOM类型的解析库,它基于COM组件,在解析Html页面时需要一个IHTMLDocument2类型的接口。在GUI程序中很容易就获取这个接口,获取它的方法很容易就可以在网上找到,在这主要说一下如何通过一段HTML字符串来生成对应的IHTMLDocument2接口。至于如何生成这个HTML字符串,我们可以通过向web服务器发送http请求,并获取它的返回,解析这个返回的数据包即可获取到对应的HTML页面数据。获取这个接口主要需要经过下面的几个步骤:使用CoCreateInstance创建一个接口,对于IHTMLDocument2接口一般是使用下面的语句:HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc);2.创建一个COM中的数组,将HTML字符串写到数组中。这个数组主要用来进行VC与VB的交互,以便VB程序能够很方便的使用COM接口。在使用这个数组时不需要关注它的具体成员,VC提供了具体的接口来使用它,在初始化它的时候只需要调用下面几个:a)SafeArrayCreateVector:这个函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这我们只是简单的将它作为一个字符数组,所以它是一个一维数组。b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,我们操作这个缓冲区就相当于操作了这个数组。c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,这个函数用来回收这个权限,并使我们对数组的操作生效调用接口的write方法,将接口与HTML字符串绑定经过这样几步就可以利用这个接口来访问HTML中的元素了,下面是它的详细代码:IHTMLDocument2* CreateIHTMLDocument2(const string &strHtml) { IHTMLDocument2 *m_spDoc = NULL; HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&m_spDoc); HRESULT hresult = S_OK; VARIANT *param; SAFEARRAY *sfArray; // Creates a new one-dimensional array sfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1); if (sfArray == NULL || m_spDoc == NULL) { return; } hresult = SafeArrayAccessData(sfArray,(LPVOID*) ¶m); param->vt = VT_BSTR; param->bstrVal = _com_util::ConvertStringToBSTR(strHtml.c_str()); hresult = SafeArrayUnaccessData(sfArray); hresult = m_spDoc->write(sfArray); return m_spDoc; }HTML元素的遍历MSHTML中,将元素的对应信息封装为IHTMLElement接口,得到对应元素的接口后可以使用它里面的get系列方法来获取它里面的各种信息,这些函数我没有一一列举,当需要时看看MSDN即可。当获取到了HTML文档的IID_IHTMLDocument2接口时,可以使用下面的步骤进行元素的遍历:接口的get_all方法获取所有的标签节点。这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针然后通过IHTMLElementCollection接口的get_length方法获取标签的总数量,根据这个数量写一个循环,在循环进行元素的遍历在循环中使用IHTMLElementCollection接口的item方法进行迭代,依次获取各个元素对应的IDispatch接口指针调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各中信息。它对应的代码如下:void EnumElements(IHTMLDocument2* m_spDoc) { CComPtr<IHTMLElementCollection> pCollec; m_spDoc->get_all(&pCollec); if (NULL == pCollec) { return ; } VARIANT varName; long len = 0; pCollec->get_length(&len); for (int i = 0; i < len; i++) { varName.vt = VT_I4; varName.llVal = i; CComPtr<IHTMLElement> pElement; CComPtr<IDispatch> pDisp; pCollec->item(varName, varName, &pDisp); if (NULL == pDisp) { continue; } pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement); if (NULL != pElement) { BSTR bstrTag; pElement->get_tagName(&bstrTag); string strTag = _com_util::ConvertBSTRToString(bstrTag); cout<<strTag.c_str()<<endl; } } }这个方法不能很好的体现各个元素的层次结构,它可以遍历所有的元素,但是默认将元素都作为同一层来表示,如果需要得到对应的子节点,可以调用get_children方法,它可以获取下面的所有子节点,使用方法与get_all类似调用JavaScript方法在这,调用JavaScript函数只能想调用普通的函数一样,根据函数名,给它参数,并获取返回值,但是不能得到它执行到中间的某个步骤,比如说这样一个函数function add(a, b){ window.location.href = "https://www.baidu.com"; return a + b }调用这个函数,只能得到a + b的值,但是并不知道它会跳转到另一个页面,在编写爬虫时如果存在这样的跳转或者通过某条语句生成了一个链接,那么使用后面说的方法是获取不到的言归正传,下面来说下如何实现调用JavaScript。调用JavaScript方法一般是使用IDispatch接口中的Invoke方法,但是使用这个略显麻烦,我在网上找到了更简单的方法,就是使用CComDispatchDriver接口中的Invoke方法,这个接口中主要有Invoke0、Invoke1、Invoke2、InvokeN几个用于调用JavaScript函数的方法,分别表示传入0个参数、1个参数、2个参数、任意个参数。一般使用如下步骤来调用:1.调用IID_IHTMLDocument2的get_Script方法,获取CComDispatchDriver接口调用CComDispatchDriver接口的GetIDOfName,传入JavaScript函数名称,获取JS函数对应的元素接口,这个函数会通过一个输出参数输出一个DISPID类型的变量。这个主要是一个ID,用来唯一标识一个js函数调用CComDispatchDriver接口的invoke函数,传入对应的参数,并调用js函数。下面是一个例子代码:bool CallJScript(IID_IHTMLDocument2* m_spDoc, const CString strFunc, CComVariant* paramArray,int nArgCnt,CComVariant* pVarResult) { CComDispatchDriver spScript; GetJScript(spScript); if (NULL == spScript) { return false; } DISPID pispid; BSTR bstrText = _com_util::ConvertStringToBSTR(strFunc); spScript.GetIDOfName(bstrText, &pispid); HRESULT hr = spScript.InvokeN(pispid, paramArray, nArgCnt, pVarResult); if(FAILED(hr)) { ShowError(GetSystemErrorMessage(hr)); return false; } return true; }在调用的时候需要组织一个CComVariant类型的数组,并提供一个数组元素个数作为参数。而对于Invoke0这样有确定函数参数的情况则要简单的多。获取js函数返回值js返回参数最终会被包装成一个VARIANT结构,在COM中为了方便操作这个结构,封装了一个CComVariant类。在操作返回值时就是围绕着CComVariant类来进行返回确定值当它返回一个确定值时很好解决,由于事先知道返回值得类型,只需要调用结构体的不同成员即可CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); cout<<varResult.lVal<<endl;当它返回一个数组时,一般需要经过这样几步的处理:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入"length"字符串,让其返回数组元素的个数在循环中调用GetPropertyByName方法,传入索引,获取对应索引位置的CComVariant值。CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varArrayLen; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"length", &varArrayLen); for (int i = 0; i < varArrayLen.intVal; i++) { CComVariant varValue; CStringW csIndex; csIndex.Format(L"%d", i); spDisp.GetPropertyByName(csIndex, &varValue); cout<<varValue.intVal<<endl; }返回一个object对象js的object对象中可以有不同的属性,不同的属性对应不同的值,类似于一个字典结构,当返回这个类型,并且我们知道这个对象中的相关属性名称的时候可以通过下面的方法来获取各个属性中的值:创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入对应属性名称的字符串,让其返回属性的值//在这假设JavaScript方法返回一个object对象,其中有两个属性,str属性中保存字符串,value属性保存一个整型数据 CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComVariant varValue; CComDispatchDriver spDisp = varResult.pdispVal; spDisp.GetPropertyByName(L"result", &varValue); cout<<"result:"<<varValue.intVal<<endl; spDisp.GetPropertyByName(L"str", &varValue); string strValue = _com_util::ConvertBSTRToString(varValue.bstrVal); cout<<"str:"<<strValue.c_str()<<endl;返回类型不确定的object对象上面这种情况只有当JavaScript代码由自己编写或者与他人进行过相关的约定的时候才可能非常清楚js函数中将会返回何种类型的值,但是大多数情况下,是不知道将会返回何种数据,比如像我们在编写爬虫的时候。这种情况下一般使用IDispatchEx接口来枚举返回对象中的属性名称然后再根据上面的方法来获取属性的值CComVariant varResult; parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult); CComQIPtr<IDispatchEx> pDispEx = varResult.pdispVal; CComDispatchDriver spDisp = varResult.pdispVal; DISPID dispid; HRESULT hr = pDispEx->GetNextDispID(fdexEnumAll, DISPID_STARTENUM, &dispid); //枚举返回对象中所有属性对应的值 while (hr == NOERROR) { BSTR bstrName; pDispEx->GetMemberName(dispid, &bstrName); if (NULL != bstrName) { DISPPARAMS params; CComVariant varVaule; cout<<_com_util::ConvertBSTRToString(bstrName)<<endl; spDisp.GetPropertyByName(bstrName, &varVaule); SysFreeString(bstrName); } hr = pDispEx->GetNextDispID(fdexEnumAll, dispid, &dispid); }这些差不多就是我当初学会的一些东西,当初在利用这个方案实现爬虫的时候还是有许多坑,也看到了它的许多局限性,以至于我最终放弃了它,采用其他的解决方案。目前在使用的时候的我发现这样几个问题:在调用js时,如果不知道函数的名称,目前为止没有方法可以调用,这样就需要我们在HTML中使用正则表达式等方法进行提取,但是在HTML中调用js的方法实在太多,而有的只有一个函数,并没有调用,这些情况给工作带来了很大的挑战MSHTML提供的功能主要是用来与IE进行交互,以便很容易实现一个类似于IE的浏览器或者与IE进行交互,但是如果要在控制台下进行相关功能的编写,则显的力不从心在控制台下它没有提供一个很好的方式来进行HTML页面的渲染。在于js进行交互的时候,只能简单的获取到一个VARIANT结构,这个结构可以表示所有常见的类型,但是在很多情况下,我们并不知道它具体代表哪个类型最后放上demo的下载地址:http://download.csdn.net/detail/lanuage/9857075
2017年06月01日
7 阅读
0 评论
0 点赞
2017-05-21
Windows资源
Windows资源是一种二进制数据,由链接器链接进程序成为程序的一部分,通过资源的方式可以很方便的对应用程序进行扩展。在Windows中资源可以是系统自定义的,也可以是用户自定义的。在VC++中资源是以被称为资源脚本的文本文件描述的(扩展名为rc),另外为了方便代码中调用资源,VC++环境中还会自动生成一个resource.h的头文件供C++代码使用,这个文件中主要定义了各个资源的ID,在vc++中使用ID来唯一标识一个资源,这个ID可以是数字也可以是字符串,其实在VC中真正用来标识资源的是字符串,通过宏MAKEINTRESOURCE可以将数字型的ID转化为对应的字符串,一般的资源函数在操作资源时都需要提供一个资源的字符串,而这个串就是利用这个宏传入ID生成的。在VC中资源脚本的基本格式为:资源名(ID串) 类型名 [语言] 资源数据资源数据可以是一段指定格式的文本或者一个文件,比如我们将wav作为资源加入到程序中,可以这样写:MY_WAVE_RES IDR_WAVE sample.wav.其中语言如果没有指定,那么默认为操作系统当前的语言环境。另外我们也可以将不同的资源放入不同的文本文件中,先定义好,然后在.rc文件中使用#include 来包含进来,比如在一个名为wav.resinclude文件中定义了一个WAV资源,然后可以在.rc文件中加上一句"#include <wav.resinclude> ”下面介绍下资源的操作中比较高级的技术引用自定义资源对于系统自定义资源,系统都提供了专门的函数来进行加载和操作,但是对于自定义资源,在操作时相对比较复杂,一般先使用FindResource和FindResourceEx在进程中找到对应的资源句柄,然后使用LoadResource将资源加载到内存中,以后就可以使用这个资源了。下面的一个例子演示了如何在当前exe中如何将另一个EXE作为资源加载,并执行它。__inline VOID GetAppPath(LPTSTR pszBuf) { DWORD dwLen = 0; if(0 == (dwLen = ::GetModuleFileName(NULL,pszBuf,MAX_PATH))) { printf("获取APP路径失败,错误码0x%08x\n",GetLastError()); return; } DWORD i = dwLen; for(; i > 0; i -- ) { if( '\\' == pszBuf[i] ) { pszBuf[i + 1] = '\0'; break; } } } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hModule = GetModuleHandle(NULL); HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA); if (INVALID_HANDLE_VALUE == hRsrc) { printf("加载自定义资源失败!\n"); return 0; } HGLOBAL hGlobalRes = LoadResource(hModule, hRsrc); LPVOID pResMem = LockResource(hGlobalRes); DWORD dwSize = SizeofResource(hModule, hRsrc); if (NULL == pResMem) { printf("获取资源所在内存失败!\n"); return 0; } TCHAR szFilePath[MAX_PATH] = _T(""); GetAppPath(szFilePath); StringCchCat(szFilePath, MAX_PATH, _T("test.exe")); HANDLE hFile = CreateFile(szFilePath, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, 0, NULL); if(!WriteFile(hFile, pResMem, dwSize, &dwSize, NULL)) { printf("写文件失败\n"); return 0; } CloseHandle(hFile); STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; CreateProcess(szFilePath, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; }为了执行上面的代码需要在该项目工程中新加一个资源,将目标EXE添加到资源中,其中资源文件会多出一行"IDR_RCDATA1 RCDATA "E:\Program\ResourcesDemo\Debug\stdWindow.exe" 在resource.h文件中生成了一个资源对应的ID,做好这些工作,该程序就能正常运行在上面的代码中,依次调用FindResource、 LoadResource、LockResource,获取资源在进程空间中的地址,并将它对应的物理页面锁定在内存中,不允许其进行内存交换。然后将这个部分的内存写入到文件形成一个新的exe,最后执行这个exe,最终上面的程序编译运行后我们会发现在程序对应的目录下会生成一个test.exe文件。更新资源在有的时候需要对程序中的资源进行更新,这种情况下一般是在原始的工程下 更改资源,然后重新编译,但是这个时候用户需要下载新的更新程序,在原始程序比较大的情况下,为了更改一个简单的资源就要重新花大量的时间下载并更新程序,可能有点杀鸡用牛刀的意思,在我们只需要更新程序中的资源的情况下,Windows提供了一种方法。首先使用BeginUpdateResource建立可执行程序文件模块的更新句柄使用UpdateResource传入之前的更新句柄,更新资源数据使用EndUpdateResource函数关闭修改句柄,如果想让整个更改存盘需要将函数的第二个参数传入FALSE,这个参数的意思是是否放弃更新,传入false表示保存更新下面是一个简单的例子 HMODULE hModule = GetModuleHandle(NULL); //加载资源 HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDI_ICON1), RT_GROUP_ICON); if (hRsrc == NULL) { printf("加载资源失败\n"); return GetLastError(); } HGLOBAL hIcon = LoadResource(hModule, hRsrc); PVOID pIconBuf = LockResource(hIcon); int nIconSize = SizeofResource(hModule, hRsrc); //更新资源 HANDLE hUpdate = BeginUpdateResource(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe"), TRUE); BOOL bRet = UpdateResource(hUpdate, MAKEINTRESOURCE(RT_GROUP_ICON), MAKEINTRESOURCE(IDI_STDWINDOW), GetUserDefaultLangID(), pIconBuf, nIconSize); bRet = EndUpdateResource(hUpdate, FALSE); return 0;枚举资源枚举资源主要使用函数EnumResourceTypes EnumResourceNames, 和EnumResourceLanguages,这几个函数分别枚举资源类型,名称和语言,在msdn中查找函数的定义发现他们的调用顺序必须是type name language,下面是一个简单的枚举的例子:BOOL CALLBACK EnumResLangProc(HANDLE hModule, LPCTSTR lpszType, LPCTSTR lpszName, WORD wIDLanguage, LONG_PTR lParam) { printf("\tlanguage :%d\n", wIDLanguage); return TRUE; } BOOL CALLBACK EnumRe1sNameProc(HMODULE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG_PTR lParam) { if ((ULONG)lpszName & 0xffff0000) { printf("\t名称:%s\n", lpszName); }else { printf("\t名称:%d\n", (USHORT)lpszName); } return EnumResourceLanguages(hModule, lpszType, lpszName, (ENUMRESLANGPROCW)EnumResLangProc, NULL); } BOOL CALLBACK EnumResTypeProc(HMODULE hModule, LPTSTR lpszType,LONG_PTR lParam) { if ((ULONG)lpszType & 0xFFFF0000) { printf("类型:%s\n", lpszType); }else { printf("类型:%d\n", (USHORT)lpszType); } return EnumResourceNames(hModule, lpszType, (ENUMRESNAMEPROCW)EnumRe1sNameProc, NULL); } int _tmain(int argc, _TCHAR* argv[]) { HMODULE hExe = LoadLibrary(_T("E:\\Program\\ResourcesDemo\\Debug\\stdWindow.exe")); if (hExe == NULL) { printf("加载目标程序出错!\n"); return GetLastError(); } printf("目标程序中包含以下资源:\n"); EnumResourceTypes(hExe, EnumResTypeProc, NULL); return 0; }这段代码有以下几点需要注意:LoadLibrary不仅仅可以用来加载dll,实际上它可以加载任意的PE文件到内存,而GetModuleHandle是在内存中查找已经存在的一个模块的句柄,而我们这个地方这个exe事先并没有加载到内存,所以这里用GetModuleHandle是不能正确加载的,只有使用LoadLibrary这几个枚举函数都需要一个回调函数,这些函数指针类型可以在msdn中查找到,在VC环境下也定义了这些函数指针,但是不知道为什么在填入函数指针时需要强制转化,否则会报错资源可以使用字符串表示,也可以使用ID表示,这些回调函数虽说传入的都是枚举到的字符串指针,但是它仍然可能是ID,所以在这不能简单的直接把他们作为字符串使用,需要进行判断,判断的依据是它是否大于65536,因为我们说只有在ID值大于这个时,系统才会将ID作为字符串来使用
2017年05月21日
5 阅读
0 评论
0 点赞
2017-05-16
枚举进程中的模块
在Windows中枚举进程中的模块主要是其中加载的dll,在VC上主要有2种方式,一种是解析PE文件中导入表,从导入表中获取它将要静态加载的dll,一种是利用查询进程地址空间中的模块,根据模块的句柄来得到对应的dll,最后再补充一种利用Windows中的NATIVE API获取进程内核空间中的模块,下面根据给出这些方式的具体的代码片段:解析PE文件来获取其中的dll在之前介绍PE文件时说过PE文件中中存在一个导入表,表中记录了程序中加载的导入dll以及这些dll中函数的信息,这个结构的定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR;我只需要获取这个结构并且根据RVA计算出它在文件中的偏移即可找到对应名称,利用之前PE解析器中的CPeFileInfo类来解析它即可,下面是具体的代码:void EnumModulesByPe(LPCTSTR pszPath) { CPeFileInfo peFile; peFile.strFilePath = pszPath; peFile.LoadFile(); if (!peFile.IsPeFile()) { printf("is not a pe file!\n"); return ; } peFile.InitDataDirectoryTable(); PIMAGE_IMPORT_DESCRIPTOR pImportTable = peFile.GetImportDescriptor(); while(!peFile.IsEndOfImportTable(pImportTable)) { printf("%s\n", peFile.RVA2fOffset(pImportTable->Name, (DWORD)peFile.pImageBase)); pImportTable++; } }利用之前的PE解析的类,首先给类中的文件路径赋值,然后加载到内存,并初始化它的数据目录表信息,从表中取出导入表的结构,根据结构中的Name字段的值来计算它的真实地址,即可解析出它里面的模块,这里我们只能解析出PE文件中自身保存的信息,如果dll是在程序运行之时调用LoadLibrary动态加载的,利用这个方法是找不到的。解析进程地址空间中的模块这个方法首先通过OpenProcess函数获取对应进程的句柄,然后调用EnumProcessModules枚举进程地址空间中当前存在的模块,这个函数会返回一个HMODULE句柄的数组,我们遍历这个数组,对其中的每个句柄调用GetModuleFileNameEx(很多模块GetModuleFileName获取不到,具体原因我没有深入研究)获取对应的文件路径。下面是具体的代码: HMODULE* phMods = NULL; HANDLE hProcess = NULL; DWORD dwNeeded = 0; DWORD i = 0; TCHAR szModName[MAX_PATH] = {}; hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId ); if (NULL == hProcess) { printf("不能打开进程[ID:0x%x]句柄,错误码:0x%08x\n",dwProcessId); return; } EnumProcessModules(hProcess, NULL, 0, &dwNeeded); phMods = (HMODULE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwProcessId); if( EnumProcessModules(hProcess, phMods, dwNeeded, &dwNeeded)) { for ( i = 0; i < (dwNeeded / sizeof(HMODULE)); i++ ) { ZeroMemory(szModName,MAX_PATH*sizeof(TCHAR)); //在这如果使用GetModuleFileName,有的模块名称获取不到,函数返回无法找到该模块的错误 if ( GetModuleFileNameEx(hProcess, phMods[i], szModName,MAX_PATH)) { printf("%ws\n", szModName); } } } HeapFree(GetProcessHeap(), 0, phMods); CloseHandle( hProcess );由于静态加载的dll在进程启动之时就已经被加载到内存中,所以利用这个方法自然可以获取静态加载的dll,但是由于它是获取进程地址空间中加载的dll,所以要求进程要正在运行,毕竟进程如果没有运行,那么也就不存在地址空间,也就无法获取其中加载的dll,另外它只能获取当前进程地址空间中的dll,有的dll这个时候还没有被加载的话,它自然也获取不到。所以这个方法也不是能获取所有加载的dll使用进程快照获取模块这种方式是先使用 CreateToolhelp32Snapshot 函数来创建一个进程模块的快照,然后通过 Module32First 和 Module32Next 函数来遍历快照中的模块信息Module32First 和Module32Next 函数会通过参数返回一个 MODULEENTRY32结构的模块信息,该结构的定义如下:typedef struct tagMODULEENTRY32 { DWORD dwSize; DWORD th32ModuleID; DWORD th32ProcessID; DWORD GlblcntUsage; DWORD ProccntUsage; BYTE* modBaseAddr; DWORD modBaseSize; HMODULE hModule; TCHAR szModule[MAX_MODULE_NAME32 + 1]; TCHAR szExePath[MAX_PATH]; } MODULEENTRY32, *PMODULEENTRY32;dwSize 参数表示该结构体的大小,在调用Module32First 和Module32Next之前需要先设置这个成员的值th32ProcessID:对应进程的IDmodBaseAddr:模块的起始地址,也就是模块被加载到内存的基地址hModule: 模块的句柄szModule:模块名称szExePath: 模块对应文件所在的路径下面是一个使用该方式获取加载模块的例子BOOL GetModulesTH32(WORD wPID) { HANDLE hSnap; MODULEENTRY32 me; me.dwSize = sizeof(me); hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, wPID); if (hSnap == INVALID_HANDLE_VALUE) { FreeLibrary(hToolhelp); return FALSE; } keepGoing = Module32First(hSnap, &me); while (keepGoing) { printf("%d, %08x, %s, %s\r\n", me.modBaseAddr, me.modBaseSize, me.szModule, me.szExePath); keepGoing = Module32Next(hSnap, &me); } CloseHandle(hSnap); FreeLibrary(hToolhelp); return TRUE; }获取内核地址空间中的模块不管是解析PE文件还是调用EnumProcessModules都只能获取用户层地址空间中的模块,但是进程不光有用户空间,还有内核空间,所以在这再提供一种枚举内核地址空间的模块的方法。枚举内核地址空间主要使用函数ZwQuerySystemInformation(也可以使用NtQuerySystemInformation)在msdn中明确指出,这两个函数未来可能不在使用,不推荐使用,但是至少现在是仍然支持的,并且可以很好的完成任务。这两个函数主要在ntdll.dll中导出,两个函数的参数用法完全相同,只是一个是比较上层一个比较底层而已。在这主要说明一个,另一个完全一样:NTSTATUS WINAPI ZwQuerySystemInformation( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );函数的第一个参数是一个枚举类型,用来表示我们将要调用此函数来获取系统哪方面的信息,第二个参数是一个缓冲区,用来存储该函数输出的值,第三个参数是缓冲区的长度,第四个参数是实际需要缓冲区的长度,说到这应该很快就可以反应过来,我们可以第一次调用这个函数传入一个NULL缓冲,缓冲长度给0,让他返回具体的长度,然后根据这个长度,动态分配一块内存,再次调用传入正确的缓冲和长度,获取数据。在调用这个函数时需要注意下面几点:这个函数是未导出的,所以在微软的开发环境中是没有它的定义的,要使用它需要我们自己定义,定义的代码如下://这个NTSTATUS结构在应用层有定义,直接使用即可 typedef NTSTATUS(WINAPI *ZWQUERYSYSTEMINFORMATION)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength );这个函数使用的一些结构是在内核开发环境DDK中定义的,在应用层中可能没有它的定义,所以在这我们也需要对它们进行定义:#define NT_SUCCESS(status) ((NTSTATUS)(status)>=0) typedef enum _SYSTEM_INFORMATION_CLASS { SystemBasicInformation, SystemProcessorInformation, SystemPerformanceInformation, SystemTimeOfDayInformation, SystemPathInformation, SystemProcessInformation, SystemCallCountInformation, SystemDeviceInformation, SystemProcessorPerformanceInformation, SystemFlagsInformation, SystemCallTimeInformation, SystemModuleInformation, SystemLocksInformation, SystemStackTraceInformation, SystemPagedPoolInformation, SystemNonPagedPoolInformation, SystemHandleInformation, SystemObjectInformation, SystemPageFileInformation, SystemVdmInstemulInformation, SystemVdmBopInformation, SystemFileCacheInformation, SystemPoolTagInformation, SystemInterruptInformation, SystemDpcBehaviorInformation, SystemFullMemoryInformation, SystemLoadGdiDriverInformation, SystemUnloadGdiDriverInformation, SystemTimeAdjustmentInformation, SystemSummaryMemoryInformation, SystemNextEventIdInformation, SystemEventIdsInformation, SystemCrashDumpInformation, SystemExceptionInformation, SystemCrashDumpStateInformation, SystemKernelDebuggerInformation, SystemContextSwitchInformation, SystemRegistryQuotaInformation, SystemExtendServiceTableInformation, SystemPrioritySeperation, SystemPlugPlayBusInformation, SystemDockInformation, SystemProcessorSpeedInformation, SystemCurrentTimeZoneInformation, SystemLookasideInformation } SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;缓冲区中存储的数据是一个表示返回数组中元素个数的DWORD类型的数据和一个对应结构体的数组,在MSDN上对这个缓冲进行解释时说这个缓冲区的头4个字节存储了对应数组的元素个数,而后面的存储的是对应结构的数组,所以在获取这个结构的数组时需要向后偏移4个字节。这个结构与我们传入的枚举值有关,比如我们在这获取的是进程内核空间中加载的模块信息,即传入的枚举值是SystemModuleInformation,它对应的结构应该是SYSTEM_MODULE_INFORMATION,它们之间的对应关系可以在MSDN中找到。这个结构也需要自己定义,它的定义如下:typedef struct _SYSTEM_MODULE_INFORMATION // Information Class 11 { ULONG Reserved[2]; PVOID pBase; ULONG Size; ULONG Flags; USHORT Index; USHORT Unknown; USHORT LoadCount; USHORT ModuleNameOffset; CHAR ImageName[256]; } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;下面就是这个的代码:void EnumKernelModules() { HMODULE hNtDll = LoadLibrary(_T("ntdll.dll")); if (INVALID_HANDLE_VALUE == hNtDll) { printf("加载ntdll.dll失败\n"); return ; } ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { printf("导出函数失败\n"); return; } PULONG pBuffInfo = NULL; DWORD dwSize = 0; ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, 0, &dwSize); pBuffInfo = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize); NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, pBuffInfo, dwSize, &dwSize); if (!NT_SUCCESS(status)) { return; } //在这为了验证之前说的,通过这两句输出发现他们的结果相同 printf("%d\n", *pBuffInfo); printf("%d\n", dwSize / sizeof(SYSTEM_MODULE_INFORMATION)); PSYSTEM_MODULE_INFORMATION pModuleInfo = (PSYSTEM_MODULE_INFORMATION)((ULONG)pBuffInfo + 4); for (int i = 0; i < *pBuffInfo; i++) { printf("%s\n", pModuleInfo[i].ImageName); } }
2017年05月16日
7 阅读
0 评论
0 点赞
2017-05-08
PE解析器的编写(四)——数据目录表的解析
在PE结构中最重要的就是区块表和数据目录表,上节已经说明了如何解析区块表,下面就是数据目录表,在数据目录表中一般只关心导入表,导出表和资源这几个部分,但是资源实在是太复杂了,而且在一般的病毒木马中也不会存在资源,所以在这个工具中只是简单的解析了一下导出表和导出表。这节主要说明导入表,下节来说导出表。RVA到fRva的转化RVA转化为fRva主要是通过某个数据在内存中的相对偏移地址找到其在文件中的相对偏移地址,在对某个程序进行逆向时,如果找到关键的那个变量或者那句指令,我根据变量或者代码指令在内存中的RVA找到它在文件中的偏移,就可以找到它的位置,修改它可能就可以破解某个程序。废话不多说,直接上代码:DWORD CPeFileInfo::RVA2fOffset(DWORD dwRVA, DWORD dwImageBase) { InitSectionTable(); vector<IMAGE_SECTION_HEADER>::iterator it; for (it = m_SectionTable.begin(); it != m_SectionTable.end(); it++) { if (dwRVA >= (DWORD)(it->VirtualAddress) && dwRVA <= (DWORD)((DWORD)(it->VirtualAddress) + it->Misc.VirtualSize) ) { break; } } if (it == m_SectionTable.end()) { return -1; } return (DWORD)(dwRVA - (DWORD)(it->VirtualAddress) + (DWORD)(it->PointerToRawData) + dwImageBase); }系统在将PE文件加载到内存中时,PE中的区块是按页的方式对齐的,也就是说同一节中的内容在一页内存中的排列方式与在文件中的排列方式相同,所以这里利用这一关系就可以根据RVA推算出它在文件中的偏移,即:fRva - Roffset = Rva - Voffset ,其中fRva是某个成员在文件中的偏移,Roffset是区块在文件中的偏移,Voffset是该区块在内存中的偏移。上述代码就是利用这个原理来计算的,代码中存在一个循环,VirtualAddress 和 VirtualSize分别代表这个区块在内存中的起始地址和这个区块所占内存的大小,当这个RVA大于起始地址,小于起始地址 + 区块大小也就说明这个RVA是处在这个区块中,这样我们就找到RVA所在区块,用RVA - 区块起始地址就得到它在区块中的偏移,这个偏移加上区块在文件中的首地址,得到的就是RVA对应的在文件中的偏移,只要知道文件的起始地址就可以知道它在文件中的详细位置了。获取数据目录表的信息数据目录表的信息主要存储在PE头结构中的OptionHeader中,回顾一下它的定义:typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;这个结构的最后一个结构是数据目录表的数组,而NumberOfRvaAndSizes表示数据目录表中元素的个数,一般都是8,而IMAGE_DATA_DIRECTORY 结构的定义如下:typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;第一个是指向某个具体的表结构的RVA,第二个是这个表结构的大小,在这个解析器中,主要显示这两项,同时为了方便在文件中查看,我们新加了一项,就是它在文件中的偏移在这个解析器的代码中,我们定义了一个结构来存储这些信息struct IMAGE_DATA_DIRECTORY_INFO { PVOID pVirtualAddress; PVOID pFileOffset; DWORD dwVirtualSize; };在类中定义了一个该结构的vector结构,同时定义一个InitDataDirectoryTable函数来初始化这个结构void CPeFileInfo::InitDataDirectoryTable() { if (!m_DataDirectoryTable.empty()) { //先清空之前的内容 m_DataDirectoryTable.clear(); } PIMAGE_SECTION_HEADER pSectionHeader = GetSectionHeader(); PIMAGE_OPTIONAL_HEADER pOptionalHeader = GetOptionalHeader(); PIMAGE_DATA_DIRECTORY pDataHeader = pOptionalHeader->DataDirectory; IMAGE_DATA_DIRECTORY_INFO dataInfo; for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++) { dataInfo.pVirtualAddress = (PVOID)(pDataHeader[i].VirtualAddress); dataInfo.dwVirtualSize = pDataHeader[i].Size; dataInfo.pFileOffset = (PVOID)RVA2fOffset((DWORD)(dataInfo.pVirtualAddress), 0); //这里调用这个函数计算它的偏移所以这里假定文件的起始地址为0 m_DataDirectoryTable.push_back(dataInfo); } }上述代码比较简单,获得了OptionHeader结构的指针后直接找到DataDirectory的地址,就可以得到数组的首地址,然后在循环中依次遍历这个数组就可以得到各项的内容,对于文件中的偏移直接调用之前写的那个转化函数即可导入表的解析导入的dll的信息的获取导入表在数据目录表的第1项,所以我们只需要区数据目录表数组中的第一个元素,从中就可以得到它的RVA,然后调用RVA到文件偏移的转化函数就可以在文件中找到它的位置,在代码中也是这样做的PIMAGE_IMPORT_DESCRIPTOR CPeFileInfo::GetImportDescriptor() { //由于这个表中保存的是RVA,要在文件中遍历,需要转为在文件中的偏移 PVOID pImportRVA = m_DataDirectoryTable[1].pVirtualAddress; //在读取这些数据的时候,是从内存中读取的,从内存中读取时,需要考虑文件被加载到内存中的基址 return (PIMAGE_IMPORT_DESCRIPTOR)RVA2fOffset((DWORD)pImportRVA, (DWORD)pImageBase); }导入表在Windows中的定义如下:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 一般给0 DWORD OriginalFirstThunk; //指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。这个IMAGE_THUNK_DATA在后面解析具体函数时会用到 }; DWORD TimeDateStamp; //忽略,很少使用它 DWORD ForwarderChain; //忽略,很少使用它 DWORD Name; //这个dll的名称 DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;这个结构是以一个数组的形式存储在对应的位置,所以说我们只要找到第一个结构的位置就可以找到剩余的位置,但是这个数组的个数事先并不知道,数组的最后一个元素都为0,所以在遍历到对应的都为0 的成员是就到了它的尾部,根据这个我们定义了一个函数,来判断它是否到达数组尾部,当不在尾部时将数组成员取出来,存储到事先定义的vector中。void CImportDlg::InitImportTable() { //获取导入函数表在文件中的偏移 PIMAGE_IMPORT_DESCRIPTOR pImportTable = m_pPeFileInfo->GetImportDescriptor(); if (NULL != pImportTable) { int i = 0; while (!IsEndOfTable(&pImportTable[i])) { m_ImportTable.push_back(pImportTable[i]); i++; } } } BOOL CImportDlg::IsEndOfTable(PIMAGE_IMPORT_DESCRIPTOR pImportTable) { //是否到达表的尾部,这个表中没有给出总共有多少项,需要自己判断 //判断条件是最后一项的所有内容都是null if (0 == pImportTable->OriginalFirstThunk && 0 == pImportTable->TimeDateStamp && 0 == pImportTable->ForwarderChain && 0 == pImportTable->Name && 0 == pImportTable->FirstThunk) { return TRUE; } return FALSE; }在显示时需要注意两点:name属性保存的是ASCII码形式的字符串,如果我们程序使用Unicode编码,需要进行对应的转化。. 要将时间戳转化为我们常见的时分秒的格式。下面是显示这些信息的部分代码: //根据Name成员中的RVA推算出其在文件中的偏移 char *pName = (char*)m_pPeFileInfo->RVA2fOffset(it->Name, (DWORD)(m_pPeFileInfo->pImageBase)); if (NULL == pName || -1 == (int)pName) { m_ImportList.InsertItem(i, _T("-")); }else { #ifdef UNICODE //如果是UNICODE字符串,那么需要进行转化 WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pName, strlen(pName), wszName, 256); m_ImportList.InsertItem(i, wszName); #else m_ImportList.InsertItem(i, pName); #endif } //显示时间戳 tm p; errno_t err1; err1 = gmtime_s(&p,(time_t*)&it->TimeDateStamp); TCHAR s[100] = {0}; _tcsftime (s, sizeof(s) / sizeof(TCHAR), _T("%Y-%m-%d %H:%M:%S"), &p); m_ImportList.SetItemText(i, 1, s);导入dll中的函数信息dll中函数的信息需要使用之前的FirstThunk来获取,其实OriginalFirstThunk与FirstThunk指向的是同一个结构,都是指向一个IMAGE_THUNK_DATA STRUC的结构,这个结构的定义如下:typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32;这个结构是一个公用体,它其实只占4个字节,当 它的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。 当 它的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。 这个结构的定义如下:typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;为什么要用两个指针指向同一个结构呢?这个跟dll的加载有关,由OriginalFirstThunk指向的结构是一个固定的值,不会被重写的值,一般它里面保存的是函数的名称,而由FirstThunk 保存的结构一般是由PE解析器进行重写,PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,也就是说此时的FirstThunk 不再指向这个INAGE_IMPORT_BY_NAME结构,而是真实的函数的RVA。因此我们称为输入地址表(IAT)。所以在解析这个PE文件时一般使用OriginalFirstThunk这个成员来获取dll中的函数信息,因为需要获取函数名称。void CImportDlg::ShowFunctionInfoByDllIndex(int nIndex) { IMAGE_IMPORT_DESCRIPTOR ImageDesc = m_ImportTable[nIndex]; //获取到对应项所指向的IMAGE_THUNK_DATA的指针 PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)m_pPeFileInfo->RVA2fOffset(ImageDesc.OriginalFirstThunk, (DWORD)m_pPeFileInfo->pImageBase); CString strInfo = _T(""); int i = 0; if (NULL == pThunkData || 0xffffffff == (int)pThunkData) { return; } //这个结构数组以0结尾 while (0 != pThunkData->u1.AddressOfData) { //当它的最高位为0时表示函数以字符串类型的函数名方式输入,此时才解析这个信息得到函数名 strInfo.Format(_T("%08x"), pThunkData); m_TunkList.InsertItem(i, strInfo); strInfo.Format(_T("%08x"), pThunkData->u1.AddressOfData); m_TunkList.SetItemText(i, 1, strInfo); if (0 == (pThunkData->u1.AddressOfData & 0x80000000)) { PIMAGE_IMPORT_BY_NAME pIibn= (PIMAGE_IMPORT_BY_NAME)m_pPeFileInfo->RVA2fOffset(pThunkData->u1.AddressOfData, (DWORD)m_pPeFileInfo->pImageBase); //name 这个域保存的是函数名的第一个字符,所以它的地址就是函数名字符串的地址 char *pszName = (char*)&(pIibn->Name); #ifdef UNICODE WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pszName, strlen(pszName), wszName, 256); m_TunkList.SetItemText(i, 2, wszName); #else m_TunkList.SetItemText(i, 3, pszName); #endif strInfo.Format(_T("%04x"), pIibn->Hint); m_TunkList.SetItemText(i, 3, strInfo); } else { m_TunkList.SetItemText(i, 2, _T("-")); m_TunkList.SetItemText(i, 3, _T("-")); } pThunkData++; } }上面的代码主要用来解析对应dll中的函数。在上面的代码中,根据用户点击鼠标的序号得到对应的dll项的结构信息,根据OriginalFirstThunk中保存的RVA找到结构IMAGE_THUNK_DATA对应的地址,得到地址后利用0x80000000这个值对它的最高位进行判断,如果为0,那么就可以获得函数名称,在获得名称时,也是需要注意函数名称在Unicode环境下需要转化。在这段代码中主要显示了函数的Thunk的rva,这个rva转化后对应的值,函数名,以及里面的Hint导出表的解析一般的exe文件不存在导出表,只有在dll中存在导出表。导出表中主要存储的是一个序号和对应的函数名,序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。 导出表在数据目录表的第0个元素。导出表的结构如下:typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //属性信息 DWORD TimeDateStamp; //生成日期 WORD MajorVersion; //主版本号 WORD MinorVersion; //副版本号 DWORD Name; //dll的名称 DWORD Base; //导出函数序号的起始值 DWORD NumberOfFunctions; //文件中包含的导出函数的总数。 DWORD NumberOfNames; //文件中命名函数的总数,这个一般与上面的那个总数相同 DWORD AddressOfFunctions; //指向导出函数地址的RVA DWORD AddressOfNames; //指向导出函数名字的RVA DWORD AddressOfNameOrdinals; //指向导出函数序号的RVA } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;下面对这3个RVA进行详细说明:AddressOfFunctions,这个RVA指向的是一个双字数组,数组中的每一项是一个RVA 值,存储的是所有导出函数的入口地址,数组的元素个数等于NumberOfFunctionsAddressOfNames:这个RVA指向一个包含所有导出函数名称的表的指针,这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。数组的项数等于NumberOfNames 字段的值AddressOfNameOrdinals:指向一个字型数组(注意这里不是双字)存储的是对应函数的地址,假如现在我们使用函数GetProcAddress在dll中导出一个函数A,它会根据这个函数名称在名称的表中查找,假设它找到的是函数名称表中的第x项与之相同,那么它会在AddressOfNameOrdinals表中查找第x项得到函数的序号,最后根据这个序号在AddressOfFunctions中找到对应的函数地址。void CExportInfoDlg::ShowFunctionInfo() { if (NULL == m_pPeFileInfo) { return; } PIMAGE_EXPORT_DIRECTORY pExportTable = m_pPeFileInfo->GetExportDeirectory(); PDWORD pAddressOfFunc = (PDWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfFunctions, (DWORD)m_pPeFileInfo->pImageBase); PWORD pOriginals = (PWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfNameOrdinals, (DWORD)m_pPeFileInfo->pImageBase); PDWORD pFuncName = (PDWORD)m_pPeFileInfo->RVA2fOffset((DWORD)pExportTable->AddressOfNames, (DWORD)m_pPeFileInfo->pImageBase); int nCount = pExportTable->NumberOfFunctions; CString strInfo = _T(""); if (pAddressOfFunc == NULL || (int)pAddressOfFunc == -1 || pOriginals == NULL || (int)pOriginals == -1 || pFuncName == NULL || (int)pFuncName == -1) { return; } for (int i = 0; i < nCount; i++) { //导出序号等于base + 在数组中的索引(pOriginals数组保存的值) if (pOriginals[i] > nCount) { //这个索引值无效 strInfo = _T("-"); }else { strInfo.Format(_T("%04x"), pOriginals[i] + pExportTable->Base); } m_FuncInfoList.InsertItem(i, strInfo); strInfo.Format(_T("%08x"), pAddressOfFunc[pOriginals[i]]); m_FuncInfoList.SetItemText(i, 2, strInfo); char *pszName = (char*)m_pPeFileInfo->RVA2fOffset(pFuncName[i], (DWORD)m_pPeFileInfo->pImageBase); #ifdef UNICODE WCHAR wszName[256] = _T(""); MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, pszName, strlen(pszName), wszName, 256); strInfo = wszName; #else strInfo = pszName; #endif m_FuncInfoList.SetItemText(i, 1, strInfo); } }上面的代码描述了这个过程。在代码中首先获取了导出函数表的数据,根据数据中的三个RVA获取它们在文件中的真实地址。首先在名称表中遍历所有函数名称,然后在对应的序号表中找到对应的序号,我在这个解析器中显示出的序号与Windows显示给外界的序号相同,但是在pe文件内部,在进行寻址时使用的是这个序号 - base的值,寻址时使用的是减去base后的值作为元素的位置。pAddressOfFunc[pOriginals[i]] 这句首先找到它在序号表中的序号值,然后根据这个序号在地址表中找到它的地址,在这得到的只是一个RVA地址,如果想得到具体的地址,还需要加上在内存或者文件的起始地址
2017年05月08日
6 阅读
0 评论
0 点赞
2017-05-06
PE解析器的编写(三)——区块表的解析
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。 具有相同属性的数据被安排到同一个区块中。区块表的结构为IMAGE_SECTION_HEADER,在PE文件中存在一个该结构的数组,用来保存各个区块的信息,这个数组的大小在PE头的结构 IMAGE_NT_HEADERS 的成员NumberOfSections描述。区块表结构IMAGE_SECTION_HEADER结构如下:typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8 union { DWORD PhysicalAddress; // 物理地址 DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个 } Misc; DWORD VirtualAddress; // 节区的 RVA 地址 DWORD SizeOfRawData; // 在文件中对齐后的尺寸 DWORD PointerToRawData; // 在文件中的偏移量 DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移 DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地) WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目 WORD NumberOfLinenumbers; // 行号表中行号的数目 DWORD Characteristics; // 节属性如可读,可写,可执行等 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;在程序中我们主要列出了,区块名称、RVA、在进行内存对齐后的尺寸,在磁盘中的大小,在文件中的偏移,节属性。在界面中,定义了一个listctrl来显示这些信息。在CPeFileInfo类中定义了一个vector<IMAGE_SECTION_HEADER> m_SectionTable;专门用来存储区块表的属性信息。获取这个信息。在这个类中与区块表有关的函数主要有两个:GetSectionHeader : 用来获取指向表的指针InitSectionTable:初始化上面定义的结构下面来一一说明这两个函数PIMAGE_SECTION_HEADER CPeFileInfo::GetSectionHeader() { PIMAGE_FILE_HEADER pFileHeader = GetFileHeader(); PIMAGE_SECTION_HEADER pSectionHeader = NULL; if (NULL == pFileHeader) { return NULL; } PIMAGE_OPTIONAL_HEADER pOptionHeader = GetOptionalHeader(); pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)(pOptionHeader) + pFileHeader->SizeOfOptionalHeader); return pSectionHeader; // PIMAGE_NT_HEADERS pNtHeader = GetNtHeaders(); // return (PIMAGE_SECTION_HEADER)((DWORD)pNtHeader + sizeof(IMAGE_NT_HEADERS)); } 在PE文件中区块表的属性信息是紧密排列在PE头结构后面的,所以我们只要知道OptionHeader结构的指针,然后加上这个结构的大小就可以获取到区块表的地址,上面的代码也是这样做的,首先获取了FileHeader的指针,这个结构中的SizeOfOptionalHeader定义了OptionHeader这个结构的大小,我们利用FileHeader + SizeOfOptionalHeader这样就偏移到了区块表所在的位置。或者更简单的方式是利用PE文件头的地址 + 文件头的大小也一样可以获取到区块表的地址void CPeFileInfo::InitSectionTable() { if (!m_SectionTable.empty()) { return ; } PIMAGE_SECTION_HEADER pSectionHeader = GetSectionHeader(); PIMAGE_FILE_HEADER pFileHeader = GetFileHeader(); if (NULL != pSectionHeader && NULL != pFileHeader) { DWORD dwCountOfSection = pFileHeader->NumberOfSections; int nCount = 0; while (nCount < dwCountOfSection) { IMAGE_SECTION_HEADER ImageSec = pSectionHeader[nCount]; m_SectionTable.push_back(ImageSec); nCount++; } } }后面就是循环遍历将所有信息写入m_SectionTable这个动态数组中。在这份代码中我们首先利用FileHeader的NumberOfSections成员获取区块表的个数,然后在循环中以这个个数作为条件,以此往后寻址,将信息写入到对应的数组中,最后在输出的时候只需要根据需求输出我们感兴趣的内容即可
2017年05月06日
3 阅读
0 评论
0 点赞
2017-05-04
PE文件解析器的编写(二)——PE文件头的解析
之前在学习PE文件格式的时候,是通过自己查看各个结构,自己一步步计算各个成员在结构中的偏移,然后在计算出其在文件中的偏移,从而找到各个结构的值,但是在使用C语言编写这个工具的时候,就比这个方便的多,只要将对应的指针类型转化为各个结构类型,就可以使用指针中的箭头来直接寻址到结构中的各个成员。这次主要说明的是PE文件头的解析,也就是之前看到的第一个界面中显示的内容,这个部分涉及到CPeFileInfo这个解析类的部分代码,以及CPeFileInfoDlg这个对话框类的代码。选择目标文件首先通过点击open按钮来弹出一个对话框,让用户选择需要解析的文件。这个部分的代码如下:void CPEInfoDlg::OnBnClickedBtnOpen() { // TODO: 在此添加控件通知处理程序代码 TCHAR szFilePath[MAX_PATH] = _T(""); OPENFILENAME ofn = {0}; ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = m_hWnd; ofn.hInstance = GetModuleHandle(NULL); ofn.nMaxFile = MAX_PATH; ofn.lpstrInitialDir = _T("."); ofn.lpstrFile = szFilePath; ofn.lpstrTitle = _T("Open ...[PEInfo] by liuhao"); ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; ofn.lpstrFilter = _T("*.*\0*.*\0"); GetOpenFileName(&ofn); m_PeFileInfo.strFilePath = szFilePath; GetDlgItem(IDC_FILE_PATH)->SetWindowText(szFilePath); m_PeFileInfo.UnLoadFile(); BOOL bLoadSuccess = m_PeFileInfo.LoadFile(); if (bLoadSuccess) { //成功打开 if (m_PeFileInfo.IsPeFile()) { ShowFileHeaderInfo(); ShowOptionHeaderInfo(); GetDlgItem(IDC_BTN_CALC)->EnableWindow(TRUE); GetDlgItem(IDC_BTN_DATA_DIR_INFO)->EnableWindow(TRUE); GetDlgItem(IDC_BTN_SECTION_INFO)->EnableWindow(TRUE); GetDlgItem(IDC_BTN_CHAR_INFO)->EnableWindow(TRUE); GetDlgItem(IDC_RVA)->EnableWindow(TRUE); }else { MessageBox(_T("打开的文件不是PE文件")); InitCommandCtrl(); } } }在这段代码中首先通过GetOpenFileName函数来弹出一个选择文件的对话框。这个函数需要传入一个OPENFILENAME 结构的指针变量,这个结构比较复杂,在这说下它比较重要的几个成员:lStructSize //结构的大小 hwndOwner //这个对话框所属的父窗口句柄 hInstance //所在模块的句柄 lpstrFile //用来保存用户选择文件的路径的缓冲 nMaxFile //缓冲区的大小 lpstrTitle //这个对话框的标题 Flags//对话框的标识,具体标识请查看MSDN,一般我们用这样几个就足够了一般只需要更改标题,内存缓冲区指针和它的大小,其余按照上面的代码默认就好用户选择后,将用户选择的文件的全路径显示出来,并调用CPefileInfo类中的加载函数进行加载,如果之前加载过,那么先卸载它。然后再在对话框中显示它主要的信息,并且将所有按钮设置为可用状态,加载与卸载PE文件结构在这个里面主要有这样几个函数m_PeFileInfo.UnLoadFile(); m_PeFileInfo.LoadFile(); m_PeFileInfo.IsPeFile(); ShowFileHeaderInfo(); ShowOptionHeaderInfo();接下来转到CPeFileInfo这个类中在这个类中定义了这样四个主要的成员 CString strFilePath; //对应PE文件所在路径 HANDLE hFile; //打开这个文件时的文件句柄 PVOID pImageBase; //文件在内存中的首地址 HANDLE hMapping; //文件映射的句柄BOOL CPeFileInfo::LoadFile() { if(strFilePath.IsEmpty()) { return FALSE; } hFile = CreateFile(strFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hFile) { return FALSE; } hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (NULL == hMapping) { CloseHandle(hFile); hFile = NULL; return FALSE; } pImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); if (NULL == pImageBase) { CloseHandle(hMapping); CloseHandle(hFile); hMapping = NULL; hFile = NULL; return FALSE; } return TRUE; } void CPeFileInfo::UnLoadFile() { if(pImageBase != NULL) { UnmapViewOfFile(pImageBase); } if(NULL != hMapping) { CloseHandle(hMapping); } if (NULL != hFile) { CloseHandle(hFile); } pImageBase = NULL; hFile = NULL; hMapping = NULL; } BOOL CPeFileInfo::IsPeFile() { PIMAGE_DOS_HEADER pDosHeader = NULL; PIMAGE_NT_HEADERS pNtHeader = NULL; if (NULL == pImageBase) { return FALSE; } pDosHeader = (PIMAGE_DOS_HEADER)pImageBase; if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { return FALSE; } pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)(pDosHeader->e_lfanew) + (DWORD)pImageBase); if (pNtHeader->Signature != IMAGE_NT_SIGNATURE) { return FALSE; } return TRUE; }在加载的时候,主要通过一个文件映射的方式,将pe文件的整个内容原模原样的拷贝到内存中,并保存这个文件句柄,文件映射句柄,文件所在内存的首地址等信息,在卸载的时候进行关闭句柄,清理资源的操作。在程序中有一个判断该文件是否是PE文件的操作。在PE的DOS头结构中的e_magic结构保存的是'MZ'这个标志对应的16进制数是0x4d5a,另外在pe头中有一个Sinature成员保存了0x50450000这个值,它对应的字符是‘PE’只有满足这两个条件的文件才是一个正常的PE文件。否则就不是。获取DOS头和PE头在之前我们说过,PE文件开始的位置是一个DOS头结构——IMAGE_DOS_HEADER STRUCT,它的第一个成员作为DOS头的标识,一般保存的都是‘MZ’,而它里面的e_lfanew则保存真正的PE头所在的偏移所在获取DOS头的时候简单的将前面的几个字节转化为这个结构即可,在寻址PE头的时候用e_lfanew成员加上文件的起始地址就可以得到PE头的地址。具体对应的代码如下:pDosHeader = (PIMAGE_DOS_HEADER)pImageBase; pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)(pDosHeader->e_lfanew) + (DWORD)pImageBase);显示FileHeader信息和ptionalHeader信息在PE头的结构体定义如下:IMAGE_NT_HEADERS STRUCT { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS ENDS这个里面的第二个第三个成员就分别是FileHeader信息和ptionalHeader信息,剩下的就只是对这个结构的部分重要成员进行解析和显示了void CPEInfoDlg::ShowFileHeaderInfo() { PIMAGE_FILE_HEADER pFileHeader = m_PeFileInfo.GetFileHeader(); if (NULL != pFileHeader) { //省略部分不重要的代码 //时间戳转为具体时间 tm p; errno_t err1; err1 = gmtime_s(&p,(time_t*)&pFileHeader->TimeDateStamp); TCHAR s[100] = {0}; _tcsftime (s, sizeof(s) / sizeof(TCHAR), _T("%Y-%m-%d %H:%M:%S"), &p); GetDlgItem(IDC_TIME_STAMP)->SetWindowText(s); } else { MessageBox(_T("显示数据错误")); } }在这函数中我们直接通过指针寻址的方式来获取其中的信息,比如NumberOfSections(当前文件中的节表数量)、TimeDateStamp(时间戳信息)、PointerToSymbolTable(符号表所在地址相对于文件的偏移)、NumberOfSymbols(符号表的数目)、SizeOfOptionalHeader(OptionalHeader结构的大小)、Characteristics(文件的属性值)。在显示属性值时,另外提供了一个转化函数将这个值转化为具体的标识。需要进行转化的请参考下面的代码void CPeFileInfo::GetFileCharacteristics(CString &strCharacter) { DWORD dwMachine = 0; PIMAGE_FILE_HEADER pFileHeader = GetFileHeader(); if (0 != (pFileHeader->Characteristics & IMAGE_FILE_RELOCS_STRIPPED)) { strCharacter += _T("文件中不存在重定位信息\r\n"); } if(0 != (pFileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE)) { strCharacter += _T("文件可执行\r\n"); } if (0 != (pFileHeader->Characteristics & IMAGE_FILE_LARGE_ADDRESS_AWARE)) { strCharacter += _T("可以处理大于2GB内容\r\n"); } if(0 != (pFileHeader->Characteristics & IMAGE_FILE_32BIT_MACHINE)) { strCharacter += _T("目标平台为32位机器\r\n"); } if (0 != (pFileHeader->Characteristics & IMAGE_FILE_SYSTEM)) { strCharacter += _T("该文件是系统文件\r\n"); } if (0 != (pFileHeader->Characteristics & IMAGE_FILE_DLL)) { strCharacter += _T("该文件是dll文件\r\n"); } if(0 != (pFileHeader->Characteristics & IMAGE_FILE_UP_SYSTEM_ONLY)) { strCharacter += _T("该程序只能运行在单核处理器上"); } }对于OptionalHeader结构的解析,目前也只是简单的对它其中的数据结构进行打印,它里面最重要的结构DataDirectory留着在后面的部分进行说明。
2017年05月04日
3 阅读
0 评论
0 点赞
2017-05-03
PE解析器的编写(一)——总体说明
之前自己学习了PE文件的格式,后来自己写了个PE文件的解析器,这段时间工作上刚好要用到它,老板需要能查看某个exe中加载的dll的一个工具,我在使用之前自己写的这个东西的时候,发现很多东西都忘记了,所以,我在这回顾下当时的思路,并记录下来,方便以后直接使用。也算是回顾下之前学习的内容,将学的东西学以致用工具总体分为这样几个部分:文件头的信息pe文件节表的信息pe文件数据目录表的信息简单的从RVA到Frva的计算工具主要采用MFC的框架作为界面,pe文件的解析部分完全由自己编写,主要使用了Windows中定义的一些结构体。刚开始开启界面时,所有功能按钮和显示界面都为空,当我们正确加载一个pe文件后这些按钮就都可以使用。加载后效果如图:两侧显示pe文件的基本信息,比如文件头部中的信息,文件的OEP,基地址等等,右侧提供一个根据RVA计算它在文件中偏移的功能,工具可以显示数据目录表的信息和节表的信息。显示节表的信息如下:显示数据目录表的信息如下:一般在数据目录表中关心导出导出表,所以这个工具也提供了导入导出表的解析功能。下面是查看导入表的功能:这个主要分为两个部分,上部分是它导入的dll文件,当点击某个dll的时候,会将这个dll中的函数给列举出来。导出表的界面如下:在这个界面中主要显示这个dll模块的名称,其中导出的函数等信息。以上是程序的主要功能,下面说下程序各个模块的组成:这个是工具中的主要对话框资源,从上到下依次是关于(这个是MFC自己生成的,我只是将它的版本信息作了修改)、显示数据目录表信息的对话框,它对应的是第三个图、用来显示文件具体标识信息的对话框、显示节区表信息的对话框、显示导出表对话框、显示导入表对话框、显示pe文件头信息的对话框,是程序的主界面;我们为每一个对话框都关联了一个类,然后在专门写了一个解析pe文件中各种信息的类,这样整体的类视图如下:到此,我对这个工具中的模块作了简单的说明,后面会一一讲解各个部分的实现。敬请期待!!!!O(∩_∩)O程序源码在此
2017年05月03日
4 阅读
0 评论
0 点赞
2017-03-22
PE文件详解(九)
本篇文章转载自小甲鱼的一篇日志,原文地址我们知道,Windows 将程序的各种界面定义为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。为了吸引大家的兴趣和目光,咱先来做个学前试验,然后再憧憬一下我们将来学习的内容有啥意义!好,小甲鱼先来演示一下如何用工具来修改资源实现汉化、改图标等,接着我们进一步从原理上来解剖 PE文件如何对资源进行存放和索引。最后,在 PE系列章节讲解完毕后,小甲鱼和大家将所有学到的知识结合在一起,我们自己打造属于我们的个性 PE工具。资源结构资源是PE 文件中非常重要的部分,几乎所有的PE 文件中都包含着资源,与导入表和导出表相比,资源的组织方式要复杂很多,其实我们只要看下图就知道俺所言不虚。我们知道我们的资源有很多种类型,每种类型的资源中可能存在多个资源项,这些资源项用不同的ID 或者名称来区分。但是要将这么多种类型的不同ID 的资源有序地组织起来是一件非常痛苦的事情,因此,我们采取类似于磁盘目录结构的方式保存。从图中我们可以看到,PE 文件中的资源是按照 资源类型 -> 资源ID -> 资源代码页的3层树型目录结构来组织资源的,通过层层索引才能够进入相应的子目录找到正确的资源。资源目录结构数据目录表中的 IMAGE_DIRECTORY_ENTRY_RESOURCE 条目(第三项)包含资源的 RVA 和大小。资源目录结构中的每一个节点都是由 IMAGE_RESOURCE_DIRECTORY 结构和紧跟其后的数个IMAGE_RESOURCE_DIRECTORY_ENTRY 结构组成的。(是不是有点像我们之前提到的文件目录?文件夹每个都长得一样,一个嵌套另一个,这样子可以实现将非常复杂的数据细化切分,小泽玛利亚、苍井空、吉泽明步、松岛枫……)我们再来看这张图:认识了这层关系后,我们来看下 IMAGE_RESOURCE_DIRECTORY 这个结构,该结构长度为 16 字节,共有 6 个字段,定义如下:IMAGE_RESOURCE_DIRECTORY STRUCTCharacteristics DWORD ? ;理论上为资源的属性,不过事实上总是0TimeDateStamp DWORD ? ;资源的产生时刻MajorVersion WORD ? ;理论上为资源的版本,不过事实上总是0MinorVersion WORD ?NumberOfNamedEntries WORD ? ;以名称(字符串)命名的入口数量NumberOfIdEntries WORD ? ;以ID(整型数字)命名的入口数量IMAGE_RESOURCE_DIRECTORY ENDS其实在这里边我们唯一要注意的就是 NameberOfNamedEntries 和 NumberOfIdEntries,它们说明了本目录中目录项的数量。两者加起来就是本目录中的目录项总和。也就是后边跟着的IMAGE_RESOURCE_DIRECTORY_ENTRY 数目。资源目录入口的结构(IMAGE_RESOURCE_DIRECTORY_ENTRY)IMAGE_RESOURCE_DIRECTORY_ENTRY 紧跟在资源目录结构后,此结构长度为 8 个字节,包含 2 个字段。该结构定义如下:IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCTName DWORD ? ;目录项的名称字符串指针或IDOffsetToData DWORD ? ;目录项指针IMAGE_RESOURCE_DIRECTORY_ENTRY ENDSName 字段完全是个百变精灵,改字段定义的是目录项的名称或ID。当结构用于第一层目录时,定义的是资源类型;当结构定义于第二层目录时,定义的是资源的名称;当结构用于第三层目录时,定义的是代码页编号。注意:当最高位为 0 的时候,表示字段的值作为 ID 使用;而最高位为 1 的时候,字段的低位作为指针使用(资源名称字符串是使用 UNICODE编码),但是这个指针不是直接指向字符串哦,而是指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构的。该结构定义如下:IMAGE_RESOURCE_DIR_STRING_U STRUCTLength DWORD ? ; 字符串的长度NameString DWORD ? ; UNICODE字符串,由于字符串是不定长的。由Length 制定长度IMAGE_RESOURCE_DIR_STRING_U ENDSOffsetOfData 字段是一个指针,当最高位为 1 时,低位数据指向下一层目录块的其实地址;当最高位为 0 时,指针指向 IMAGE_RESOURCE_DATA_ENTRY 结构。注意:将 Name 和 OffsetToData 用做指针时需要注意,该指针是从资源区块开始的地方算起的偏移量(即根目录的起始位置的偏移量),不是我们习惯的 RVA 哦。最后,在上图中我们看到,在第一层的时候,IMAGE_RESOURCE_DIRECTORY_ENTRY的Name 字段作为资源类型使用。具体类型匹配见下表:资源数据入口经过三层 IAMGE_RESOURCE_DIRECTORY_ENTRY (一般是3层,偶尔更年期少一些。第一层资源类型,第二层资源名,第三层是资源的 Language),第三层目录结构中的 OffsetOfData 指向 IMAGE_RESOURCE_DATA_ENTRY 结构。该结构描述了资源数据的位置和大小,定义如下:IMAGE_RESOURCE_DATA_ENTRY STRUCTOffsetToData DWORD ? ; 资源数据的RVASize DWORD ? ; 资源数据的长度CodePage DWORD ? ; 代码页, 一般为0Reserved DWORD ? ; 保留字段IMAGE_RESOURCE_DATA_ENTRY ENDS千山万水,此处的 IMAGE_RESOURCE_DATA_ENTRY 结构就是真正的资源数据了。结构中的OffsetOfData 指向资源数据的指针,其为 RVA 值。
2017年03月22日
6 阅读
0 评论
0 点赞
2017-03-22
PE文件详解(八)
本文转载自小甲鱼PE文件详解系列教程原文传送门当应用程序需要调用DLL中的函数时,会由系统将DLL中的函数映射到程序的虚拟内存中,dll中本身没有自己的栈,它是借用的应用程序的栈,这样当dll中出现类似于mov eax, [1000000]这样直接寻址的代码时,由于事先并不知道它会被映射到应用程序中的哪个位置,并且可能这个内存地址已经被使用,所以当调用dll中的函数时,系统会进行一个基址重定位的操作。系统是根据dll中的基址重定位表中的信息决定如何进行基址重定位,哪些位置的指令需要进行基址重定位。所以这次主要说明基址重定位表。这个重定位表位于数据目录表的第六项。这个表的主要结构如下:IMAGE_BASE_RELOCATION STRUC VirtualAddress DWORD ? ; 重定位数据开始的RVA 地址 SizeOfBlock DWORD ? ; 重定位块得长度 TypeOffset WORD ? ; 重定项位数组 IMAGE_BASE_RELOCATION ENDSVirtualAddress 是 Base Relocation Table 的位置它是一个 RVA 值SizeOfBlock 是 Base Relocation Table 的大小;这个结构的示意图如下:TypeOffset 是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址。高4位一般是3,表示这个地址是一个32位的地址,它与 VirtualAddress 相加即是指向PE 映像中需要修改的那个地址的位置,注意这里不是定位到对应代码的位置接下来进行手工的方式找到需要重定位的代码位置:打开一个dll文件,发现它的基址重定位表所在RVA = 0x00004000通过计算得到它是在.relo ,对应文件的偏移为0x800,查看这个位置的值为:VirtualAddress = 0x1000,SizeOfBlock = 0x18通过它的大小,得知需要重定位的位置主要有(0x18 - 8) / 2 = 8,最后一个以0结尾,所以实际上总共有7处需要重定位这8个位置分别为:0x028, 0x02e, 0x003e 0x04b, 0x051, 0x61, 0x6c后面的以此类推,可以发现这些需要重定位的位置,存储的都是一些立即寻址的地址
2017年03月22日
3 阅读
0 评论
0 点赞
2017-03-21
PE文件详解(七)
本文转载自小甲鱼PE文件讲解系列原文传送门这次主要说明导出表,导出表一般记录着文件中函数的地址等相关信息,供其他程序调用,常见的.exe文件中一般不存在导出表,导出表更多的是存在于dll文件中。一般在dll中保存函数名称以及它的地址,当某个程序需要调用dll中的函数时,如果这个dll在内存中,则直接找到对应函数在内存中的位置,并映射到对应的虚拟地址空间中,如果在内存中没有对应的dll,则会先通过PE加载器加载到内存,然后再进行映射导出表结构导出表(Export Table)中的主要成分是一个表格,内含函数名称、输出序数等。序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。在此我们不提倡仅仅通过序数来索引函数的方法,这样会给DLL 文件的维护带来问题。例如当DLL 文件一旦升级或修改就可能导致调用改DLL 的程序无法加载到需要的函数。数据目录表的第一个成员指向导出表,是一个IMAGE_EXPORT_DIRECTORY(以后简称IED)结构,IED 结构的定义如下:IMAGE_EXPORT_DIRECTORY STRUCT Characteristics DWORD ? ; 未使用,总是定义为0 TimeDateStamp DWORD ? ; 文件生成时间 MajorVersion WORD ? ; 未使用,总是定义为0 MinorVersion WORD ? ; 未使用,总是定义为0 Name DWORD ? ; 模块的真实名称 Base DWORD ? ; 基数,加上序数就是函数地址数组的索引值 NumberOfFunctions DWORD ? ; 导出函数的总数 NumberOfNames DWORD ? ; 以名称方式导出的函数的总数 AddressOfFunctions DWORD ? ; 指向输出函数地址的RVA AddressOfNames DWORD ? ; 指向输出函数名字的RVA AddressOfNameOrdinals DWORD ? ; 指向输出函数序号的RVA IMAGE_EXPORT_DIRECTORY ENDSName:一个RVA 值,指向一个定义了模块名称的字符串。如即使Kernel32.dll 文件被改名为”Ker.dll”。仍然可以从这个字符串中的值得知其在编译时的文件名是”Kernel32.dll”。NumberOfFunctions:文件中包含的导出函数的总数。NumberOfNames:被定义函数名称的导出函数的总数,显然只有这个数量的函数既可以用函数名方式导出。也可以用序号方式导出,剩下的NumberOfFunctions 减去NumberOfNames 数量的函数只能用序号方式导出。该字段的值只会小于或者等于 NumberOfFunctions 字段的值,如果这个值是0,表示所有的函数都是以序号方式导出的。AddressOfFunctions:一个RVA 值,指向包含全部导出函数入口地址的双字数组。数组中的每一项是一个RVA 值,数组的项数等于NumberOfFunctions 字段的值。Base:导出函数序号的起始值,将AddressOfFunctions 字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出 序号。假如Base 字段的值为x,那么入口地址表指定的第1个导出函数的序号就是x;第2个导出函数的序号就是x+1。总之,一个导出函数的导出序号等 于Base 字段的值加上其在入口地址表中的位置索引值。这个只是一个导出序号导出给外部进行使用的,当我们在分析PE文件进行相关函数的定址时,不使用这个序号,表中也没有存储函数的导出序号AddressOfNames 和 AddressOfNameOrdinals:均为RVA 值。前者指向函数名字符串地址表。这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。数组的项数等于NumberOfNames 字段的值,所有有名称的导出函数的名称字符串都定义在这个表中;后者指向另一个word 类型的数组(注意不是双字数组)。数组项目与文件名地址表中的项目一一对应,项目值代表函数入口地址表的索引,这样函 数名称与函数入口地址关联起来。(举个例子说,加入函数名称字符串地址表的第n 项指向一个字符串“MyFunction”。那么可以去查找 AddressOfNameOrdinals 指向的数组的第n 项,假如第n 项中存放的值是x,则表示AddressOfFunctions 字段描述的地址表中的第x 项函数入口地址对应的名称就是“MyFunction”他们的关系如图所示:一般在分析定位函数地址的时候采用的是通过函数名称来定位在定位时可以使用序号的方式,也可以使用函数名的方式来定位,使用序号需要提前知道这个函数对应的序号,这个非常困难,还要一种方式是采用函数名找到对应函数的序号,然后再通过序号定位,一般在进行定位时都是使用函数名进行定位从序号查找函数入口地址 定位到PE 文件头从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA从导出表的 Base 字段得到起始序号将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址从函数名称查找入口地址如果已知函数的名称,如何得到函数的入口地址呢?与使用序号来获取入口地址相比,这个过程要相对复杂一点!Windows 装载器的工作步骤如下:最初的步骤是一样的,那就是首先得到导出表的地址从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址一帮情况下病毒程序就是通过函数名称查找入口地址的,因为病毒程序作为一段额外的代码被附加到可执行文件中的。如果病毒代码中用到某些 API 的话,这些 API 的地址不可能在宿主文件的导出表中为病毒代码准备好。因此只能通过在内存中动态查找的方法来实现获取API 的地址。接下来就是来实际分析一个PE文件。通过之前的知识,发现这个导出表的RVA = 0x00002060,表所在节区为.rdat,节区在内存中的RVA = 0x00002000,节区在文件中的偏移 = 0x00000600。通过之前的计算公式得到导出表在文件中的偏移为0x00000660.定位到这个地方发现这个表中的内容如下:通过解析知道Name = 0x0000209c ==>0x0000069cBase = 0x00000002 NumberOfFunctions = 0x02NumberOfNames = 0x02AddressOfFunctions = 0x2088 ==>0x688AddressOfNames = 0x2090==>0x690AddressOfNameOrdinals = 0x2098 ⇒ 0x698对于保存的是RVA的变量,后面都是通过换算得到的其值在内存中的偏移对于AddressOfNames来说,它指向的是一个保存了函数名的RVA,我们在对应偏移位置得到它的值为0x20A8 ==> 0x6a8,从文件中的内容来看,这个位置保存到额正好是两个导出函数的值。两个函数名分别为:_DecCount ==>0_IncCount ==> 1后面的是它们在这个位置的编号,等会需要这个编号,中的它们在函数地址表中对应的索引接下来根据AddressOfNameOrdinals中的值,00 01,发现它们在函数地址表中的索引分别为0 1最后再AddressOfFunctions中得到它们分别为0x1046和0x1023也就是_DecCount = 0x1046 _IncCount = 0x1023我们通过反汇编工具W32Dasm,查看这个dll的反汇编代码:这个dll加载到内存中后它的基地址为0x10000000,这样得到两个函数在内存中的地址为:_DecCount =0x10001046_IncCount =0x10001023在它的反汇编中找到函数的地址发现正好是这两个值:
2017年03月21日
10 阅读
0 评论
0 点赞
1
...
11
12
13
...
19