1. 程式人生 > >Catch me if you can

Catch me if you can

本文的目的就是想探究dll檔案中的變數是如何匯出。藉此瞭解ntoskrnl.exe 的匯出到底是怎麼實現的。
在前面的《SSDT HOOK》程式碼段中有這麼一句話:

 extern "C"  PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable;

當時是說:這個符號是從ntoskrnl.exe 中匯出的。從當時測試的時候改變符號名發現執行錯誤就可以看出來,這個符號絕對是從ntoskrnl.exe 檔案中查找出來的。今天使用Depend walker 查看了一下,有這樣的結果。
這裡寫圖片描述

按鍵F10(c/c++ 符號形式) 轉換,發現都是同一個函式名,因此可以判定這個符號是用C匯出的。

這是因為如果使用的是c++符號形式匯出,那麼由於多型性的原因,其匯出格式會發生改變。

為此我又做了一次測試。新建dll檔案生成cpp 格式。原始碼如下:

FINAL.H
// The following ifdef block is the standard way of creating macros which make exporting 
// from a DLL simpler. All files within this DLL are compiled with the FINAL_EXPORTS
// symbol defined on the command line. this symbol should not be defined on any project
// that uses this DLL. This way any other project whose source files include this file see // FINAL_API functions as being imported from a DLL, wheras this DLL sees symbols // defined with this macro as being exported. #ifdef FINAL_EXPORTS #define FINAL_API __declspec(dllexport) #else #define FINAL_API __declspec(dllimport)
#endif // This class is exported from the FINAL.dll class FINAL_API CFINAL { public: CFINAL(void); // TODO: add your methods here. }; extern FINAL_API int nFINAL; extern FINAL_API int nTemp ; //add here,others are default input. FINAL_API int fnFINAL(void); -------------- // FINAL.cpp : Defines the entry point for the DLL application. // #include "stdafx.h" #include "FINAL.h" BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } // This is an example of an exported variable FINAL_API int nFINAL=0; FINAL_API int nTemp = 0x10; //add here. // This is an example of an exported function. FINAL_API int fnFINAL(void) { return 42; } // This is the constructor of a class that has been exported. // see FINAL.h for the class definition CFINAL::CFINAL() { return; }

程式很簡單,

這時候開啟depends 檢視一下匯出情況。
下圖是c++預設的匯出符號表(最左側的C++ 表示是通過C++方式匯出)
這裡寫圖片描述

F10切換後,去掉裝飾,不知道這個算不算變為C的方式呢?後期注意這個問題。
這裡寫圖片描述

雖然用depends 可以切換,但是我更想知道編譯器做了什麼,用PEView 可以檢視(匯出表name段在.rdata段)。
這裡寫圖片描述
由於PEView 不能複製結果出來,在這裡我用十六進位制檢視器(Winhex)查看了一下,結果如下。

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

0002CF80   41 45 40 58 5A 00 3F 3F  34 43 46 49 4E 41 4C 40   [email protected] ??4[email protected]
0002CF90   40 51 41 45 41 41 56 30  40 41 42 56 30 40 40 5A   @[email protected]@@Z
0002CFA0   00 3F 66 6E 46 49 4E 41  4C 40 40 59 41 48 58 5A    [email protected]@YAHXZ
0002CFB0   00 3F 6E 46 49 4E 41 4C  40 40 33 48 41 00 3F 6E    [email protected]@3HA ?n
0002CFC0   54 65 6D 70 40 40 33 48  41 00 00 00 00 00 00 00   [email protected]@3HA       

可以看到,編譯連結後的檔案中存放的變數符號為[email protected]@3HA .

接著改變連結方式為Extern "C" .

FINAL.H 其他不變
//extern FINAL_API int nTemp;
extern "C" FINAL_API int nTemp ;

生成的檔案結果是:

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

0002CF60   00 00 01 00 02 00 03 00  04 00 46 49 4E 41 4C 2E             FINAL.
0002CF70   64 6C 6C 00 3F 3F 30 43  46 49 4E 41 4C 40 40 51   dll ??0[email protected]@Q
0002CF80   41 45 40 58 5A 00 3F 3F  34 43 46 49 4E 41 4C 40   [email protected] ??4[email protected]
0002CF90   40 51 41 45 41 41 56 30  40 41 42 56 30 40 40 5A   @[email protected]@@Z
0002CFA0   00 3F 66 6E 46 49 4E 41  4C 40 40 59 41 48 58 5A    [email protected]@YAHXZ
0002CFB0   00 3F 6E 46 49 4E 41 4C  40 40 33 48 41 00 6E 54    [email protected]@3HA nT
0002CFC0   65 6D 70 00                                        emp 

可以看到,變數符號名並沒有改變nTemp .這就是C宣告的意思,不改變原變數名。從上面的dump中可以看出來,函式名C++生成的也不是原有的函式名,那麼在C下呢,我又試了一次:

//Final.h   the last line.
//add here
extern "C" FINAL_API int sum(int a,int b);

-----
//FINAL.CPP末尾新增
FINAL_API int sum(int a,int b)
{
    return a + b;
}

生成的dll檔案用Depends 檢視。
這裡寫圖片描述

F10一下後
這裡寫圖片描述

depends E 欄中 c 表示用C的方式生成,C++表示用C++的方式生成。

看到函式宣告只剩下了函式名。
但是F10自由切換,還是不知道檔案中到底是怎麼樣的,看一下。

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

0002DFA0   56 30 40 41 42 56 30 40  40 5A 00 3F 66 6E 46 49   [email protected]@@Z ?fnFI
0002DFB0   4E 41 4C 40 40 59 41 48  58 5A 00 3F 6E 46 49 4E   [email protected]@YAHXZ ?nFIN
0002DFC0   41 4C 40 40 33 48 41 00  6E 54 65 6D 70 00 73 75   [email protected]@3HA nTemp su
0002DFD0   6D 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   m               
0002DFE0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00                   

可以看到就是depends中的sum (因為來回切換都沒有變–)

來看下如何匯入這些變數

#include "stdafx.h"
#include <stdio.h>
#pragma comment(lib,"FINAL.lib")

extern "C" int _declspec(dllimport) nTemp;
int main(int argc, char* argv[])
{
//  printf("%d",nMydll);

 printf("%d",nTemp);
    return 0;
}

//output: 16Press any key to continue.

如果將extern "C" int _declspec(dllimport) nTemp; 中的C 去掉,會怎麼樣。

--------------------Configuration: mydll_test - Win32 Debug--------------------
Linking...
mydll_test.obj : error LNK2001: unresolved external symbol "__declspec(dllimport) int nTemp" ([email protected]@3HA)
Debug/mydll_test.exe : fatal error LNK1120: 1 unresolved externals
執行 link.exe 時出

其實這個錯誤就是說找不到符號,上面實驗做完應該知道我們檔案是.cpp ,那麼就會按照c++的方式去找:
1. 首先,編譯器將nTemp 轉化為C++下的符號[email protected]@3HA
2. 然後 ,通過PE檔案結構找到符號[email protected]@3HA ,接著讀出它的RVA。

問題是,我們的DLL檔案根本就沒有這個符號,用C的方式生成的符號是nTemp 。怎麼找都找不到。

而最開始的例子就已經說明我要找的變數是用C方式匯出的,讓編譯器將第一步用C的方式生成nTemp ,這次當然就可以找成功了。

最後來看下反彙編過程。

13:    printf("%d",nTemp);
00401028   mov         eax,[__imp__nTemp (0042a18c)]
0040102D   mov         ecx,dword ptr [eax]
0040102F   push        ecx
00401030   push        offset string "%d" (0042201c)
00401035   call        printf (00401060)
0040103A   add         esp,8

對於__imp__nTemp 我的解釋是:首先這個是匯入進來的,加字首__imp__ ,然後加入符號名nTemp 。注意匯入表能得到的永遠是地址,而不是值。也就是說匯入nTemp 得到的是nTemp值的地址,要取值nTemp必須通過從地址中取出dword。否則彙編就變成了mov eax, __imp__nTemp,或者說有&ntemp=__imp__nTemp=42a18c.

同樣的,用C++的方式形成的反彙編是:

13:    printf("%d",nTemp);
00401028   mov         eax,[[email protected]@3HA (0042a18c)]
0040102D   mov         ecx,dword ptr [eax]
0040102F   push        ecx
00401030   push        offset string "%d" (0042201c)
00401035   call        printf (00401060)
0040103A   add         esp,8

字首為__imp_ 少了一個下劃線。

研究做完了,現在有幾個問題待解決:
***

extern FINAL_API int nFINAL;
//extern FINAL_API int nTemp;
extern int FINAL_API  nTemp ;

這兩個宣告都能通過,一個int在前,一個int在後,區別是什麼?

***
最初的那個問題:

 extern "C"  PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable;

這句宣告中沒有用__declspec(dllimport) .為什麼還可以獲得此值呢。

如果普通都可以的話,那麼我測試程式改成這樣,為什麼就不能呢。是因為核心的原因麼。

#pragma comment(lib,"FINAL.lib")

extern int  nTemp;         //no __declspec(dllimport) --編譯失敗
int main(int argc, char* argv[])
{
//  printf("%d",nMydll);

 printf("%d",nTemp);
    return 0;
}

***
最後一點:

dll匯出檔案FINAL.H中的巨集定義
#ifdef FINAL_EXPORTS
#define FINAL_API __declspec(dllexport)
#else
#define FINAL_API __declspec(dllimport)
#endif

在哪裡測試是否已經define 了 FFINAL_EXPORTS .絕對是巨集定義了,以下為例:

#ifdef FINAL_EXPORTS
#define FINAL_API __declspec(dllex444444444port)
#else
#define FINAL_API __declspec(dllimport)
#endif

修改dllimport 不錯,但是一旦修改dllexport 就出錯,說明絕對是巨集定義了。
那麼到底是在哪裡巨集定義的呢,編譯器自動搞的??????????