主頁 > 企業開發 > 逆向基礎知識-匯編和PE檔案

逆向基礎知識-匯編和PE檔案

2022-10-06 08:53:37 企業開發

匯編基礎知識

1. 九個暫存器(32位)

暫存器                                                                       編號
eax:累加器(accumulator), 它是很多加法乘法指令的預設暫存器,                        0
ecx:計數器(counter), 是重復(REP)前綴指令和LOOP指令的內定計數器,                1
edx:總是被用來放整數除法產生的余數,                                           2
ebx:"基地址"(base)暫存器, 在記憶體尋址時存放基地址,                              3
esp:存盤堆疊的最頂端                                                         4
EBP:是"基址指標",不是必須的                                                    5
esi:                                                                      6
edi:變址暫存器,主要用于存放存盤單元在段內的偏移量,作為通用暫存器,也可存盤算術邏輯運算的運算元和運算結果,它們可作一般的存盤器指標使用,                                                                      7
eip:存盤當前CPU馬上要執行的指令
暫存器32位可以拆分為兩個16位暫存器 eax拆分成ax(低16位)  ax還可以拆分為ah(高位)和al(低位)

2. 八個標志位

CF(進位標志位):運算時最高位產生進位或借位時為1        #  針對無符號數的運算
ZF(零標志位):若當前運算結果為0,標志位為1    xor eax,eax ZF置1   mov eax,0 不會修改標志位的值
SF(符號標志位):該標志位與運算結果二進制的最高位相同,運算結果為負,則標志位為1    
OF(溢位標志位):如果運算結果超過了機器能表示的范圍則標志位為1    # 針對有符號數的運算 正+正=負 / 負+負=正 表示溢位
                符號位有進位:1,最高有效位有進位:1        最終OF位為1 xor 1 = 0 (計組的溢位判斷)
PF(奇偶標志位):運算結果的最低有效位元組中(即低八位)含1的個數為偶數則標志位為1    
AF(輔助進位標志):運算結果的低4位向高4位有進位或借位時為1
次要
TF(跟蹤標志):為方便程式除錯而設定,若TF=1,則CPU處于單步作業方式,在每條指令結束后產生中斷
DF(方向標志位):用來控制串處理指令(movsd)的處理方向,DF為1則串處理程序中地址自動遞減,否則自動遞增

image-20220312100258130

溢位判斷??

1.無符號,有符號都不溢位         al為8位     無符號超過ff溢位,CF位為1  有符號數兩個小于7f的結果超過7f溢位,OF位為1
mov al,8
add al,8
2.無符號溢位,有符號不溢位      當成有符號數后 ff為負數,正數+負數當然不會溢位
mov al,0ff
add al,2
3.無符號不溢位,有符號溢位      有符號數 7f + 2 = 81  兩個整數相加結果為負數  所以溢位
mov al,7f
add al,2
4.無符號,有符號都溢位    兩個正數相加成了負數
mov al,0FE
add al,80

image-20220417134417409

image-20220417134558972

3. 匯編指令

對暫存器進行操作比較簡單
mov eax,0x1 --將1存入eax    add eax,0x2 --eax+2存入eax        注意前后兩個運算元不可同時為記憶體單元
對記憶體進行操作要給出所占記憶體的空間    word---16位  dword---32位 
mov word ptr ds:[0x12345678],0x1234  --ds:為段暫存器,將1234寫入記憶體地址為0x12345678(記憶體編號-32位)的位置
LEA eax,dword ptr ds:[0x10ffcc]    --將ds所指的地址編號(10ffcc)取出存入到暫存器eax中
記憶體尋址方式
1.直接尋址:mov word ptr ds:[0x12345678],0x1234
2.暫存器尋址:mov ecx,12345678    mov eax,dword ptr ds:[ecx]    --先將地址存到暫存器里,然后利用暫存器尋址找到記憶體地址 
3.暫存器+立即數
mov ecx,12345678   mov eax,dword ptr ds:[ecx+4]        --將ecx所存地址+4后的記憶體單元的值存到eax里
mov ecx,12345678   lea eax,dword ptr ds:[ecx+4]        --將ecx所存地址+4的記憶體地址存到eax里
4.[reg+reg*{1,2,4,8}]
mov eax,13ffc4   move ecx,2        mov edx,dword ptr ds:[eax+ecx*4]
5.[reg+reg*{1,2,4,8}+立即數]
mov eax,13ffc4   move ecx,2        mov edx,dword ptr ds:[eax+ecx*4+8]
讀取堆疊的資料 --兩種方式
1、base加偏移  堆疊底為高地址
讀第一個壓入的資料:mov esi,dword ptr ds:[ebx-4]
讀第四個壓入的資料:mov esi,dword ptr ds:[ebx-0x10]
2.top加偏移    堆疊頂為低地址
讀第二個壓入的資料:mov edi,dword ptr ds:[edx+8]    
讀第三個壓入的資料:mov edi,dword ptr ds:[edx+4]
入堆疊(edx為堆疊頂,ebx為堆疊底)  以下三種方式都可以(往往會把push pop 隱藏,要學會看這些識別)
1.sub edx,4                                  mov dword ptr ds:[edx],0x11111111
2.mov dword ptr ds:[edx-4],0x11111111        sub edx,4    
3.lea edx,dword ptr ds:[edx-4]              mov dowrd ptr ds:[edx],0x11111111
系統默認堆疊中esp為堆疊頂,ebp為堆疊底
push 0x12345678     --將12345678入堆疊,同時將top-4
push ax        --將暫存器ax的值入堆疊,top-2  (不可push進8位的)

出堆疊操作:  以下都可以
1.mov eax,dword ptr ds:[edx]        add edx,4
2.lea edx,dword ptr ds:[edx+4]        mov eax,dword ptr ds:[edx-4]    
pop eax    --堆疊頂出堆疊,將出堆疊的元素存到eax里
pop ax     --堆疊頂出堆疊,將出堆疊元素的低16位存進ax里

pushad --將八個通用暫存器的值全部存入堆疊,先存eax,              esp和ebp不要修改,系統分配的
popad  --將堆疊中八個值出堆疊,存入八個通用暫存器中

其他指令

xor eax,eax  --將eax置零

adc R/M,R/M/IMM     帶進位加法,結果加上CF位   兩邊不能同時為記憶體    R為暫存器,M為記憶體,IMM為立即數  
sbb R/M,R/M/IMM        帶借位減法,結果減去CF位     兩邊不能同時為記憶體        

xchg R/M,R/M        交換資料,不能出現立即數

movs指令  --用于在記憶體與記憶體之間移動資料  很有可能是一段字串的復制
mov edi,0x11111      mov esi,0x11112      --先指定兩個變址暫存器的地址
movs dword ptr es:[edi],dword ptr ds:[esi]    --在記憶體與記憶體間移動資料    簡寫為movsd 移動四個位元組
movs word ptr es:[edi],dword ptr ds:[esi]        --簡寫為movsw 移動兩個位元組
注意:DF位為0,移動完后esi和edi存的地址編號都會同時加4/2    DF位為1,移動后esi和edi所存的地址同時減4或2
stos指令  --置數,大小由前面的 dword/word/byte決定
stos dword/word/byte ptr es:[edi]    將eax/ax/al的值存盤到[edi]指定的記憶體單元,簡寫為stosd,移動后同樣地址會改變
注意:當出現edi時  段暫存器往往用es

mov ecx,10(ecx中存盤操作要重復的次數)        
rep movsd    --重復執行從esi先edi移動的指令16次,由于edi和esi執行完后會自動 +4 或者 -4        
rep stosd

修改eip不可用mov:可用以下指令
1.格式:jmp reg/imm     jmp 0x123456    當跳轉的位置距離當前小于128個位元組 最終指令為:jmp short 0x123456
jmp的作用,修改eip,然后cpu按eip執行對應地址
2.call 0x123456     注意要在call后的地址下斷點    call后esp會減4,將call的回傳地址的下一地址壓堆疊(call地址+硬編碼)
  retn   call后要回傳    相當于pop eip,esp會加4    ret 8    --兩條指令,一個rtn,一個esp+8

比較指令
cmp R/M,R/M/IMM  比較兩個運算元,實際上相當于sub指令,只是相減的結果不保存在第一個運算元中,根據相減的結果改變零標志位,當兩個運算元相等時,零標志位置1,只改變ZF標志位,當相減結果小于0時,符號標志位SF變為1
test R/M,R/M/IMM,一定程度上與cmp相似,兩個數進行與操作,結果不保存,但是會改變相應的標志位
常用于判斷一個數是否為空,若為空,自身相與之后仍未空

移位指令
SAL/SAR Reg/Mem,CL/IMM        --算術左移/右移,  10000001右移1位 1100 0000
正數,三碼相同,所以無論左移還是右移都是補0.而負數的補碼就需要注意,左移在右邊補0,右移需要在左邊補1 
SHL/SHR Reg/Mem,CL/IMM        --邏輯左移/右移    
## C語言中往左移有無符號沒有區別,往右移需要注意符號位  
ROL/ROR Reg/Mem,CL/IMM        --回圈左移/右移    算術右移會將右邊溢位的位移到左邊的新的位,CF與溢位的為一樣 rotate
RCL/RCR Reg/Mem,CL/IMM        --帶進位左移/右移    把CF位當做自己的一部分

image-20220418134903327

image-20220418134920107

JCC指令 中文含義 檢查符號位
JZ/JE 若為0則跳轉;若相等則跳轉 ZF=1
JNZ/JNE 若不為0則跳轉;若不相等則跳轉 ZF=0
JS 若為負則跳轉 SF=1
JNS 若為正則跳轉 SF=0
JP/JPE 若1出現次數為偶數則跳轉 PF=1
JNP/JPO 若1出現次數為奇數則跳轉 PF=0
JO 若溢位則跳轉 OF=1
JNO 若無溢位則跳轉 OF=0
JC/JB/JNAE(無符號數) 若進位則跳轉;若低于則跳轉;若不高于等于則跳轉 CF=1
JNC/JNB/JAE(無符號數) 若無進位則跳轉;若不低于則跳轉;若高于等于則跳轉; CF=0
JBE/JNA(無符號數) 若低于等于則跳轉;若不高于則跳轉 ZF=1或CF=1
JNBE/JA(無符號數) 若不低于等于則跳轉;若高于則跳轉 ZF=0或CF=0
JL/JNGE(有符號數) 若小于則跳轉;若不大于等于則跳轉 SF != OF
JNL/JGE(有符號數) 若不小于則跳轉;若大于等于則跳轉; SF = OF
JLE/JNG(有符號數) 若小于等于則跳轉;若不大于則跳轉 SF != OF 或 ZF=1
JNLE/JG(有符號數) 若不小于等于則跳轉;若大于則跳轉 SF=0F 且 ZF=0

4. 堆疊圖

首先將函式呼叫所用到的引數壓堆疊,然后call陳述句執行后首先將回傳的地址壓堆疊,之后會開辟出一段緩沖區用于存盤區域變數,一般會將緩沖區的垃圾資料全部初始化為相同的數,mov eax,0xCCCCCCCC     rep stosd    --用于初始化緩沖區
在函式外(return后)原來的地址進行平衡堆疊(使esp和ebp指向同一地址),將引數出堆疊        --稱為外平堆疊 add exp,0x8(由引數個數決定)

5. C語言與匯編

1、C語言中定義一個沒有任何操作的函式,在編譯時仍會生成固定的出入堆疊操作的匯編代碼,若不想生成,可定義裸函式

int __declspec(naked) plus(){
    __asm{
        //在函式呼叫之前會先push 1 push 2(傳引數)  call后會執行  push 回傳地址
         //保留呼叫前的堆疊底
        push ebp
        //提升堆疊
        mov ebp,esp
        sub esp,0x40
        //保留現場
        push ebx
        push esi
        push edi
        //填充緩沖區        主要用于存盤函式的區域變數
        mov eax,0xcccccccc
        mov ecx,0x10                     // 之所以是10 是因為之前提升堆疊0x40 / 4 = 10  堆疊一個格四個位元組
        lea edi,dword ptr ds:[ebp-0x40]
        rep stosd //每次填充四個位元組,重復16次
        //函式的核心功能
        mov eax,dword ptr ds:[ebp+0x8]      //把第一個引數給eax  ebp+0x4為函式回傳地址
        add eax,dword ptr ds:[ebp+0xc]        //第二個引數 + eax -> eax
        //恢復現場
        pop edi        //取出堆疊頂給edi,然后esp+4
        pop esi           //取出堆疊頂給esi,然后esp+4
        pop ebx           //取出堆疊頂給ebx,然后esp+4
        //降低堆疊
        mov esp,ebp
        pop ebp        //恢復堆疊底,剛開始ebp保留過
        ret            //相當于pop eip 把函式回傳地址401171給eip
    }
}        //裸函式,系統不會生成任何指令,呼叫時會出錯,會導致指令跳轉后回不來,往往需要自己寫入匯編指令

image-20220417160716094

IEEE規范??

IEE754 規定浮點數階碼 E 采用"指數e的移碼-1"來表示,請記住這一點,為什么指數移碼要減去 1,這是 IEEE754 對階碼的特殊要求,以滿足特殊情況,比如對正無窮的表示,

對于階碼為 0 或 255 的情況,IEEE754標準有特別的規定:

如果 階碼 E=0 并且尾數 M 是 0,則這個數的真值為 ±0, 正負號和數符位有關,

如果階碼 E=255 并且尾數 M 全是0,則這個數的真值為 ±∞(同樣和符號位有關)

階碼E = 指數e的移碼-1 等價于 E = e + 127

移碼(又叫增碼)是對真值補碼的符號位取反,一般用作浮點數的階碼,引入的目的是便于浮點數運算時的對階操作,

image-20220417165600769

呼叫約定

呼叫約定 引數壓堆疊順序 平衡堆疊
__cdecl(c與c++默認) 從右至左入堆疊 呼叫者清理堆疊(外平堆疊)
__stdcall 從右至左入堆疊 自身清理堆疊(內平堆疊)
__fastcall ECX/EDX傳送最左的兩個(速度快),剩下從右至左入堆疊 自身清理堆疊(內平堆疊)

尋找程式入口??

VC的入口點(IDA的start函式,會直接呼叫main()函式,在start()函式中被呼叫的函式有三個引數的,并且回傳值被傳入exit()函式的,可以重點查看)

main函式有三個引數,在一些編譯器自己執行的函式下找有三個引數呼叫的函式call(往往有三個push指令)

注:有符號數與無符號數對于計算機記憶體存盤沒有任何區別,
char i = 0xff;                     //對應有符號數-1
unsigned char k = 0xff;            //對應無符號數255        
//再進行型別轉換--比較大小和數值運算時應特別注意是有符號數還是無符號數

全域變數(基址)識別

mov eax,byte/word/dword ptr ds:[0x12345678]    [arr (0x1123456)],eax    --將eax直接賦值給某塊地址,往往為全域變數 
可以通過暫存器的寬度確定全域變數型別

變數個數識別

image-20220314102854156

注:C語言中if陳述句中若為 > 在匯編中的JCC陳述句為jle(小于等于),因為不滿足小于等于才會執行下面的指令

IF..ESLE IF陳述句判斷

1、每個條件跳轉指令要跳轉的地址前面都有一個jmp指令(另一個判斷陳述句的jmp)
2、這些jmp指令跳轉的地址都是一樣的(跳轉到函式執行后的部分)    (if else陳述句只能執行一次)
3、如果某個分支沒有條件判斷,則為else部分

陣列匯編代碼辨認

int x = 1;            --mov dword ptr [ebp-4],1
int b = arr[x];        --mov ecx,dword ptr [ebp-4]     
                    mov edx,dword ptr [ebp+ecx*4-34h]     #34h為剛開始定義的陣列大小最頂端地址
二維陣列與一維陣列對于記憶體存盤來說沒有區別
int arr[3][4]    要取arr[1][2]  對于編譯器來說即arr[1*12+2]    
    匯編 mov edx,dword ptr [ebp+1*4*4+2-34h]  行號*每行元素個數*4+列號*4
void f(int arr[])
{
    cout << "陣列傳參中size為" << sizeof(arr);     // 8  64位系統傳入的的指標寬度為8位元組
}
int main(int argc, char const *argv[])
{
    int arr[5] = {1, 2, 3, 4, 5};
    /*int arr[1] = {1};
    cout << "main中size為" << sizeof(arr) << " "; // 20
    f(arr);*/
    return 0;
}

位元組對齊/結構體對齊??

本質:效率還是空間,二選一的結果

用法 #pragma pack(n)    后跟結構體的定義
對齊引數:n為位元組對齊數,其值為1、2、4、8,默認為8,如果這個值比結構體成員的sizeof值小,name該成員的偏移量以此值為準,也就是說,結構體成員的偏移量取二者中的最小值,比如說規定位元組對齊數為4,結構體內有_int64型別(8位元組),結果仍按四位元組對齊
#pragma pack(8)
struct test
{
    int a;
    _int64 b;
    char c;
};
//8位元組對齊的話,輸出sizeof(test) 為 8 * 3 = 24
//4位元組對齊,輸出sizeof(test)為 4 + 4 * 2 +4 = 16

image-20220417171705881

原則一:資料成員對齊規則:結構的資料成員,第一個資料成員放在offset()為0的地方,以后每個資料成員存盤的起始位置要從該成員大小的整數倍開始,如上圖8位元組對齊,雖然a占了四個位元組,b的大小為8個位元組,所以b從8開始存盤
原則二:結構體作為成員:如果一個結構里有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存盤(struct a里有struct b,b里有char int double 等元素那b應該從8的整數倍開始存盤)
原則三:收尾作業,結構體的總大小,也就是sizeof的結果,必須是內部最大成員大小的整數倍,不足的要補齊
這三個原則具體怎樣理解呢?我們看下面幾個例子,通過實體來加深理解,

struct test{
    char c;
    //int a;
    double b;
};
int main(){
    cout << sizeof(test);  
    //由于double為8個位元組,最終結果為8的整數倍,16位元組,如果加上int a,結果仍為8,a可占原來c補齊的位置
    //char c;下面在加char c1; char c2; char c3;  結果仍為16,注意順序,要放在int a前面
}
struct {
   short a1;
   short a2;
   short a3;
}A;
struct{
    long a1;
    short a2;
}B;
sizeof(A) = 6; 這個很好理解,三個short都為2,
sizeof(B) = 8; 這個比是不是比預想的大2個位元組?long為4,short為2,整個為8,因為原則3,

建議:書寫時按照資料型別從小到大的順序書寫,節省記憶體空間

Switch陳述句識別??

當switch陳述句的case分支小于4時,生成的匯編與if..else 類似,分支大于等于 4 時,編譯器會生成一張大表,存盤多個分支的case結果,前提是case的值要連續,差值不可太大,差值大于256時按if-else處理,當傳入引數x時,首先會對引數進行減1(case分支最小的數),然后根據這個數用統一的運算式進行尋址決定后續的case陳述句

switch(x):
{
    case 1:
        cout<<1;
    case 2:
        cout<<2;
    case 3:
        cout<<3;
    case 4:
        cout<<4;
    default:
        cout<<"error";
}
//switch生成大表的反匯編,不用一個一個判斷,大大節省效率,在游戲加密中經常會用到
mov eax,dword ptr [ebp-8]            (傳進來的引數)
mov dword ptr [ebp-4],eax            (引數傳給區域變數)
mov ecx,dword ptr [ebp-4]            (取出區域變數)
sub ecx,1                            (減去case判斷中的最小的數決定偏移量)     --重點關注
mov dword ptr [ebp-4],ecx            (把減后的結果存到區域變數中)
cmp dword ptr [ebp-4],3                (將減后的結果與3比較,其實就相當于判斷x是否大于4)
ja 0040d826h                        (結果大于3的話直接執行default)
mov edx,dword ptr [ebp-4]            (將減1的結果賦給edx)
jmp dword ptr [edx*4+40D841h]        (根據偏移量,決定每個case的跳轉地址,)  40D841h為為創建的大表的起始地址

image-20220417174500257

注:case的值的順序對生成的匯編代碼無影響,如果洗掉case 2,則生成的大表仍會保留,編譯器會把default的地址寫到case2對應的偏移地址,再洗掉一個case,該地址也會填充default

如果case的值相差較多但小于256,會生成一個小表

當不斷刪去中間的值時,不洗掉最大最小值,因為減去的是最小值,編譯器最侄訓生成一張小表,地址位于緊挨著大表之下,

傳入引數x時,編譯器會先對小表進行判斷,將偏移量存到 dl 里,再根據 dl 去大表中尋找地址 --可節省記憶體空間

//生成的小表只有一個位元組,最多存256,所以case陳述句的偏移值不可以太大
// 創建小表時,通過小表把中間不連續的值相對于case最小值的偏移做了優化  多了如下的匯編  小表地址004010d0
mov eax,dword ptr [ebp-4]     將傳入的引數的值相對于case的最小值的偏移給了ecx后又給了eax
xor edx,edx                    (將edx清零)    
mov dl,byte ptr (004010d0)[eax]    --相當于mov dl,byte ptr[004010d0+eax] //去小表中查值給了dl
//比如說case:301  .... case:308,case:309  傳入308,根據這項操作會把dl的值變為01,查大表的時候直接 40108ch+4就可以了  
jmp dword ptr [edx*4+40108ch]        --根據小表得到的 dl 去大表中找case陳述句執行后的
//如下圖小表中的04給了dl          04*4 +40108ch就是default的地址

image-20220417182929163

指標??

指標加減
char* a;                    //指標只要做加減運算就是按 去掉一個*后的資料型別來計算
char* b;
a = (char*)100;
b = (char*)200;
int x = b- a;               //只有相同型別才可以做加減,*的數量相同  這里去掉一個*后,x = (200-100)/1=100
printf("%d\n",x);     

char** a;                    //指標只要做加減運算就是按 去掉一個*后的資料型別來計算
char** b;
a = (char**)100;
b = (char**)200;
int x = b- a;  // 這里去掉一個*后,x = (200-100)/4=25  64為下指標的寬度為8位
printf("%d\n",x);

long *a;
long *b;
long c;
a = (long *)200;
b = (long *)100;
int x = a - b;
printf("%d\n", x);    //(200 - 100)/4 = 25
printf("%d\n", sizeof(c));     //long 的寬度為4    long long 為8
多維指標加減
char* p1;
char** p2;

printf("%d\n",*p1);    //mov eax,dword ptr [ebp-4]        mov cl,byte ptr [eax]    mov byte ptr[ebp-20h],cl
printf("%d\n",*(p1+2));    //mov edx,dword ptr [ebp-4]   movsx eax,byte ptr [edx+2](char為1位元組)   push eax;
printf("%d\n",*p2);        //與上面匯編無異
printf("%d\n",*(*p2)); //mov eax,dword ptr [ebp-8]   mov ecx,dword ptr [eax]
                       //movsx edx,byte ptr [ecx](把暫存器里的值當地址取值)    push edx
printf("%d\n",*(*(p2+1)+1)); //p2+1的型別char** 計算時先去掉x再決定偏移,*(p2+1)為+4,而*(p2+1)+1為加1 
指標型別轉換
int x = 1;
char y = 'a';
x = (char)97;        
y = x;
cout << x << "  " << y;  //97  a

int* x;
char* y;
y = (char*)10;        //char* 與 int* 是可以互相轉換的
x = (int*)y;
指標與匯編
int x;               //區域變數的宣告不會生成匯編代碼
x = 10;               
--mov dword ptr [ebp-4],0Ah    把0A賦給ebp-4的位置(緩沖區)
int *px = &x;        
--lea eax,[ebp-4]              取出變數x的地址  ebp-4的位置,這里是lea
--mov dword ptr [ebp-8],eax    將變數x的地址給px,緩沖區的 ebp - 8
int x1 = *px;                  
--mov ecx,dword ptr [ebp-8]    取出ebp-8的地址即px指向的x的地址放到ecx里
--mov edx,dword ptr [ecx]      把ecx里存的px指向的地址里存的數即x放到edx里  現在edx存的就是x
--mov dword ptr [ebp-0Ch],edx  把取出來的*px放到緩沖區 ebp-0Ch的地址
結構體指標
struct Arg{
   int a1;
   int a2;
   int a3;
};
Arg*** pArg;
pArg = (Arg*)100;     //型別強轉 ,以下不同陳述句均按pArg = (Arg* 100)為初始值
pArg++;                //做運算時去掉一個*,按剩下的型別長度計算,砍掉*后仍為*型別,四位元組
printf("%d\n",pArg);    //結果為104

Arg* pArg;
pArg++;    
printf("%d\n",pArg);    //結果為112
pArg = pArg + 5;        //結果為100 + 5*12

Arg* pArg2;
pArg2 = (Arg*)20;
int x = pArg - pArg2;    //相減結果為int型別,最終結果為(100 - 20) / 12 = 6

Arg s;
s.a1 = 10;        //mov dword ptr [ebp-0Ch],0Ah
s.a2 = 20;        //mov dword ptr [ebp-8],14h
s.a3 = 30;        //mov dword ptr [ebp-4],1Eh
Arg* px = &s;    //lea eax,[ebp-0Ch](取第一個值a1的地址)  mov dword ptr [ebp-10h],eax(將a1地址作為存放結構體s的地址)
printf("%d %d %d\n",px->a1,px->a2,px->a3);//若a2 a3 為char或short型,編譯器會自動根據其型別的寬度先后尋找
                                        //注意char型只有一個位元組,其值不可超過127
int x = 10;
Arg* px = (Arg*)&x;    //同樣可以,編譯器只存x的地址,若要取px->a2,其會自動在x后數四個位元組作為a2的值,與它是不是結構體無關
cout << px->a1 << "   " << px->a2 << "  " << px->a3;
//結果為10   7339524  0  第一個值正常取,后面的不一定
陣列指標與指標陣列
int (*px)[5];                        //陣列指標      int* px[5];  為指標陣列,本質是陣列,陣列的內容為int*型別
printf("%d\n",sizeof(px));            //結果為4
px = (int (*)[5]) 10;                //強轉
printf("%d\n",px);                    //10
px++;                                // 10 + 5 * 4 = 30
printf("%d\n",px);        

int arr[5]={1,2,3,4,5};
int (*px)[2];
px = (int (*)[2])arr;            //px為陣列指標的首地址
printf("%d %d\n",px,*px);        //px與*px的值一樣,但型別不同  *px為arr陣列首地址,而px為存放陣列首地址的地址
printf("%d %d\n",px++,*px);        //px++加的為 4 * 2 = 8,去掉 *后為 int [2]
printf("%d %d\n",px,(*px)+1);    //*px + 1 為 + 4   *px為arr,加的為指向的陣列型別的每個元素的寬度,    
printf("%d\n",*(*(px+1)+1));//*(px+1) 加4*2=8個位元組,指向arr的3的地址,之后再往后移四個位元組,結果為4,等價于px[1][1]    
printf("%d\n",*(*(px+4)+5));  //(8*4+5*4)/4 =13 如果arr有40個元素,這樣訪問的就是第14個元素    

用int型指標訪問char型陣列,將四個連續的值作為一個int型指標的基本資料單元
char ch[20] = {0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF,0x10,0x11,0x12,0x13,0x14};
int *px[2][3];
px = (int (*)[2][3]ch);  //型別強轉
printf("%x\n",*(*(*(px+1)+2)+2)); 最內層 1*(2*3)*4 次內層2*3*4  外層2*4
printf("%x\n",*(*(*(px)+1)+1)); 1*3*4 + 1*4 = 16  向后偏移16個元素后開始數四個數,小端存盤結果即為該值 0x14131211
函式指標
//函式指標
int func(int x,int y){
    return x + y;
}    
int main(){
    int (*pFunc)(int,int);
    pFunc = (int (*)(int,int))func;  //等價于 pFunc = func;
    int x = pFunc(1,2);
    printf("%d\n",x);        //結果為3
}
宏定義
#define MAX(A,B) ((A)>(B)?(A):(B))
  • 宏是在編譯前直接做了替換
  • 宏名識別符號與(A,B)之間不允許有空格
  • 宏與函式的區別:函式需要分配額外的空間,宏只是做了替換
  • 為了避免出錯,宏定義往往給形參加上括號
  • 末尾不需要分號
#ifdef和#endif
  • 用于注釋掉一段代碼

我們自己撰寫程式的時候,需要看到一系列的除錯代碼,但是發給客戶的時候,客戶可不希望看到什么什么OK的代碼,所以我們希望能很容易地注釋掉這段代碼,
這時需要用到預處理指令 #ifdef 和 #endif :

#include <stdio.h>
#define CONFIG_DEBUG 
int main(){
    FILE *fp;
    fp=fopen("D:\\DEV\\test.txt","r"); 
    if(NULL==fp){
        printf("error!");
    }
#ifdef CONFIG_DEBUG 
    printf("open test.txt ok");
#endif
    return 0;
}
  • 防止頭檔案重復包含
a.h檔案如下
#include <stdio.h>
#include "b.h"
b.h檔案如下
#include "a.h"
c.h
#include "a.h"
#include "b.h"
int main(){
    printf("Hello!");
}

程式是這樣寫的話,編譯器就會出現Error #include nested too deeply的錯誤,因為這里 b.h 和 a.h 都互相包含,c.c檔案在include的時候重復include了a.h,我們希望c.c檔案中執行#include "b.h"的時候 b.h 能進行判斷,如果沒有#include "a.h"則include,如果已經include了,則不再重復定義,

把 b.h修改成以下內容

#ifndef _A_H
#define _A_H 
#include "a.h"
#endif
記憶體磁區

常量區資料不可修改

void func(){
    char *x = "china";        //x放在堆疊區,存放的是指向china的地址,china存在常量區  所以x指向的字串不可以修改
    char y[] = "china";          //與上同樣,但是會從常量區把china拷貝到堆疊里,可以修改,    
}
char *x = "china";        //x放在堆疊區,存放的是指向china的地址,china存在常量區  所以x指向的字串不可以修改
char y[] = "china";    
void func2(){
    *(x+1) = 'A';    //不可以修改,china存在常量區
    y[1] = 'A';      //可以修改,該字符陣列存在全域區
}

記憶體分配與釋放

//動態申請記憶體:malloc函式  回傳型別為void *(可以轉為任意型別指標) 或 NULL(如果記憶體不夠申請則回傳NULL)
ptr = (int *)malloc(sizeof(int)*128);  //申請完一定要判斷 ptr 是否為NULL
//初始化分配的記憶體空間
memset(ptr,0,sizeof(int)*128);
//使用
*(ptr) = 1;
//使用完畢,釋放申請的堆空間
free(ptr);
//將指標設定為NULL
ptr = NULL;

6. PE檔案格式

注意

記憶體中的PE檔案格式與磁盤中是不一樣的,

PE檔案主要分為以下幾種:

種類 主擴展名
可執行系列 .exe .scr
庫系列 .dll .ocx .cpl .drv
驅動程式系列 .sys .vxd
物件檔案系列 .obj
特征:
執行時先進行拉伸,哪怕只有一位元組的資料要存盤也需要占據200h位元組的空間
分節,每一個節存盤不同的資料,資料的概要性描述資訊存到塊表(節標),應對多開問題,只賦值能讀能寫的資料        

硬碟對齊:200h 記憶體對齊:1000h

image-20220321090925343

DOS頭

e_magic --"MZ標記",用于判斷是否是可執行檔案,被稱為魔術數字,它被用于表示一個MS-DOS兼容的檔案型別,所有MS-DOS兼容的可執行檔案都將這個值設為0x5A4D,表示ASCII字符MZ,MS-DOS頭部之所以有的時候被稱為MZ頭部,就是這個緣故,

e_lfnew --NT頭相對于檔案的偏移,比如其值為00 00 00 E8,自此向下查E8個位元組即為PE檔案頭地址,中間有垃圾資料

NT頭
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;          //4個位元組的PE標志,通常設定成00004550h,其ASCII碼為PE00,這個欄位是PE檔案頭的開始 
    IMAGE_FILE_HEADER FileHeader;      //檔案頭標準PE頭
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;//可選頭可選PE頭
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

包含標志位DWORD Signature和標準PE頭以及可選PE頭,DOS頭與標準PE頭位元組固定,可選PE頭位元組不固定

標準PE頭(20位元組)
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;            //程式運行的CPU型號,作用是區別這個exe是哪個CPU可以跑的.重要.
    WORD    NumberOfSections;      //節的數量  (可以理解為匯編中區的個數)現在我們有兩個,一個.rdata 一個.text 重要
    DWORD   TimeDateStamp;        //時間戳,檔案的創建時間  --程式的編譯時間,參考用,沒有實際作用
    //加殼前往往會看PE檔案和 對應的map檔案(存放了exe要用到的各種資訊)的時間戳是否一樣,不一樣就不能加殼

    //DWORD   PointerToSymbolTable;   //符號表地址  我們使用的PDB檔案(里面有函式嗎什么的)都存放在這個表中,不過微軟是單獨生成的PDB檔案,所以這個欄位沒用,主要是給別人用
    //DWORD   NumberOfSymbols;      //符號表大小
    WORD    SizeOfOptionalHeader;   //可選頭大小,這個欄位很重要.因為要通過這個欄位,才知道可選頭是多大,而不懂PE的人求選項頭都是用sizeof()求出來的.所以真正的選項頭大小要靠這個欄位
    WORD    Characteristics;    //檔案屬性,描述檔案資訊的,每個位有不同含義,可執行檔案值為10F,即第0 1 2 3 8位置1
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可選PE頭

--在32位里默認大小為E0h ---64位里默認大小F0h

偏移量 offset = dos_head->e_lfanew + SIZE_OF_NT_SIGNATURE(即:4) + sizeof(IMAGE_FILE_HEADER) -標準PE頭

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;                  /*機器型號,判斷是PE是32位:10B  還是64位:20B*/
    //BYTE    MajorLinkerVersion;          /*連接器版本號高版本*/
    //BYTE    MinorLinkerVersion;          /*連接器版本號低版本,組合起來就是 5.12 其中5是高版本,C是低版本*/
    DWORD   SizeOfCode;           //代碼節的總大小(512為一個磁盤扇區),必須是FileAlignment整數倍      沒用
    DWORD   SizeOfInitializedData;      //初始化資料的節的總大小,也就是.data必須是FileAlignment整數倍     沒用
    DWORD   SizeOfUninitializedData;    //未初始化資料的節的大小,也就是 .data ?必須是FileAlignment整數倍  沒用
    DWORD   AddressOfEntryPoint;      //程式執行入口(OEP) RVA(相對偏移)                              重要
    DWORD   BaseOfCode;           //代碼的節的起始RVA(相對偏移)也就是代碼區的偏移,偏移+模塊首地址定位代碼區
    DWORD   BaseOfData;           //資料節的起始偏移(RVA),同上                               沒用
    DWORD   ImageBase;          //記憶體鏡像基址,程式在記憶體中執行時的加載基址                      重要            目的:地址被別的程式占用后仍可改為別的值            記憶體中的程式入口地址一般都是OEP的值加上這個地址的值     最重要

    DWORD   SectionAlignment;           /*記憶體中的節對齊*/
    DWORD   FileAlignment;             /*檔案中的節對齊*/                  重要
    //WORD    MajorOperatingSystemVersion;    /*作業系統版本號高位*/
    //WORD    MinorOperatingSystemVersion;    /*作業系統版本號低位*/
   // WORD    MajorImageVersion;          /*PE版本號高位*/
    //WORD    MinorImageVersion;          /*PE版本號低位*/
   // WORD    MajorSubsystemVersion;        /*子系統版本號高位*/
    //WORD    MinorSubsystemVersion;        /*子系統版本號低位*/
    //DWORD   Win32VersionValue;          /*32位系統版本號值,注意只能修改為4 5 6表示作業系統支持nt4.0 以上,5的話依次類推*/
    DWORD   SizeOfImage;//整個程式在記憶體中占用的空間(PE映尺寸imagebuffer),可以比實際值大,必須是SectionAlignment整數倍                                                                                     重要
    DWORD   SizeOfHeaders;   //所有頭(頭的結構體大小)+節表的大小,嚴格按照FileAlignment對齊                    重要
    DWORD   CheckSum;  //校驗和,對于驅動程式,可能會使用,用于判斷檔案是否被修改,兩個兩個值加起來,回圈求和             有用
    WORD    Subsystem;              /*檔案的子系統 :重要*/
    WORD    DllCharacteristics;         /*DLL檔案屬性,也可以成為特性,可能DLL檔案可以當做驅動程式使用*/
    DWORD   SizeOfStackReserve;        /*預留的堆疊的大小*/
    DWORD   SizeOfStackCommit;         /*立即申請的堆疊的大小(分頁為單位)*/
    DWORD   SizeOfHeapReserve;        /*預留的堆空間大小*/
    DWORD   SizeOfHeapCommit;         /*立即申請的堆的空間的大小*/
    DWORD   LoaderFlags;            /*與除錯有關*/
    DWORD   NumberOfRvaAndSizes;       //目錄項    下面的成員,資料目錄結構的專案數量  10h                 有用
    //這個域標識了接下來的DataDirectory陣列,請注意它被用來標識這個陣列,而不是陣列中的各個入口數字,這一點非常重要,

    IMAGE_DATA_DIRECTORY DataDirectory[16];/*資料目錄,默認16個,16是宏,這里方便直接寫成16  有匯入匯出表重定向表 重要*/
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
//PE格式 imagebase + 偏移

流出imagebase 一般exe是400000h ,目的是為了記憶體保護,空出一段記憶體的保護地址,一個exe程式對應一個4GB空間,但這一個4GB空間里會有很多PE檔案,所以要有imagebase,防止載入多個PE檔案的時候基址被搶占,如果一個PE占據了400000,那么其它的PE可以通過修改自己的imagebase的值從而改變載入的地址完成加載, 經過PEloader拉伸后得到檔案鏡像(imagebuffer)

image-20220418201420432

DataDirectory:資料目錄,這是一個陣列,陣列的項定義如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
RVA與FOA轉換??

RVA:相對虛擬地址,可以理解為檔案被裝載到虛擬記憶體(拉伸)后先對于基址的偏移地址,它的對齊方式一般是以1000h為單位,以虛擬記憶體對齊的方式對齊的,具體對齊需要參照IMAGE_OPTIONAL_HEADER32中的SectionAlignment成員,

計算公式:RVA = VA - ImageBase

FOA:檔案偏移地址,可以理解為檔案在磁盤上存放時相對于檔案開頭的偏移地址,它的對齊方式一般是以200h為單位,以檔案對齊的方式對齊的,具體對齊需要參照IMAGE_OPTIONAL_HEADER32中的FileAlignment成員,

RVA轉換為FOA??

1.檔案對齊跟記憶體對齊一樣的情況:RVA即FOA

2.檔案對齊和記憶體對齊不一樣的情況:

我們需要判斷RVA屬于哪個節/頭,這里也要分為兩種情況!

1)如果RVA屬于檔案頭部(DOS頭 + PE頭 + 節表),頭部大小是檔案對齊大小的整數倍!

那么不需要進行計算了,因為DOS頭和PE頭和節表在檔案中和在記憶體中展開都是一樣的,直接從開始位置尋找到RVA個位元組即可,就是找0x24A30,也就是FOA(檔案偏移地址)

2)如果RVA不在頭,就要判斷在哪個節里面

判斷節開始位置到節結束位置 我們的RVA是否在這個范圍里面,總共分為三步驟:

第一步:指定節.VirtualAddress  <= RVA <= 指定節.VirtualAddress + VirtualSize(當前節記憶體實際大小)

第二步:差值 = RVA - 指定節.VirtualAddress

第三步:FOA = 指定節.PointerToRawData + 差值
size_t RVAToFOA(
    size_t x,
    PVOID memoryBuff
){

    PIMAGE_DOS_HEADER idh = NULL;
    PIMAGE_NT_HEADERS inh = NULL;
    PIMAGE_FILE_HEADER ifh = NULL;
    PIMAGE_OPTIONAL_HEADER ioh = NULL;
    PIMAGE_SECTION_HEADER ish = NULL;

    idh = (PIMAGE_DOS_HEADER)memoryBuff;
    inh = (PIMAGE_NT_HEADERS)((BYTE *)memoryBuff + idh->e_lfanew); //NT頭
    ifh = (PIMAGE_FILE_HEADER)((BYTE *)inh + sizeof(DWORD));    //標準PE頭
    ioh = (PIMAGE_OPTIONAL_HEADER)((BYTE *)ifh + IMAGE_SIZEOF_FILE_HEADER); //可選PE頭
    ish = (PIMAGE_SECTION_HEADER)((BYTE *)ioh + ifh->SizeOfOptionalHeader); //節表

    DWORD sectionCount = inh->FileHeader.NumberOfSections;
    DWORD memoryAli = inh->OptionalHeader.SectionAlignment;
    //DWORD RVA = x - ioh->ImageBase;
    DWORD RVA = x;
    if(RVA < pOptionHeader->SizeOfHeaders) return RVA; //RVA屬于檔案頭部(DOS頭 + PE頭 + 節表)

    for(DWORD i = 0;i < sectionCount ; i++){ //RVA不在檔案頭部,需要判斷在哪個節
        PIMAGE_SECTION_HEADER tmpSec = (ish + i);  //判斷在哪個節 這里是指標的加減 加的是整個節
        if( RVA >= tmpSec->VirtualAddress && RVA <= (tmpSec->VirtualAddress + tmpSec->Misc.VirtualSize)){
            return tmpSec->PointerToRawData + (RVA - tmpSec->VirtualAddress);//檔案中的偏移加上 (RVA-記憶體中偏移)
        }
    }

    return ERROR;
}
//**************************************************************************                                
//RVA2FOA:將記憶體偏移轉換為檔案偏移                                
//引數說明:                                
//pFileBuffer FileBuffer指標                                
//dwRva RVA的值                                
//回傳值說明:                                
//回傳轉換后的FOA的值  如果失敗回傳0                                
//**************************************************************************                                
DWORD RVA2FOA(IN LPVOID pFileBuffer, IN DWORD dwRva)
{
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pNextSectionHeader = NULL;
    //DOS頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭    
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案欄位中
    if (dwRva < pOptionalHeader->SizeOfHeaders)    //偏移小于頭的大小,記憶體偏移則為檔案偏移
    {
        return dwRva;
    }
    //首個節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    //下一個節表
    pNextSectionHeader = pSectionHeader + 1;
    //回圈遍歷節表
    for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++, pNextSectionHeader++)//注意這里i從1開始 i < NumberOfSections
    {    //注意這里的pSectionHeader已經是加了基址的,不是偏移, 是絕對地址,而dwRva是偏移地址
        if (dwRva >= pSectionHeader->VirtualAddress && dwRva < pNextSectionHeader->VirtualAddress)//大于當前節的記憶體偏移而小于下一節的記憶體偏移
        {    //則dwRva屬于當前節,則dwRva - VirtualAddress為dwRva基于當前節的偏移,此偏移加上當前節的檔案起始偏移地址 則為dwRva在檔案中的偏移
            return pSectionHeader->PointerToRawData + (dwRva - pSectionHeader->VirtualAddress);
        }
    }    //出回圈后pSectionHeader指向最后一個節表
    //大于當前節(最后一節)的記憶體偏移且小于記憶體映射大小
    if (dwRva >= pSectionHeader->VirtualAddress && dwRva < pOptionalHeader->SizeOfImage)
    {    //同上return
        return pSectionHeader->PointerToRawData + (dwRva - pSectionHeader->VirtualAddress);
    }
    else //大于記憶體映射大小
    {
        printf("dwRva大于記憶體映射大小\n");
        return -1;
    }
}
//測驗 記憶體偏移轉換為檔案偏移 是否正確
bool TestRvaToFile(LPVOID pFileBuffer ,DWORD Offset)
{
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_NT_HEADERS pNTHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pNextSectionHeader = NULL;
    //DOS頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    //NT頭
    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭    
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案欄位中
    //回圈遍歷節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    for (int i = 1; i <= pFileHeader->NumberOfSections; i++, pSectionHeader++)
    {
        //通過函式由Rva得出的FOA
        DWORD FuncFOA = RVA2FOA(pFileBuffer, Offset + pSectionHeader->VirtualAddress);
        //查節表得出的FOA
        DWORD RealFOA = Offset + pSectionHeader->PointerToRawData;
        if (FuncFOA != RealFOA)
        {
            printf("第%d個節表測驗錯誤!\n", i);
            return false;
        }
    }    //出回圈后pSectionHeader指向不存在的節表,多了一節
    pSectionHeader = NULL;  //所以pSectionHeader指標不可再使用
    printf("測驗成功!\n");
    return true;
}

FOA轉換為RVA

設FOA為節資料的任意一位置

1.計算差值偏移:
FOA - 指定節.PointerToRawData(檔案中偏移) = 差值
2.計算RVA:
差值 + 指定節.VirtualAddress(記憶體中偏移) = RVA
3.計算虛擬地址:
VA = RVA + ImageBase
需要注意的就是我們的 FOA 在哪一個節中:
指定節.PointerToRawData <=  FOA <= 指定節..PointerToRawData + 指定節..SizeofRawData
聯合體(共用體)
union data{
    int n;
    char ch;
    double f;
};

共用體的所有成員占用同一段記憶體,修改一個成員會影響其余所有成員,共用體占用的記憶體等于最長的成員占用的記憶體,共用體使用了記憶體覆寫技術,同一時刻只能保存一個成員的值,如果對新的成員賦值,就會把原來成員的值覆寫掉,

節表

偏移:SecOffset = dos_head->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof(IMAGE_FILE_HEADER)+ peHeader>FileHeader.SizeOfOptionalHeader;

image-20220322170321216

IMAGE_SECTION_HEADER STRUCT
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8個位元組的節區名稱,一般情況下是以"\0"結尾的ASCII碼字串來表示的名稱
//注意:該名稱并不遵守必須以"\0"結尾的規律,如果不以"\0"結尾,系統會截取8個位元組的長度進行處理,  .text/.data
union{                            
  DWORD PhysicalAddress;       
  DWORD VirtualSize;            //節區的尺寸,檔案對齊前的實際大小,但可以修改故不準確
        //不一定比sizeofRawData小   因為VirtualSize在記憶體中與檔案中大小不同,檔案中不存放未初始化變數,而記憶體中會存放
}Misc;  //聯合體   四個位元組,存放該節在沒有對齊前記憶體中的真實Size,但可以修改
DWORD VirtualAddress;         // 節區的 RVA(相對偏移地址)地址    節區在*記憶體*中的偏移地址,加上imagebase才是真實地址
DWORD SizeOfRawData;            // 節在檔案中對齊后的尺寸
DWORD PointerToRawData;        // 節區在檔案中的偏移量,一定是檔案對齊的整數倍      檔案打開根據此值判斷節內容位置
//DWORD PointerToRelocations;     // 在OBJ檔案中使用,重定位的偏移,對exe無意義
//DWORD PointerToLinenumbers;   // 行號表的偏移(除錯時使用)
//WORD NumberOfRelocations;      // 在OBJ檔案中使用,重定位項數目,對exe無意義
//WORD NumberOfLinenumbers;    // 行號表中行號的數目
DWORD Characteristics;
//節屬性如可讀,可寫,可執行等 .test節6000020  4000000為該節可執行 2000000表示該節可寫 20為包含可執行代碼
IMAGE_SECTION_HEADER ENDS

image-20220418210848269

CALL與JMP執行

004011F8 E8 53 FE FF FF call 00401050 步入后地址為 004011FD

004011FD --都是記憶體中運行時的地址

真正要跳轉的地址 = E8這條指令的下一行地址 + X X = 真正要跳轉的地址 - E8這條指令的下一行地址 = 401050 - 4011FD = FFFFFE53

E8 53 FE FF FF為五個位元組,硬編碼知識,E8下一條指令地址為 E8 + 5 = FD

故要跳轉的地方 = E8當前指令地址(拉伸后的地址) + 5 + X

JMP指令類似

空白區添加代碼時 添加的是E8 53 FE FF FF (x) 需利用上面的公式計算出x

任意代碼空白區添加代碼??

1、根據上面的公式,修改 E8后的地址 ,在代碼區手動填寫 messagebox函式起始地址 0x77E5425F

計算E8 后的四個位元組 x : x = 77E5425F(真正要跳轉地址) - (E8指令下一條指令地址+imagebase)(注意區分記憶體和檔案的對齊偏移)

然后修改E9 后的四個位元組,方法同上

然后修改OEP(程式入口地址為插入的二進制指令的地址)

  • 添加代碼前我們要判斷ShellCode是否能放得下,就要SizeOfRaw-misc>shellcode的長度

  • 我們要知道,添加的代碼是正在運行中的,所以要加上ImageBase,但是我們是在ImageBuffer中添加的代碼,ImageBuffer的起始地址是我們自己malloc出來的,不是真正在記憶體中執行的地址,所以我們要減去pImageBuffer加上ImageBase得到真正的運行地址

  • 這個代碼有兩種寫法,一是在FileBuffer中添加(需要注意記憶體偏移的檔案偏移的影響),二是在ImageBuffer中添加(不需要注意記憶體檔案偏移,因為檔案已經拉伸了)

  • 我們需要知道MessageBox的地址,我們在DT里面添加斷點bp MessageBoxA,然后查看就知道斷點的位置了

#include "stdafx.h"
#include<stdio.h>
#include<windows.h>
#include<malloc.h>
#define ShellCodeIen  0x12
#define MessageBoxAdder 0x77D507EA
BYTE ShellCode[]=
{
    0x6A,00,0x6A,00,0x6A,00,0x6A,00,
    0xE8,00,00,00,00,
    0xE9,00,00,00,00
};
//
//FileBuffer函式
DWORD ReadPEFile(LPVOID* ppFileBuffer)
{
    FILE* pFile=NULL;
    DWORD SizeFileBuffer=0;
    pFile=fopen("軟體路徑","rb");
    if(!pFile)
    {
        printf("打開notepad失敗\n");
        return 0;
    }
    //獲取檔案大小
    fseek(pFile,0,SEEK_END);
    SizeFileBuffer=ftell(pFile);
    fseek(pFile,0,SEEK_SET);
    if(!SizeFileBuffer)
    {
        printf("讀取檔案大小失敗\n");
        return 0;
    }
    //開辟空間
    *ppFileBuffer=malloc(SizeFileBuffer);
    if(!*ppFileBuffer)
    {
        printf("開辟空間失敗\n");
        fclose(pFile);
        return 0;
    }
    //復制資料
    size_t n=fread(*ppFileBuffer,SizeFileBuffer,1,pFile); //將讀取到的PE檔案存到了pFileBuffer里
    if(!n)
    {
        printf("復制資料失敗\n");
        free(*ppFileBuffer);
        fclose(pFile);
        return 0;
    }
    fclose(pFile);
    return SizeFileBuffer;
}

//FileBuffer--->ImgaeBuffer
DWORD FileBufferToImageBuffer(LPVOID pFileBuffer,LPVOID* ppImageBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;

    if(!pFileBuffer)
    {
        printf("FileBuffer函式呼叫失敗\n");
        return 0;
    }
    printf("%x\n",pFileBuffer);
//判斷是否是PE檔案
    pDosHeader=(PIMAGE_DOS_HEADER)pFileBuffer;
    if(pDosHeader->e_magic!=IMAGE_DOS_SIGNATURE)
    {
        printf("不是有效的MZ標志\n");
        return 0;
    }

    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
    if(pNTHeader->Signature!=IMAGE_NT_SIGNATURE)
    {
        printf("不是有效的PE標志\n");
        return 0;
    }

    pFileHeader=(PIMAGE_FILE_HEADER)(((DWORD)pNTHeader)+4); //把pNTHeader轉為DWORD  NT頭+4才是標準PE頭

    pOptionalHeader=(PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader+20); //標準PE頭+20位為可選PE頭
    //開辟ImageBuffer空間
    *ppImageBuffer=malloc(pOptionalHeader->SizeOfImage);
    if(!*ppImageBuffer)
    {
        printf("開辟ImageBuffer空間失敗");
        return 0;
    }
    printf("SizeOfImage%x\n",pOptionalHeader->SizeOfImage);
    //malloc清零
    memset(*ppImageBuffer,0,pOptionalHeader->SizeOfImage); 

    //復制Headers
    printf("SizeOfHeader%x\n",pOptionalHeader->SizeOfHeaders);
    memcpy(*ppImageBuffer,pDosHeader,pOptionalHeader->SizeOfHeaders);

    //回圈復制節表  可選PE頭 + 標準PE頭的SizeOfOptionalHeader得到節表
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
    for(int i=1;i<=pFileHeader->NumberOfSections;i++,pSectionHeader++)
    {
        memcpy((LPVOID)((DWORD)*ppImageBuffer+pSectionHeader->VirtualAddress),(LPVOID)((DWORD)pFileBuffer+pSectionHeader->PointerToRawData),pSectionHeader->SizeOfRawData);
        printf("%d\n",i);
    }
    printf("拷貝完成\n");
    return pOptionalHeader->SizeOfImage;
}


//shellCode
LPVOID shellCode(LPVOID pImageBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;
    PBYTE ShellCodeBegin=NULL;
    if(!pImageBuffer)
    {
        printf("傳入引數pImageBuffer失敗\n");
        return 0;
    }
    pDosHeader=(PIMAGE_DOS_HEADER)pImageBuffer;
    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pImageBuffer+pDosHeader->e_lfanew);
    pFileHeader=(PIMAGE_FILE_HEADER)((DWORD)pNTHeader+4);
    pOptionalHeader=(PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader+20);
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
    //看看shellcode是否有地方加入
    if((pSectionHeader->SizeOfRawData-pSectionHeader->Misc.VirtualSize)< ShellCodeIen)
    {
        printf("空間不足加入shellcode!!!!\n");
        free(pImageBuffer);  //不足必須把剛剛申請的記憶體釋放掉
        return 0;
    }
    printf("SizeOfRaw=%x\n",pSectionHeader->SizeOfRawData);
    printf("Misc=%x\n",pSectionHeader->Misc.VirtualSize);
    printf("空間充足\n");
    //判斷FileAligment&SectionAliment
    if(pOptionalHeader->FileAlignment==pOptionalHeader->SectionAlignment)
    {
        printf("檔案對齊和記憶體對齊相等\n");
        ShellCodeBegin=(PBYTE)(pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize+(DWORD)pImageBuffer);    //把shellcode復制到空閑區
        if(!memcpy(ShellCodeBegin,ShellCode,ShellCodeIen))
        {
            printf("代碼初步加入失敗\n");
            return 0;
        }
        printf("代碼初步加入成功\n");
        //E8修正            pImageBuffer為我們自己malloc的地址基址  得到偏移后加上運行時的imagebase才是真正的地址
        DWORD CallAddr=(DWORD)((DWORD)MessageBox-((DWORD)pOptionalHeader->ImageBase+(DWORD)ShellCodeBegin+0xD-(DWORD)pImageBuffer));   // 0xD 是因為E8下一條指令在shellcode的偏移為0xD
        if(!CallAddr)
        {
            printf("E8地址獲取失敗\n");
            return 0;
        }
        *(PDWORD)(ShellCodeBegin+0x9)=CallAdd;
        printf("E8修正完成\n");
        //E9修正
        DWORD JmpAddr=(DWORD)((DWORD)pOptionalHeader->AddressOfEntryPoint-((DWORD)ShellCodeBegin+ShellCodeIen-(DWORD)pImageBuffer)); //ShellCodeIen為宏常量 0x12
        if(!JmpAddr)
        {
            printf("E9地址獲取失敗\n");
            return 0;
        }
        *(PDWORD)(ShellCodeBegin+0xE)=JmpAddr;
        printf("E9修正完成\n");
        //修正OEP  為偏移, ShellCodeBegin-pImageBuffer即可 注意型別轉換
        printf("OEP=%x\n",pOptionalHeader->AddressOfEntryPoint);
        pOptionalHeader->AddressOfEntryPoint=(DWORD)ShellCodeBegin-(DWORD)pImageBuffer;
        printf("OEP=%x\n",pOptionalHeader->AddressOfEntryPoint);
        printf("OEP修正完成\n");
        printf("完成!!!!!\n");
        return pImageBuffer;

    }
    else
    {
        printf("檔案對齊和記憶體對齊不一樣\n");
        pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
        ShellCodeBegin=(PBYTE)(pSectionHeader->VirtualAddress+pSectionHeader->Misc.VirtualSize+(DWORD)pImageBuffer);     //把shellcode復制到空閑區
        if(!memcpy(ShellCodeBegin,ShellCode,ShellCodeIen))
        {
            printf("代碼初步加入失敗\n");
            return 0;
        }
        printf("代碼初步加入成功\n");
        //E8修正
        DWORD CallAddr=(DWORD)((DWORD)MessageBox-((DWORD)pOptionalHeader->ImageBase+(DWORD)ShellCodeBegin+0xD-(DWORD)pImageBuffer));
        if(!CallAddr)
        {
            printf("E8地址獲取失敗\n");
            return 0;
        }
        *(PDWORD)(ShellCodeBegin+0x9)=CallAdd;
        printf("E8修正完成\n");
        //E9修正
        DWORD JmpAddr=(DWORD)((DWORD)pOptionalHeader->AddressOfEntryPoint-((DWORD)ShellCodeBegin+ShellCodeIen-(DWORD)pImageBuffer));
        if(!JmpAddr)
        {
            printf("E9地址獲取失敗\n");
            return 0;
        }
        *(PDWORD)(ShellCodeBegin+0xE)=JmpAddr;
        printf("E9修正完成\n");
        //修正OEP
        printf("OEP=%x\n",pOptionalHeader->AddressOfEntryPoint);
        pOptionalHeader->AddressOfEntryPoint=(DWORD)ShellCodeBegin-(DWORD)pImageBuffer;
        printf("OEP=%x\n",pOptionalHeader->AddressOfEntryPoint);
        printf("OEP修正完成\n");
        printf("完成!!!!!\n");
        return pImageBuffer;        
    }

}

//ImageBufferToFileBuffer     將插入代碼后的再縮短,縮短后才可以存盤
DWORD ImageBufferToFileBuffer(LPVOID pImageBuffer,LPVOID* ppBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;

    if(!pImageBuffer)
    {
        printf("error");
        return 0;
    }

    pDosHeader=(PIMAGE_DOS_HEADER)pImageBuffer;
    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pImageBuffer+pDosHeader->e_lfanew);
    pFileHeader=(PIMAGE_FILE_HEADER)((DWORD)pNTHeader+4);
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + 20);
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);

    //得到FileBuffer的大小
    for(int i=1;i<pFileHeader->NumberOfSections;i++,pSectionHeader++)
    {
        printf("%d\n",i);
    }

    //回圈到最后一個節表
    DWORD SizeOfBuffer=pSectionHeader->PointerToRawData+pSectionHeader->SizeOfRawData;

    //開辟空間
    *ppBuffer=malloc(SizeOfBuffer);
    if(!*ppBuffer)
    {
        printf("開辟Buffer空間失敗\n");
        return 0;
    }
    printf("SizeOfBuffer%x\n",SizeOfBuffer);
    memset(*ppBuffer,0,SizeOfBuffer);

    //復制頭
    memcpy(*ppBuffer,pImageBuffer,pOptionalHeader->SizeOfHeaders);
    //復制節表
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
    for(int j=1;j<=pFileHeader->NumberOfSections;j++,pSectionHeader++)
    {
        printf("%d\n",j);
        memcpy((LPVOID)((DWORD)*ppBuffer+pSectionHeader->PointerToRawData),(LPVOID)((DWORD)pImageBuffer+pSectionHeader->VirtualAddress),pSectionHeader->SizeOfRawData);
    }
    printf("拷貝完成\n");
    return SizeOfBuffer;
}

//存貯到新的exe  將記憶體資料存到硬碟
BOOL MemeryToFile(LPVOID pBuffer,DWORD SizeOfBuffer)
{
    FILE* fpw=fopen("軟體路徑","wb");
    if(!fpw)
    {
        printf("fpw error");
        return false;
    }
    if (fwrite(pBuffer, 1, SizeOfBuffer, fpw) == 0)
    {
        printf("fpw fwrite fail");
        return false;
    }
    fclose(fpw);            
    fpw = NULL;
    printf("success\n");
    return true;

} 

int main()
{
    LPVOID pFileBuffer=NULL;
    LPVOID* ppFileBuffer=&pFileBuffer;
    LPVOID pImageBuffer=NULL;
    LPVOID* ppImageBuffer=&pImageBuffer;
    DWORD SizeOfFileBuffer=0;
    DWORD SizeOfImageBuffer=0;
    DWORD SizeOfBuffer=0;

    LPVOID pBuffer=NULL;
    LPVOID* ppBuffer=&pBuffer;


    //呼叫filebuffer函式
    SizeOfFileBuffer=ReadPEFile(ppFileBuffer);
    if(!SizeOfFileBuffer)
    {
        printf("FileBuffer函式呼叫失敗 \n");
        return 0;
    }
    pFileBuffer=*ppFileBuffer;
    printf("ni ma de");


    //呼叫FileBufferToImageBuffer函式
    SizeOfBuffer=FileBufferToImageBuffer(pFileBuffer,ppImageBuffer);

    if(!SizeOfBuffer)
    {
        printf("呼叫FileBufferToImageBuffer函式失敗");
        return 0;
    }

    //呼叫ShellCode函式
    pImageBuffer=shellCode(pImageBuffer);

    //呼叫ImageBufferToBuffer
    SizeOfBuffer=ImageBufferToFileBuffer(pImageBuffer,ppBuffer);
    pBuffer=*ppBuffer;
    if(!SizeOfBuffer)
    {
        printf("SizeOfBuffer error");
        return 0;
    }

    //呼叫MemeryToFile
    if(MemeryToFile(pBuffer,SizeOfBuffer)==false)   //pBuffer為存盤的起始位置,SizeOfBuffer為要存盤的大小
    {
        printf("end");
        return 0;
    }
    return 0; 
} 
合并節與擴大節

img

從 filebuffer拷貝到imagebuffer(檔案中而非記憶體中):首先根據sizeofimage決定需要在記憶體中分多大的空間,然后首先拷貝頭+節表按照對齊的方式拷貝過去,然后拷貝節,根據PointerToRawData決定從哪里開始拷貝,拷貝到imagebuffer的位置由 VirtualAddress+imagebase決定, 記憶體中運行時,imagebuffer的值變為imagebase

拷貝的大小在Misc的SizeOfRawData,

在imagebuffer中的某個地址映射到filebuffer 首先根據該地址 - imagebuffer 得到偏移量a,

如果a > VirtualAddress 并且 a < VirtualAddress + misc.VirtualSize 則該地址位于VirtualAddress對應的節中,

a - VirtualAddress 得到該地址距離該節開始地址的偏移,那么在檔案中的映射相對于節的偏移也是 a - VirtualAddress

//將所有節合并為一個節
#include "windows.h"
#include "stdio.h"
VOID h3202()
{
    char FilePath[] = "CrackHead.exe";    //CRACKME.EXE        CrackHead.exe
    char CopyFilePath[] = "CrackHeadcopy.exe";    //CRACKMEcopy.EXE       CrackHeadcopy.exe
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    LPVOID pImageBuffer = NULL;;            //會被函式改變的 函式輸出之一
    LPVOID* ppImageBuffer = &pImageBuffer;    //傳進函式的形參
    int SizeOfFileBuffer;
    int SizeOfImageBuffer;
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_NT_HEADERS pNTHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pFirstSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pEndOfSectionHeader = NULL;
    DWORD CallX = NULL;    //即E8后跟的4位元組
    DWORD JmpX = NULL;    //即E9后跟的4位元組

    //pFileBuffer即指向已裝載到記憶體中的exe首部
    if (!ReadPEFile(FilePath, ppFileBuffer))
    {
        printf("檔案讀取失敗\n");
        return;
    }
    SizeOfImageBuffer = CopyFileBufferToImageBuffer(pFileBuffer, ppImageBuffer);
    //VerifyFileBufferAndImageBuffer(pFileBuffer, pImageBuffer);

    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;    // 強轉 DOS_HEADER 結構體指標
    //NT頭
    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pImageBuffer + pDosHeader->e_lfanew);
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭    
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案欄位中
    //首個節表     可選PE頭加上標準PE頭里的可選PE頭大小SizeOfOptionalHeader
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    pFirstSectionHeader = pSectionHeader;

    //修改首個節表長度資訊
    //printf("SizeOfImageBuffer:%x\nVirtualAddress:%x\n", SizeOfImageBuffer, pFirstSectionHeader->VirtualAddress);
    pFirstSectionHeader->SizeOfRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/Align(SizeOfImageBuffer - 
                       pFirstSectionHeader->VirtualAddress,pOptionalHeader->FileAlignment);
    //printf("SizeOfRawData:%x\n", pFirstSectionHeader->SizeOfRawData);
    pFirstSectionHeader->Misc.VirtualSize = pFirstSectionHeader->SizeOfRawData;//將第一個節的記憶體與檔案大小相等
    //printf("VirtualSize:%x\n", pFirstSectionHeader->Misc.VirtualSize);

    //修改首節表Characteristics
    DWORD Characteristics = pSectionHeader->Characteristics;
    pSectionHeader++;
    for (int i = 2; i <= pFileHeader->NumberOfSections; i++, pSectionHeader++)    //注意從第二個開始
    {
        Characteristics = Characteristics | pSectionHeader->Characteristics;
        memset(pSectionHeader, 0, IMAGE_SIZEOF_SECTION_HEADER);    //抹去其他節表資訊
    }    

    //出回圈后pSectionHeader指向節表末尾    
    pFirstSectionHeader->Characteristics = Characteristics;
    pEndOfSectionHeader = pSectionHeader;
    printf("Characteristics:%x\n", pFirstSectionHeader->Characteristics);

    //修改節數量
    pFileHeader->NumberOfSections = 1;

    //拷貝到檔案
    free(pFileBuffer);
    SizeOfFileBuffer = CopyImageBufferToFileBuffer(pImageBuffer, ppFileBuffer);
    MemeryToFile(pFileBuffer, SizeOfFileBuffer, CopyFilePath);
}
//**************************************************************************                                
//Align:計算對齊后的值                    
//引數說明:                                
//x  需要進行對齊的值                                
//Alignment 對齊大小                        
//回傳值說明:                                
//回傳x進行Alignment值對齊后的值                                
//**************************************************************************    
int Align(int x, int Alignment)
{
    if (x%Alignment==0)
    {
        return x;
    }
    else
    {
        return (1 + (x / Alignment)) * Alignment;
    }
}
擴大節

--只能擴大最后一個

1、拉伸到記憶體(只是邏輯上,實際代碼操作并不需要這一步),需要注入的代碼為 ShellCode

2、分配一塊新的空間NewImageBuffer:SizeOfImage + sizeof ( ShellCode) --Ex

3、擴大節,修改最后一個節的 SizeOfRawData 和 VirtualSize

OrgSecSize = max ( SizeOfRawData 或 VirtualSize記憶體對齊后的值 )

∵ 有些初始化資料未全部寫入檔案,VirtualSize 可能比 SizeOfRawData 大,必須保證添加的代碼不影響到原程式

VirtualSize = OrgSecSize + sizeof ( ShellCode)

SizeOfRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/( OrgSecSize + sizeof(ShellCode) ) 按檔案對齊后的值

4、修改SizeOfImage大小

SizeOfImage = SizeOfImage + Ex(即sizeof ( ShellCode))
image-20220419125030635

//擴大節
#include "windows.h"
#include "stdio.h"
#define MESSAGEBOXADDR 0x76AF39A0 //這個值需要將任一exe檔案拖入OD打開,搜索bp MessageBoxA 記錄它的地址到這里(每次開機都不同)
unsigned char ShellCode320[] =
{
    0x6A,0x00,0x6A,0x00,0x6A,0x00,0x6A,0x00,
    0xE8,0x00,0x00,0x00,0x00,
    0xE9,0x00,0x00,0x00,0x00
};

void h320()
{
    char FilePath[] = "CrackHead.exe";    //CRACKME.EXE        CrackHead.exe
    char CopyFilePath[] = "CrackHeadcopy.exe";    //CRACKMEcopy.EXE       CrackHeadcopy.exe
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    LPVOID pNewFileBuffer = NULL;
    int SizeOfFileBuffer;
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_NT_HEADERS pNTHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    DWORD CallX = NULL;    //即E8后跟的4位元組
    DWORD JmpX = NULL;    //即E9后跟的4位元組

    SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);    //pFileBuffer即指向已裝載到記憶體中的exe首部
    if (!SizeOfFileBuffer)
    {
        printf("檔案讀取失敗\n");
        return;
    }
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //NT頭
    pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭    
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案欄位中
    //首個節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++){}    //注意這里i從1開始 i < NumberOfSections
    //出回圈后pSectionHeader指向最后一個節表    

    // OrgSecSize 為 該節的原始大小 (最后一個節的原始大小)
    printf("VirtualSize: %x\nSizeOfRawData: %x\n", pSectionHeader->Misc.VirtualSize, pSectionHeader->SizeOfRawData);
    DWORD OrgSecSize = max(pSectionHeader->Misc.VirtualSize, pSectionHeader->SizeOfRawData);
    printf("OrgSecSize: %x\n", OrgSecSize);

    //ShallCode即放在節區頭起Size之后,這里先計算地址,再修改PE中的值與開辟空間
    //X即E8后的數 = 要跳轉的地址 - (E8所在地址 + 5)            (E8 所在地址+5 即 call指令的下一條指令的地址)
    //那么要跳轉的地址即messageboxA地址,E8所在地址即 ImageBase記憶體運行基址 + VirtualAddress節所在偏移+Size +8 才到E8 (∵ShallCode就在節開頭)
    CallX = MESSAGEBOXADDR - (pOptionalHeader->ImageBase + pSectionHeader->VirtualAddress + OrgSecSize + 8 + 5);   // 8 為E8在shellcode的偏移

    //jump 要跳轉的地址即OEP程式入口點, X = 程式入口點 - (E9所在地址 + 5)
    //這里程式入口點即ImageBase基址 + 修改前的OddAddressOfEntryPoint         E9所在地址計算同上   下式是化簡后約去了ImageBase
    //ImageBase + AddressOfEntryPoint - (ImageBase + VirtualAddress +13 +5 )
    JmpX = pOptionalHeader->AddressOfEntryPoint - (pSectionHeader->VirtualAddress + OrgSecSize + 13 + 5);

    //將上述計算后的值放入ShellCode320
    *(PDWORD)(ShellCode320 + 9) = CallX;
    *(PDWORD)(ShellCode320 + 14) = JmpX;
    for (int i = 0; i < sizeof(ShellCode320); i++)
    {
        printf("%x ", ShellCode320[i]);
    }
    printf("\n");

    //計算完地址后 計算新節表值  VirtualSize以Size + ShellCode長度 按記憶體對齊(其實VirtualSize可以不用記憶體對齊),SizeOfRawData同理
    int NewVirtualSize = OrgSecSize + sizeof(ShellCode320);
    int NewSizeOfRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/Align(OrgSecSize + sizeof(ShellCode320), pOptionalHeader->FileAlignment);
    printf("NewSizeOfRawData%x, NewVirtualSize:%x\n", NewSizeOfRawData, NewVirtualSize);

    // 在修改節表值之前需要用到新舊值 ∴上述節表新值暫不真正修改只作記錄,
    // 修改SizeOfImage值
    pOptionalHeader->SizeOfImage = Align(pSectionHeader->VirtualAddress + NewVirtualSize, pOptionalHeader->SectionAlignment);

    //在修改節表值之前新空間長度   原空間長度  減去 舊SizeOfRawData  加上新SizeOfRawData 
    int SizeOfNewFileBuffer = SizeOfFileBuffer - pSectionHeader->SizeOfRawData + NewSizeOfRawData;

    //修改入口點 pSectionHeader指向最后一個節表  OrgSecSize為最后一個節的原始大小 此時pOptionalHeader為添加的shellcode
    pOptionalHeader->AddressOfEntryPoint = pSectionHeader->VirtualAddress + OrgSecSize;//最后一個節偏移+大小,即插入的shellcode代碼 

    //Characteristics
    printf("Characteristics:%x\n", pSectionHeader->Characteristics);
    pSectionHeader->Characteristics = pSectionHeader->Characteristics  | 0x60000020;
    printf("Characteristics:%x\n", pSectionHeader->Characteristics);

    //修改最后一個節表值
    pSectionHeader->Misc.VirtualSize = NewVirtualSize;
    pSectionHeader->SizeOfRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/NewSizeOfRawData;
    //printf("VirtualSize: %x\nSizeOfRawData: %x\n", pSectionHeader->Misc.VirtualSize, pSectionHeader->SizeOfRawData);

    //修改值后開始重新分配空間
    pNewFileBuffer = malloc(SizeOfNewFileBuffer);
    memset(pNewFileBuffer, 0, SizeOfNewFileBuffer);
    memcpy(pNewFileBuffer, pFileBuffer, SizeOfFileBuffer);    //復制原空間
    printf("SizeOfFileBuffer:%x\nSizeOfNewFileBuffer:%x\n", SizeOfFileBuffer, SizeOfNewFileBuffer);

    // 復制新空間
    memcpy((void*)((DWORD)pNewFileBuffer + pSectionHeader->PointerToRawData + OrgSecSize), ShellCode320, sizeof(ShellCode320));

    //memcpy((void*)((DWORD)pNewFileBuffer + SizeOfFileBuffer), ShellCode320, sizeof(ShellCode320));    //復制ShellCode
    //剩余部分填充0
    //memset((void*)((DWORD)pNewFileBuffer + SizeOfFileBuffer + sizeof(ShellCode320)), 0, (SizeOfNewFileBuffer - SizeOfFileBuffer - sizeof(ShellCode320)));
    MemeryToFile(, SizeOfNewFileBuffer, CopyFilePath);
    free(pNewFileBuffer);
    free(pFileBuffer);
}
新增節

img

1、判斷是否有足夠的空間,可以添加一個節表.
判斷條件:

1、判斷是否有足夠的空間,可以添加一個節表(40個位元組 28h).新增節后面加一個全0的節,所以共需80個位元組
判斷條件:SizeOfHeader - (DOS + 垃圾資料 + PE標記 + 標準PE頭 + 可選PE頭 + 已存在節表) >= 2個節表的大小 (如果只有一個節表以上的空間也可以加不會報錯,但是會有安全隱患)

2、需要修改的資料

  1. 添加一個新的節表(可以copy一份可執行的.text節表)
  2. 在新增節后面 填充一個節大小的000
  3. 修改PE頭中節的數量
  4. 修改sizeOfImage的大小
  5. 在原有資料的最后,新增一個節的資料(記憶體對齊的整數倍)
  6. 修正新增節表的屬性

(1)首先判斷頭部的空白區夠不夠加節表, 所有的節表后面必須跟40個位元組0,所以添加節表時得確保有兩個節表大小(即80位元組)的連續剩余,

分三種情況:

a.頭部的最后一個節表后直接可以放下兩個節區
b.節表后放不下,但是dos頭后 標準PE頭前的一堆垃圾資料可以放下,可以修改e_lfnew
c.前兩種情況都不行,需擴大最后一個節把代碼填進去

(2)新增節需要修改的內容:

SizeOfImage
NumOfSections
節表內屬性

(3) 在記憶體中添加新的節表時,需要注意:
新增節表中的VirtualAddress(記憶體中的偏移)必須它上一個節表中VirtualAddress+max(VirtualSize,SizeOfRawData)
(注意: 不一定SizeOfRawData比VirtualSize大)

新增節表中的PoinTtoRawData(檔案中的偏移)是最后一個節表中的PointToRawData(檔案偏移)+SizeOfRawData(檔案大小)

  • 需要修改的屬性

    • 添加一個新的節表(可以直接copy 可執行的.text段,)

    • 修改標準PE頭的NumberOfSection

    • 修改sizeOfImage(+1000,可選PE頭里)

  • 我們怎么修改新的節表的屬性

    • VirtualSize的修正:跟記憶體對齊相等即可,記憶體對齊的倍數就行,可以直接修改為自己加的節的Size

    • VirtualAddress的修正:看上一個節表的屬性,如果檔案對齊和記憶體對齊相等,就等于上一個節的VirtualAdress+RawSize,如果檔案和記憶體對齊不相等,就等于上一個節的VirtualAdress+RawSize(記憶體對齊后的大小)

    • SizeOfRaw的修正:和VirtualSize一樣即可,

    • PointToRawData的修正:上一個節的PointToRawData+SizeOfRaw

  • 寫代碼的時候要注意節表的遍歷,清楚這是第幾個節表,增加完沒有?

  • 檔案對齊和記憶體對齊不一樣的時候,我們可以做一個回圈,讓他累加,直到記憶體對齊的倍數

#include "stdafx.h"
#include<stdio.h>
#include<windows.h>
#include<malloc.h>
#define ShellCodeIen  0x12
#define MessageBoxAdder 0x77D507EA
BYTE ShellCode[]=
{
    0x6A,00,0x6A,00,0x6A,00,0x6A,00,
    0xE8,00,00,00,00,
    0xE9,00,00,00,00
};

//這里只是新增了個節表,并沒有加具體的內容
//FileBuffer函式
DWORD ReadPEFile(LPVOID* ppFileBuffer)
{
    FILE* pFile=NULL;
    DWORD SizeFileBuffer=0;
    pFile=fopen("C://Documents and Settings//ma_lic//桌面//IISPutScanner.exe","rb");
    if(!pFile)
    {
        printf("打開notepad失敗\n");
        return 0;
    }
    //獲取檔案大小
    fseek(pFile,0,SEEK_END);
    SizeFileBuffer=ftell(pFile);
    fseek(pFile,0,SEEK_SET);
    if(!SizeFileBuffer)
    {
        printf("讀取檔案大小失敗\n");
        return 0;
    }
    //開辟空間
    *ppFileBuffer=malloc(SizeFileBuffer);
    if(!*ppFileBuffer)
    {
        printf("開辟空間失敗\n");
        fclose(pFile);
        return 0;
    }
    //復制資料
    size_t n=fread(*ppFileBuffer,SizeFileBuffer,1,pFile);
    if(!n)
    {
        printf("復制資料失敗\n");
        free(*ppFileBuffer);
        fclose(pFile);
        return 0;
    }
    fclose(pFile);
    return SizeFileBuffer;
}

//FileBuffer--->ImgaeBuffer
DWORD FileBufferToImageBuffer(LPVOID pFileBuffer,LPVOID* ppImageBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;

    if(!pFileBuffer)
    {
        printf("FileBuffer函式呼叫失敗\n");
        return 0;
    }
    printf("%x\n",pFileBuffer);
    //判斷是否是PE檔案
    pDosHeader=(PIMAGE_DOS_HEADER)pFileBuffer;
    if(pDosHeader->e_magic!=IMAGE_DOS_SIGNATURE)
    {
        printf("不是有效的MZ標志\n");
        return 0;
    }

    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
    if(pNTHeader->Signature!=IMAGE_NT_SIGNATURE)
    {
        printf("不是有效的PE標志\n");
        return 0;
    }

    pFileHeader=(PIMAGE_FILE_HEADER)(((DWORD)pNTHeader)+4);    
    pOptionalHeader=(PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader+20);
    //開辟ImageBuffer空間
    *ppImageBuffer=malloc(pOptionalHeader->SizeOfImage+pOptionalHeader->SectionAlignment);//增加節才加上SectionAligement
    if(!*ppImageBuffer)
    {
        printf("開辟ImageBuffer空間失敗");
        return 0;
    }
    printf("SizeOfImage%x\n",pOptionalHeader->SizeOfImage);
    //malloc清零
    memset(*ppImageBuffer,0,pOptionalHeader->SizeOfImage);

    //復制Headers
    printf("SizeOfHeader%x\n",pOptionalHeader->SizeOfHeaders);
    memcpy(*ppImageBuffer,pDosHeader,pOptionalHeader->SizeOfHeaders);

    //回圈復制節表
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
    for(int i=1;i<=pFileHeader->NumberOfSections;i++,pSectionHeader++)
    {
        memcpy((LPVOID)((DWORD)*ppImageBuffer+pSectionHeader->VirtualAddress),(LPVOID)((DWORD)pFileBuffer+pSectionHeader->PointerToRawData),pSectionHeader->SizeOfRawData);
        printf("%d\n",i);
    }
    printf("拷貝完成\n");
    return pOptionalHeader->SizeOfImage;
}

//AddSection
LPVOID AddSection(LPVOID pImageBuffer)
{
    if(!pImageBuffer)
    {
        printf("pImageBuffer引數傳入失敗\n");
        return 0;
    }

    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;
    PIMAGE_SECTION_HEADER pNewSectionTable=NULL;

    pDosHeader=(PIMAGE_DOS_HEADER)pImageBuffer;
    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pImageBuffer+pDosHeader->e_lfanew);
    pFileHeader=(PIMAGE_FILE_HEADER)((DWORD)pNTHeader+4);
    pOptionalHeader=(PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader+20);
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);

    //判斷檔案對齊和記憶體對齊
    if(pOptionalHeader->FileAlignment==pOptionalHeader->SectionAlignment)
    {
        printf("檔案對齊和記憶體對齊相等\n");
        // 判斷是否有足夠的空間新增節表
        DWORD SizeOfSectionTable=0x28;  // 要加的節表為40個位元組  但需要有80個位元組的空白區
        DWORD FreeBase=((DWORD)pOptionalHeader->SizeOfHeaders-((DWORD)pSectionHeader+pFileHeader->NumberOfSections*SizeOfSectionTable-(DWORD)pImageBuffer));
        if(FreeBase<SizeOfSectionTable*2) //但需要有80個位元組的空白區
        {
            printf("沒有足夠的空間新增節表!!!\n");   //可以把PE標記以下的PE頭,可選PE頭,節表整體上移,然后修改e_lfnew
            free(pImageBuffer);
            return 0;
        }
        printf("有足夠的空間新增節表!!!\n");

        //修改NumberOfSection
        pFileHeader->NumberOfSections=pFileHeader->NumberOfSections+1;
        printf("NumberOfSection=%d\n",pFileHeader->NumberOfSections);

        //修改SizeOfImage
        printf("SizeOfImage=%x\n",pOptionalHeader->SizeOfImage);
        pOptionalHeader->SizeOfImage=pOptionalHeader->SizeOfImage+pOptionalHeader->SectionAlignment;
        printf("SizeOfImage=%x\n",pOptionalHeader->SizeOfImage);

        //這里就不同擴大ImageBuffer了,上面已經增加了

        //填寫新的節表(復制.text節表然后再修正)
        pNewSectionTable=(PIMAGE_SECTION_HEADER)((DWORD)pSectionHeader+(pFileHeader->NumberOfSections-1)*SizeOfSectionTable);
        //開始復制.text
        memcpy(pNewSectionTable,pSectionHeader,SizeOfSectionTable);
        //修正新的節表
        //回圈到倒數第二個節表
        for(int i=1;i<pFileHeader->NumberOfSections-1;i++,pSectionHeader++)
        {
            printf("%d\n",i);
        }
        pNewSectionTable->Misc.VirtualSize=pOptionalHeader->SectionAlignment;
        printf("%x\n",pNewSectionTable->Misc.VirtualSize);
        pNewSectionTable->VirtualAddress=pSectionHeader->VirtualAddress+
            max(pSectionHeader->SizeOfRawData,pSectionHeader->VirtualSize);
        printf("%x\n",pNewSectionTable->VirtualAddress);
        pNewSectionTable->SizeOfRawData=https://www.cnblogs.com/SVicen/archive/2022/10/05/pOptionalHeader->SectionAlignment;
        printf("%x\n",pNewSectionTable->SizeOfRawData);
        pNewSectionTable->PointerToRawData=https://www.cnblogs.com/SVicen/archive/2022/10/05/pSectionHeader->PointerToRawData+pSectionHeader->SizeOfRawData;
        printf("%x\n",pNewSectionTable->PointerToRawData);    
        printf("新的節表修正完成!!!\n");

        return pImageBuffer;
    }
    else
    {
        printf("記憶體對齊和檔案對齊不相等\n");
        printf("檔案對齊和記憶體對齊相等\n");
        // 判斷是否有足夠的空間新增節表
        DWORD SizeOfSectionTable=0x28;
        DWORD FreeBase=((DWORD)pOptionalHeader->SizeOfHeaders-((DWORD)pSectionHeader+pFileHeader->NumberOfSections*SizeOfSectionTable-(DWORD)pImageBuffer));
        if(FreeBase<SizeOfSectionTable*2)
        {
            printf("沒有足夠的空間新增節表!!!\n");
            free(pImageBuffer);
            return 0;
        }
        printf("有足夠的空間新增節表!!!\n");

        //修改NumberOfSection
        pFileHeader->NumberOfSections=pFileHeader->NumberOfSections+1;
        printf("NumberOfSection=%d\n",pFileHeader->NumberOfSections);

        //修改SizeOfImage
        printf("SizeOfImage=%x\n",pOptionalHeader->SizeOfImage);
        pOptionalHeader->SizeOfImage=pOptionalHeader->SizeOfImage+pOptionalHeader->SectionAlignment;
        printf("SizeOfImage=%x\n",pOptionalHeader->SizeOfImage);

        //這里就不用擴大ImageBuffer了,上面已經增加了
        //填寫新的節表(復制.txt節表然后再修正)
        pNewSectionTable=(PIMAGE_SECTION_HEADER)((DWORD)pSectionHeader+(pFileHeader->NumberOfSections-1)*SizeOfSectionTable);
        //開始復制.text
        memcpy(pNewSectionTable,pSectionHeader,SizeOfSectionTable);
        //修正新的節表
        //回圈到倒數第二個節表
        for(int i=1;i<pFileHeader->NumberOfSections-1;i++,pSectionHeader++)
        {
            printf("%d\n",i);
        }
        pNewSectionTable->Misc.VirtualSize=pOptionalHeader->SectionAlignment;
        printf("%x\n",pNewSectionTable->Misc.VirtualSize);
        DWORD RawSize=pSectionHeader->SizeOfRawData;
        printf("%x????\n",RawSize);
        //這里因為檔案對齊和記憶體對齊不一樣,所以需要對齊
        while(RawSize%pOptionalHeader->SectionAlignment!=0)
        {
            RawSize++;
            //printf("RawSize=%x\n",RawSize);
        }
        pNewSectionTable->VirtualAddress=pSectionHeader->VirtualAddress+RawSize;
        printf("%x\n",pNewSectionTable->VirtualAddress);
        pNewSectionTable->SizeOfRawData=https://www.cnblogs.com/SVicen/archive/2022/10/05/pOptionalHeader->SectionAlignment;
        printf("%x\n",pNewSectionTable->SizeOfRawData);
        // 新增節的PointerToRawData 等于 之前最后一個節的PointerToRawData + 這個節的SizeOfRawData
        pNewSectionTable->PointerToRawData=https://www.cnblogs.com/SVicen/archive/2022/10/05/pSectionHeader->PointerToRawData+pSectionHeader->SizeOfRawData;
        printf("%x\n",pNewSectionTable->PointerToRawData);    
        printf("新的節表修正完成!!!\n");
        return pImageBuffer;    
    } 
}

//ImageBufferToFileBuffer
DWORD ImageBufferToFileBuffer(LPVOID pImageBuffer,LPVOID* ppBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader=NULL;
    PIMAGE_NT_HEADERS pNTHeader=NULL;
    PIMAGE_FILE_HEADER pFileHeader=NULL;
    PIMAGE_OPTIONAL_HEADER pOptionalHeader=NULL;
    PIMAGE_SECTION_HEADER pSectionHeader=NULL;

    if(!pImageBuffer)
    {
        printf("error");
        return 0;
    }

    pDosHeader=(PIMAGE_DOS_HEADER)pImageBuffer;
    pNTHeader=(PIMAGE_NT_HEADERS)((DWORD)pImageBuffer+pDosHeader->e_lfanew);
    pFileHeader=(PIMAGE_FILE_HEADER)((DWORD)pNTHeader+4);
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + 20);
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);


    //得到FileBuffer的大小
    for(int i=1;i<pFileHeader->NumberOfSections;i++,pSectionHeader++)
    {
        printf("%d\n",i);
    }
    printf("numberofsection=%d\n",pFileHeader->NumberOfSections);
    printf("%x\n",pSectionHeader->Misc.VirtualSize);
    printf("%x\n",pSectionHeader->VirtualAddress);
    printf("%x\n",pSectionHeader->SizeOfRawData);
    printf("%x\n",pSectionHeader->PointerToRawData);

    //回圈到最后一個節表
    DWORD SizeOfBuffer=pSectionHeader->PointerToRawData+pSectionHeader->SizeOfRawData;
     printf("SizeOfBuffer=%x\n",SizeOfBuffer);
    //開辟空間
    *ppBuffer=malloc(SizeOfBuffer);
    if(!*ppBuffer)
    {
        printf("開辟Buffer空間失敗\n");
        return 0;
    }

    memset(*ppBuffer,0,SizeOfBuffer);

    //復制頭
    memcpy(*ppBuffer,pImageBuffer,pOptionalHeader->SizeOfHeaders);
    //復制節表
    pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader+pFileHeader->SizeOfOptionalHeader);
    printf("woc\n");
    for(int j=1;j<=pFileHeader->NumberOfSections;j++,pSectionHeader++)
    {
        printf("%d\n",j);
        memcpy((LPVOID)((DWORD)*ppBuffer+pSectionHeader->PointerToRawData),(LPVOID)((DWORD)pImageBuffer+pSectionHeader->VirtualAddress),pSectionHeader->SizeOfRawData);
    }
    printf("拷貝完成\n");
    return SizeOfBuffer; 
}

//存貯到新的exe
BOOL MemeryToFile(LPVOID pBuffer,DWORD SizeOfBuffer)
{
    FILE* fpw=fopen("C://Documents and Settings//ma_lic//桌面//NEWSECIISPutScanner.exe","wb");
    if(!fpw)
    {
        printf("fpw error");
        return false;
    }
    if (fwrite(pBuffer, 1, SizeOfBuffer, fpw) == 0)
    {
        printf("fpw fwrite fail");
        return false;
    }
    fclose(fpw);            
    fpw = NULL;
    printf("success\n");
    return true;

}

int main()
{
    LPVOID pFileBuffer=NULL;
    LPVOID* ppFileBuffer=&pFileBuffer;
    LPVOID pImageBuffer=NULL;
    LPVOID* ppImageBuffer=&pImageBuffer;
    DWORD SizeOfFileBuffer=0;
    DWORD SizeOfImageBuffer=0;
    DWORD SizeOfBuffer=0;

    LPVOID pBuffer=NULL;
    LPVOID* ppBuffer=&pBuffer;


    //呼叫filebuffer函式
    SizeOfFileBuffer=ReadPEFile(ppFileBuffer);
    if(!SizeOfFileBuffer)
    {
        printf("FileBuffer函式呼叫失敗 \n");
        return 0;
    }
    pFileBuffer=*ppFileBuffer;
    printf("ni ma de");


    //呼叫FileBufferToImageBuffer函式
    SizeOfBuffer=FileBufferToImageBuffer(pFileBuffer,ppImageBuffer);

    if(!SizeOfBuffer)
    {
        printf("呼叫FileBufferToImageBuffer函式失敗");
        return 0;
    } 
    //呼叫AddSection函式
    pImageBuffer=AddSection(pImageBuffer);

    //呼叫ImageBufferToBuffer
    SizeOfBuffer=ImageBufferToFileBuffer(pImageBuffer,ppBuffer);
    pBuffer=*ppBuffer;
    if(!SizeOfBuffer)
    {
        printf("SizeOfBuffer error");
        return 0;
    }

    //呼叫MemeryToFile
    if(MemeryToFile(pBuffer,SizeOfBuffer)==false)
    {
        printf("end");
        return 0;
    } 
    return 0; 
}
//新增節另一種寫法
//**************************************************************************                                
//AddNewSection:新增節,可能含有頭抬升,函式將保存新增節后的檔案
//引數說明:                                
//pFileBuffer:FileBuffer指標    
//SizeOfFileBuffer:pFileBuffer指向的大小,也即FileBuffer大小
//SizeOfNewSectio:新增節的大小
//pNewFileBuffer:回傳指向新FileBuffer的指標
//回傳值說明:                                
//新FileBuffer的大小,NewFileBuffer的大小
//**************************************************************************    
int AddNewSection(IN LPVOID pFileBuffer,IN int SizeOfFileBuffer,IN DWORD SizeOfNewSection, OUT LPVOID* ppNewFileBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_NT_HEADERS pNTHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pLastSectionHeader = NULL;    //指向最后一個節表
    PIMAGE_SECTION_HEADER pNewSectionHeader = NULL;        //指向最后一個節表的下一個節表,即不存在的節表作為新開辟的節表
    bool isUplift = false;
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
    //首個節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)    //注意這里i從1開始 i < NumberOfSections
    {
    }    //出回圈后pSectionHeader指向最后一個節表                
    pLastSectionHeader = pSectionHeader;
    pNewSectionHeader = pLastSectionHeader + 1;
    //節表結束的位置+兩個節表的大小 仍然≤ 頭大小,才可繼續,插一個留兩個為了插入一個新節表后仍有一個節表的位置填充0
    if ((DWORD)pNewSectionHeader + IMAGE_SIZEOF_SECTION_HEADER * 2 <= (DWORD)pFileBuffer + pOptionalHeader->SizeOfHeaders)
    {    //要節表后留出兩個節表的空位且全為0,保證其中沒有可能使用的資料,才允許插入新節表,
        PBYTE pTemp = (PBYTE)pNewSectionHeader;
        for (int i = 0; i < IMAGE_SIZEOF_SECTION_HEADER * 2; i++, pTemp++)
        {
            if (*pTemp)
            {
                printf("節表插入空位存在資料,需進行頭抬升\n");
                isUplift = true;
                break;
            }
        }
    }
    else
    {
        printf("無節表插入空位,需進行頭抬升\n");
        isUplift = true;
    }
    //isUplift = true; //頭抬升測驗 需要修改e_lfanew
    if (isUplift)
    {
        if ((DWORD)pFileBuffer + sizeof(IMAGE_DOS_HEADER) - (DWORD)pNTHeader >= IMAGE_SIZEOF_SECTION_HEADER * 2)
        {
            printf("可抬升NT頭\n");
            //開始拷貝,將NT頭拷貝到DOS頭結束之后,長度為NT頭開始到最后一個節表結束時的長度,即pNewSectionHeader
            memcpy((void*)((DWORD)pFileBuffer + sizeof(IMAGE_DOS_HEADER)), pNTHeader, (DWORD)pNewSectionHeader - (DWORD)pNTHeader);
            //拷貝后重置e_lfanew位置
            //printf(" e_lfanew: %x\n", pDosHeader->e_lfanew);
            pDosHeader->e_lfanew = sizeof(IMAGE_DOS_HEADER);
            //printf("e_lfanew: %x\n", pDosHeader->e_lfanew);
            //抬升后更新所有被抬升的頭指標
            //NT頭
            pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
            //PE頭
            pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);    //NT頭地址 + 4 為 FileHeader 首址
            //可選PE頭    
            pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER為固定值且不存在于PE檔案欄位中
            //首個節表
            pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
            for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)    //注意這里i從1開始 i < NumberOfSections
            {
            }    //出回圈后pSectionHeader指向最后一個節表                
            pLastSectionHeader = pSectionHeader;
            pNewSectionHeader = pLastSectionHeader + 1;
            //驗證代碼,判斷是否是有效的PE標志
            if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)    //基址pFileBuffer + lfanew 為 NTHeader首址
            {
                printf("抬升后驗證失敗,不是有效的PE標志\n");
                return 0;
            }
            printf("抬升成功!\n");
            //抬升成功后將最后一個節表后兩個節表位置的空間置零
            memset(pNewSectionHeader, 0, IMAGE_SIZEOF_SECTION_HEADER * 2);

        }
        else
        {
            printf("不可抬升NT頭,不可插入節表\n");
            free(pFileBuffer);
            return 0;
        }
    }
    //開始構造新節表
    printf("可插入節表\n");
    strcpy((char*)pNewSectionHeader->Name, (char*)".NewSec");
    //printf("Name:%s\n", pNewSectionHeader->Name);
    pNewSectionHeader->Misc.VirtualSize = SizeOfNewSection;
    //節區在記憶體中的偏移 = 記憶體中整個PE檔案映射的大小
    pNewSectionHeader->VirtualAddress = pOptionalHeader->SizeOfImage;
    //節區在檔案對齊中的大小  以VirtualSize記憶體對齊向上取整
    printf("FileAlignment:%x\n", pOptionalHeader->FileAlignment);
    pNewSectionHeader->SizeOfRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/Align(SizeOfNewSection, pOptionalHeader->FileAlignment);
    printf("SizeOfRawData:%x\n", pNewSectionHeader->SizeOfRawData);
    //節區在檔案中的偏移 = 檔案大小  (也可以是最后一個節區所在檔案中位置 + 最后一個節區在檔案對齊中的大小)二者一致
    pNewSectionHeader->PointerToRawData = https://www.cnblogs.com/SVicen/archive/2022/10/05/SizeOfFileBuffer;//pLastSectionHeader->PointerToRawData + pLastSectionHeader->SizeOfRawData;
    //printf("PointerToRawData:%x\n",pNewSectionHeader->PointerToRawData);
    pNewSectionHeader->PointerToRelocations = 0;
    //printf("PointerToRelocations:%x\n", pNewSectionHeader->PointerToRelocations);
    pNewSectionHeader->PointerToLinenumbers = 0;
    pNewSectionHeader->NumberOfRelocations = 0;
    pNewSectionHeader->NumberOfLinenumbers = 0;
    pNewSectionHeader->Characteristics = 0xe0000060;
    //新節表構造完畢, 修改節的數量
    pFileHeader->NumberOfSections++;
    //修改sizeOfImage的大小
    //節區在記憶體對齊中的大小  以VirtualSize記憶體對齊向上取整,對齊后加到SizeOfImage中
    printf("SectionAlignment: %x\nSizeOfImage:%x\n", pOptionalHeader->SectionAlignment, pOptionalHeader->SizeOfImage);
    pOptionalHeader->SizeOfImage += Align(SizeOfNewSection, pOptionalHeader->SectionAlignment);
    printf("SizeOfImage:%x\n", pOptionalHeader->SizeOfImage);
    //printf("Characteristics:%x\n", pNewSectionHeader->Characteristics);
    //開辟節區新空間
    int SizeOfNewFileBuffer = SizeOfFileBuffer + pNewSectionHeader->SizeOfRawData;
    *ppNewFileBuffer = malloc(SizeOfNewFileBuffer);
    //新節區之前的值全部復制,新節表部分已改變
    memcpy(*ppNewFileBuffer, pFileBuffer, SizeOfFileBuffer);
    //新節區全部置零
    memset((void*)((DWORD)*ppNewFileBuffer + pNewSectionHeader->PointerToRawData), 0, pNewSectionHeader->SizeOfRawData);
    return SizeOfNewFileBuffer;
}
匯出表

匯出表在可選PE頭的最后一個結構--目錄項陣列(16項)的第一個目錄就是匯出表,該結構只有兩個屬性,VirtualAddress和 Size(沒用),之后可以不再拉伸,直接算偏移,將VirtualAddress (RVA)轉換為FOA,再去檔案中找具體的匯出表結構,

匯出表就是提供函式的,不一定只有dll才可以提供函式,exe也可以

img

img

上圖中,AddressOfNames指向一個陣列,陣列里保存著一組RVA,每個RVA指向一個字串,這個字串即匯出的函式名,與這個函式名對應的是AddressOfNameOrdinals中的對應項,
獲取匯出函式地址時,先在AddressOfNames中找到對應的名字,(注意要將AddressOfNames的RVA轉為FOA)比如Func2,他在AddressOfNames中是第二項,然后從AddressOfNameOrdinals中取出第二項的值,這里是2,表示函式入口保存在AddressOfFunctions這個陣列中下標為2的項里,即第三項,取出其中的值,加上 模塊基地址 便是匯出函式的地址,
如果函式是以序號匯出的,那么查找的時候直接用序號減去Base(見上上圖X),得到的值就是函式在AddressOfFunctions中的下標,此時與AddressOfNameOrdinals這張表無關,函式地址表的下標從0開始,

注:序號表寬度為2

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    //以下重要
    DWORD   Name; // DLL的名稱地址
    DWORD   Base; // 索引基數,序號    的基數,按序號匯出函式的序號值從Base開始遞增,
    DWORD   NumberOfFunctions; // 所有匯出函式的數量,不一定準確,其值為序號最大-最小+1 若序號不連續則此值不準確
    DWORD   NumberOfNames;     // 按函式名字匯出的函式的數量,
    DWORD   AddressOfFunctions;  //一個RVA,指向一個DWORD陣列,陣列中的每一項是一個匯出函式的RVA,順序與匯出序號相同,
    DWORD   AddressOfNames;      //一個RVA,依然指向一個DWORD陣列,陣列中的每一項仍然是一個RVA,指向一個表示函式名字,
    //AddressOfNames與AddressOfFunctions大小可以不一樣,有的把函式名字隱藏了
    DWORD   AddressOfNameOrdinals;  //一個RVA,還是指向一個WORD陣列,
    //陣列中的每一項與AddressOfNames中的每一項對應,表示該名字的函式在AddressOfFunctions中的序號 
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

練習:

1、撰寫程式列印所有的匯出表資訊

2、撰寫函式 GetFunctionAddrByName ( FileBuffer指標,函式名指標)

3、撰寫函式 GetFunctionAddrByOrdinals ( FileBuffer指標,函式名匯出序號)

//1、撰寫程式列印所有的匯出表資訊
#include "Currency.h"
#include "windows.h"
#include "stdio.h"
VOID h324()        //輸出匯出表
{
    char FilePath[] = "Dll1.dll";    //CRACKME.EXE        CrackHead.exe
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
    DWORD nameFOA = NULL;
    DWORD AddressOfNamesFOA = NULL;
    DWORD AddressOfNameOrdinalsFOA = NULL;
    DWORD AddressOfFunctionsFOA = NULL;
    DWORD AddressOfFunctions = NULL;
    WORD Ordinal = NULL;
    char * name = NULL;

    DWORD result = NULL;
    typedef int(*lpPlus)(int, int);    //測驗查找出的函式用
    lpPlus myPlus;

    if (!ReadPEFile(FilePath, ppFileBuffer))
    {
        printf("檔案讀取失敗\n");
        return;
    }
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //匯出表
    pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    printf("DIRECTORY_ENTRY_EXPORT VirtualAddress:%x\n", pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    printf("FOA:%x\n", RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    printf("匯出表檔案名字串Name:%x\n", pExportDirectory->Name);
    printf("匯出函式起始序號Base:%d\n", pExportDirectory->Base);
    printf("匯出函式的個數:%d\n", pExportDirectory->NumberOfFunctions);
    printf("以函式名字匯出的函式個數NumberOfNames:%d\n", pExportDirectory->NumberOfNames);

    printf("*******函式地址表*******\n");
    AddressOfFunctionsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfFunctions);
    for (int i = 0; i < pExportDirectory->NumberOfFunctions; i++)
    {    //因Address表元素為4位元組,絕對地址加上i*4直接取第i個元素
        AddressOfFunctions = *(PDWORD)((DWORD)pFileBuffer + AddressOfFunctionsFOA + i * 4);
        printf("下標:%d,函式地址:%x\n", i, AddressOfFunctions);
    }
    printf("*******函式名稱表*******\n");
    //匯出表中的AddressOfNames為Rva,將其轉換為FOA得到AddressOfNamesFOA
    AddressOfNamesFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNames);
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {    //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer構成的絕對地址才能取出其中的值,
        //取出的值即Names地址表第i個name的Rva地址,轉成FOA得到name的FOA地址
        nameFOA = RVA2FOA(pFileBuffer, *(PDWORD)((DWORD)pFileBuffer + AddressOfNamesFOA));
        name = (char *)(nameFOA + (DWORD)pFileBuffer);//name的FOA加上pFileBuffer構成絕對地址,該地址才真正指向字串
        printf("下標:%d,函式名:%s\n", i, name);
        AddressOfNamesFOA += 4;    //往前走4位元組,指向Names地址表下一個元素,即下一個name地址
    }
    printf("*******函式序號表*******\n");
    AddressOfNameOrdinalsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNameOrdinals);//同Names表找法
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {
        Ordinal = *(PWORD)((DWORD)pFileBuffer + AddressOfNameOrdinalsFOA + i * 2);    //因為Ordinal表元素為2位元組,絕對地址加上i*2直接取第i個元素  序號表寬度為兩個位元組
        printf("下標:%d,Ordinal序號:%d\n", i, Ordinal);
    }
    result = (DWORD)GetFunctionAddrByName(pFileBuffer, "Plus");    //得到的是函式Rva地址
    printf("result:%x\n", result);
    result = (DWORD)GetFunctionAddrByOrdinals(pFileBuffer, 2);
    printf("result:%x\n", result);
}

//2、撰寫函式 GetFunctionAddrByName ( FileBuffer指標,函式名指標)
//**************************************************************************                                
//GetFunctionAddrByName:根據名字找到匯出表中的函式地址            
//引數說明:                                
//pFileBuffer:FileBuffer指標                        
//str: 函式名指標                    
//回傳值說明:                                
//回傳匯出表中的函式地址                                
//**************************************************************************    
LPVOID GetFunctionAddrByName(LPVOID pFileBuffer, char* str)
{
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
    DWORD nameFOA = NULL;
    DWORD AddressOfNamesFOA = NULL;
    DWORD AddressOfNameOrdinalsFOA = NULL;
    DWORD AddressOfFunctionsFOA = NULL;
    WORD Ordinal = NULL;
    char * name = NULL;
    int i = 0;
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //匯出表
    pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    //printf("NumberOfNames:%d\n", pExportDirectory->NumberOfNames);
    //匯出表中的AddressOfNames為Rva,將其轉換為FOA得到AddressOfNamesFOA
    AddressOfNamesFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNames);
    for (i = 0; i < pExportDirectory->NumberOfNames; i++)
    {    //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer構成的絕對地址才能取出其中的值,
        //取出的值即Names地址表第i個name的Rva地址,轉成FOA得到name的FOA地址
        nameFOA = RVA2FOA(pFileBuffer, *(PDWORD)((DWORD)pFileBuffer + AddressOfNamesFOA));
        name = (char *)(nameFOA + (DWORD)pFileBuffer);//name的FOA加上pFileBuffer構成絕對地址,該地址才真正指向字串
        if (!strcmp(str, name))   //要查找名字為 str 的函式地址
        {
            break;
        }
        AddressOfNamesFOA += 4;    //往前走4位元組,指向Names地址表下一個元素,即下一個name地址
    }
    if (i == pExportDirectory->NumberOfNames)
    {
        printf("找不到名為%s的函式!\n", str);
        return 0;
    }
    AddressOfNameOrdinalsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNameOrdinals);//同Names表找法
    Ordinal = *(PWORD)((DWORD)pFileBuffer + AddressOfNameOrdinalsFOA + i*2);    //因為Ordinal表元素為2位元組,絕對地址加上i*2直接取第i個元素   找到名字為str的函式對應的序號為Ordinal
    printf("i:%d,Ordinal:%d\n", i,Ordinal);
    AddressOfFunctionsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfFunctions);
//得到序號后,根據序號再去函式地址表中找對應的索引,注意這里的ordinal*4,因為在AddressOfFunctionsFOA一個序號對應四位元組的地址
    return (LPVOID)*(PDWORD)((DWORD)pFileBuffer + AddressOfFunctionsFOA + Ordinal * 4);//同上
}

//3、撰寫函式 GetFunctionAddrByOrdinals ( FileBuffer指標,函式名匯出序號)
//**************************************************************************                                
//GetFunctionAddrByOrdinals:根據序號找到匯出表中的函式地址            
//引數說明:                                
//pFileBuffer:FileBuffer指標                        
//ord:函式序號                
//回傳值說明:                                
//回傳匯出表中的函式地址                                
//**************************************************************************    
LPVOID GetFunctionAddrByOrdinals(LPVOID pFileBuffer, DWORD ord)
{
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
    DWORD nameFOA = NULL;
    DWORD AddressOfNameOrdinalsFOA = NULL;
    DWORD AddressOfFunctionsFOA = NULL;
    WORD Ordinal = NULL;
    int i = 0;
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //匯出表
    pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    if (ord - pExportDirectory->Base >= pExportDirectory->NumberOfNames || ord - pExportDirectory->Base < 0)
    {
        printf("找不到序號為%d的函式!\n", ord);
        return 0;
    }
    AddressOfFunctionsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfFunctions);//匯入表函式表地址轉FOA
    return (LPVOID)*(PDWORD)((DWORD)pFileBuffer + AddressOfFunctionsFOA + (ord - pExportDirectory->Base) * 4);//絕對地址 加上(ord-base)*4 直接取第ord-base個元素
}
重定位表

為什么需要重定位表

img

重定位表中每個塊存盤的是相對地址,偏移量

位置:可選PE頭中資料目錄項的第6個結構就是重定位表,注意該結構可能不止一個

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;        //重定位記憶體頁的起始RVA  就是下面的 X, 根據偏移去找要重定向的地址RVA,可以節省空間
    DWORD   SizeOfBlock;        //重定位塊的長度   單位是位元組,可根據它確定下一塊從什么時候開始
//  WORD    TypeOffset[1]; 后面跟的兩位元組的資料,有若干個,記錄了有多少地方需要修復,16位,低12位地址 + X就是要修復的RVA
//  因為記憶體中的頁大小為1000H,2^12就可以表示所有的頁內地址
} IMAGE_BASE_RELOCATION;
//之所以有多個塊,每個塊給一個起始RVA   因為要重定位的地址有可能差距比較大 比如說一個塊40000 一 個塊60000  一個節有多個塊

img

img

注:聯合體不是連續的,分為塊,每個塊的前八個位元組時確定的,后邊的不確定,SizeOfBlock中存的Y即每一塊的大小,每個塊其實就是一個分頁,把每一頁中需要修改的東西放到一塊里,根據基址 + 偏移 確定需要修改的位置

用12位就足以存盤所需要的的記憶體大小,但由于記憶體對齊,需要16位,但真正需要修改的地址等于低12位+VirtualAddress存的X的值

最后一個結構的VirtualAddress和SizeOfBlock都為0,

具體項的數量 = (SizeOfBlock - 8) / 2,(整個塊的大小 - 前八個位元組)/ 2 除以2是因為每個項占兩個位元組,

#include "Currency.h"
#include "windows.h"
#include "stdio.h"

VOID h325()        //列印重定向表所有內容
{
    char FilePath[] = "Dll1.dll";    //CRACKME.EXE        CrackHead.exe     Dll1.dll        R.DLL    LoadDll.dll
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_BASE_RELOCATION pRelocationTable = NULL;
    DWORD nameFOA = NULL;
    DWORD AddressOfNamesFOA = NULL;

    if (!ReadPEFile(FilePath, ppFileBuffer))
    {
        printf("檔案讀取失敗\n");
        return;
    }
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //重定向表
    pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));
    printf("RelocationTable VirtualAddress:%x\n", pOptionalHeader
           ->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    if (!pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress)
    {
        printf("該檔案沒有重定向表!\n");
        return;
    }
    int i = 1;
    while ( pRelocationTable->VirtualAddress && pRelocationTable->SizeOfBlock)
    {
        printf("第%d個塊VirtualAddress:%x\n", i, pRelocationTable->VirtualAddress);
        printf("第%d個塊SizeOfBlock:%x\n", i,pRelocationTable->SizeOfBlock);
        printf("第%d個塊項數:%d\n", i, (pRelocationTable->SizeOfBlock - 8)/2);
        pRelocationTable = (PIMAGE_BASE_RELOCATION)(pRelocationTable->SizeOfBlock + (DWORD)pRelocationTable);
        i++;
    }
}
移動匯出表

第一步:在DLL中新增一個節,并回傳新增后的FOA

第二步:復制 AddressOfFunctions (函式地址表)進這個FOA的位置

? 長度:4*NumberOfFunctions (每個表項4位元組)

第三步:復制 AddressOfNameOrdinals(函式序號表)注意是無縫銜接,接上面的位置繼續復制
長度:NumberOfNames*2(每個表項2位元組)

第四步:復制AddressOfNames (函式名稱表) 同樣無縫銜接,下同
長度:NumberOfNames*4 (每個表項4位元組)

第五步:復制所有的函式名字串(長度不確定)
長度不確定,注意:復制時需要修復 AddressOfNames 逐個指向這些字串首址

第六步:復制IMAGE_EXPORT_DIRECTORY結構

第七步:修復IMAGE_EXPORT_DIRECTORY結構中的

AddressOfFunctions
AddressOfNameOrdinals
AddressOfNames

第八步:修復目錄項中的 VirtualAddress 值,指向新的 IMAGE_EXPORT_DIRECTORY

這里要梳理清楚在移動程序中的各個引數,在什么時候轉換為FOA,什么時候轉換為RVA,

定位匯出表的地址時,需要將IMAGE_SECTION_HEADER[0].VirtualAddress的RVA->FOA,以便于在檔案中定位匯出表的地址,

在移動匯出表的AddressOfFunction、AddressOfNameOrdinals、AddressOfNames時,需要將RVA->FOA以便于在檔案中復制

在移動AddressOfNames中的地址指向的函式名字串時,要將AddressOfNames中的地址RVA->FOA,并且在移動完后,修改AddressOfNames中存盤的地址時,要FOA->RVA,

將匯出表中的AddressOfFunctions、AddressOfNameOrdinals、AddressOfNames 地址修改為新復制的對應的地址時,要將新復制的對應的地址FOA->RVA,

修復資料目錄項中的VirualAddress時,要將新復制的匯出表的存盤地址FOA->RVA

上述流程大致為下圖所示:(主要還是理解文字)
image-20220327102241414

#include "Currency.h"
#include "windows.h"
#include "stdio.h"
VOID h3261()    //移動匯出表的全部內容到新節,包括函式地址表,名稱表,序號表, 修改完后的Dll仍然好使
{
    char FilePath[] = "Dll1.dll";    //CRACKME.EXE        CrackHead.exe
    char CopyFilePath[] = "Dll12.dll";    //CRACKMEcopy.EXE       CrackHeadcopy.exe
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_SECTION_HEADER pNewSectionHeader = NULL;        //指向最后一個節表的下一個節表,即不存在的節表作為新開辟的節表
    int SizeOfFileBuffer;
    int SizeOfNewFileBuffer;

    LPVOID pNewFileBuffer = NULL;
    LPVOID* ppNewFileBuffer = &pNewFileBuffer;
    DWORD nameFOA = NULL;
    DWORD AddressOfNamesFOA = NULL;
    DWORD AddressOfNameOrdinalsFOA = NULL;
    DWORD AddressOfFunctionsFOA = NULL;
    DWORD AddressOfFunctions = NULL;
    WORD Ordinal = NULL;
    char* name = NULL;

    DWORD result = NULL;
    typedef int(*lpPlus)(int, int);    //測驗查找出的函式用

    DWORD P = 0; //相對新節的偏移即相對pNewSectionHeader->PointerToRawData的偏移,會隨著資料填充而一直變化,初值必須為0

    SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);    //pFileBuffer即指向已裝載到記憶體中的exe首部
                                                                /*pFileBuffer = *ppFileBuffer;*/
    if (!SizeOfFileBuffer)
    {
        printf("檔案讀取失敗\n");
        return;
    }
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //匯出表
    pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    printf("DIRECTORY_ENTRY_EXPORT VirtualAddress:%x\n", pOptionalHeader
           ->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    printf("FOA:%x\n", RVA2FOA(pFileBuffer, pOptionalHeader
              ->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));
    printf("匯出表檔案名字串Name:%x\n", pExportDirectory->Name);
    printf("匯出函式起始序號Base:%d\n", pExportDirectory->Base);
    printf("匯出函式的個數:%d\n", pExportDirectory->NumberOfFunctions);
    printf("以函式名字匯出的函式個數NumberOfNames:%d\n", pExportDirectory->NumberOfNames);

    printf("*******函式地址表*******\n");
    AddressOfFunctionsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfFunctions);
    for (int i = 0; i < pExportDirectory->NumberOfFunctions; i++)
    {    //因Address表元素為4位元組,絕對地址加上i*4直接取第i個元素
        AddressOfFunctions = *(PDWORD)((DWORD)pFileBuffer + AddressOfFunctionsFOA + i * 4);
        printf("下標:%d,函式地址:%x\n", i, AddressOfFunctions);
    }
    printf("*******函式名稱表*******\n");
    //匯出表中的AddressOfNames為Rva,將其轉換為FOA得到AddressOfNamesFOA
    AddressOfNamesFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNames);
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {    //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer構成的絕對地址才能取出其中的值,
        //取出的值即Names地址表第i個name的Rva地址,轉成FOA得到name的FOA地址
        nameFOA = RVA2FOA(pFileBuffer, *(PDWORD)((DWORD)pFileBuffer + AddressOfNamesFOA + i * 4));//真正的FOA
        name = (char*)(nameFOA + (DWORD)pFileBuffer);//name的FOA加上pFileBuffer構成絕對地址,該地址才真正指向字串
        printf("下標:%d,函式名:%s\n", i, name);
        //AddressOfNamesFOA += 4;    //往前走4位元組,指向Names地址表下一個元素,即下一個name地址
    }
    printf("*******函式序號表*******\n");
    AddressOfNameOrdinalsFOA = RVA2FOA(pFileBuffer, pExportDirectory->AddressOfNameOrdinals);    //同Names表找法
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {
        Ordinal = *(PWORD)((DWORD)pFileBuffer + AddressOfNameOrdinalsFOA + i * 2);    //因為Ordinal表元素為2位元組,絕對地址加上i*2直接取第i個元素
        printf("下標:%d,Ordinal序號:%d\n", i, Ordinal);
    }
    result = (DWORD)GetFunctionAddrByName(pFileBuffer, "Plus");    //得到的是函式Rva地址
    printf("result:%x\n", result);


    result = (DWORD)GetFunctionAddrByOrdinals(pFileBuffer, 2);
    printf("result:%x\n", result);

    //新增節,可能頭抬升,因此全拷貝到pNewFileBuffer,之后上面的尋址得全部來一遍
    SizeOfNewFileBuffer = AddNewSection(pFileBuffer, SizeOfFileBuffer, 1000, ppNewFileBuffer);
    //引數分別為 原filebuffer指標,原大小,新節的大小,指向新節的指標  回傳值為新節區的大小
    //重新對pNewFileBuffer來一遍
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pNewFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNewFileBuffer + pDosHeader->e_lfanew + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
    //首個節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)    //注意這里i從1開始 i < NumberOfSections
    {
    }    //出回圈后pSectionHeader指向最后一個節表,也即新的節表
    pNewSectionHeader = pSectionHeader;
    //匯出表
    pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress));


    /*******************************************************************************************/
    //舊函式地址表
    AddressOfFunctionsFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfFunctions);
    //拷貝函式地址表
    //新filebuffer頭+新節表中的檔案偏移 作為拷貝目的地址, 新filebuffer頭 + 舊函式地址表檔案偏移 作為拷貝源,  拷貝長度 函式表項數 *4
    //P為相對新節的偏移即相對pNewSectionHeader->PointerToRawData的偏移,會隨著資料填充而一直變化
    memcpy((PVOID)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P), (PVOID)((DWORD)pNewFileBuffer + AddressOfFunctionsFOA), pExportDirectory->NumberOfFunctions * 4);

    //修改匯出表中函式地址表地址值           節頭 + 偏移
    pExportDirectory->AddressOfFunctions = pNewSectionHeader->VirtualAddress + P;
    //所有操作完畢,偏移可以往前加了    P相當于記錄了SizeOfRaw 拷貝完地址表還要接著利用P拷貝下面的序號表和函式名表
    P += pExportDirectory->NumberOfFunctions * 4;

    //新函式地址表測驗
    AddressOfFunctionsFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfFunctions);
    for (int i = 0; i < pExportDirectory->NumberOfFunctions; i++)
    {    //因Address表元素為4位元組,絕對地址加上i*4直接取第i個元素
        AddressOfFunctions = *(PDWORD)((DWORD)pNewFileBuffer + AddressOfFunctionsFOA + i * 4);
        printf("下標:%d,函式地址:%x\n", i, AddressOfFunctions);
    }


    /*******************************************************************************************/
    //舊序號表
    AddressOfNameOrdinalsFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfNameOrdinals);
    //拷貝序號表
    //新filebuffer頭+新節表中的檔案偏移+上一次拷貝后P記錄的相對新節的偏移  作為拷貝目的地址, 新filebuffer頭 + 舊函式地址表檔案偏移 作為拷貝源,  拷貝長度 序號表項數 *2
    memcpy((PVOID)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P ), (PVOID)((DWORD)pNewFileBuffer + AddressOfNameOrdinalsFOA), pExportDirectory->NumberOfNames * 2);
    //修改匯出表中序號表地址值   這里修正之后最后再移動完匯出表后就不用修正了     同樣是節頭+偏移
    pExportDirectory->AddressOfNameOrdinals = pNewSectionHeader->VirtualAddress + P;
    //所有操作完畢,偏移可以往前加了
    P += pExportDirectory->NumberOfNames * 2;

    //新序號表測驗
    AddressOfNameOrdinalsFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfNameOrdinals);
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {
        Ordinal = *(PWORD)((DWORD)pNewFileBuffer + AddressOfNameOrdinalsFOA + i * 2);    //因為Ordinal表元素為2位元組,絕對地址加上i*2直接取第i個元素
        printf("下標:%d,Ordinal序號:%d\n", i, Ordinal);
    }

    /*******************************************************************************************/
    //舊函式名表
    AddressOfNamesFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfNames);
    //拷貝舊函式名表,這里因為字串地址未確定,所以全拷貝0
    //拷貝目的地址同上,值0,長度為函式名表*4,因為表內裝的是地址值,每項4位元組
    memset((PVOID)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P), 0, pExportDirectory->NumberOfNames * 4);
    //記錄下函式名表中的項在FOA中的地址,即現在按照這個地址真實找得到表所在的地址,∵過了之后P就改變了,∴需先記錄下
    PDWORD NamesTableFOA = (PDWORD)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P);
    //修改匯出表中函式名表地址值   
    pExportDirectory->AddressOfNames = pNewSectionHeader->VirtualAddress + P;
    //所有操作完畢,偏移可以往前加了
    P += pExportDirectory->NumberOfNames * 4;

    //接下來拷貝每一個字串
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {    //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer構成的絕對地址才能取出其中的值,
        //取出的值即Names地址表第i個name的Rva地址,轉成FOA得到name的FOA地址
        nameFOA = RVA2FOA(pNewFileBuffer, *(PDWORD)((DWORD)pNewFileBuffer + AddressOfNamesFOA));
        name = (char*)(nameFOA + (DWORD)pNewFileBuffer);//name的FOA加上pFileBuffer構成絕對地址,該地址才真正指向字串
        printf("下標:%d,函式名:%s,字串長度%d\n", i, name,strlen(name));
        //拷貝目的為新filebuffer頭+新節表中的檔案偏移+上一次拷貝后P記錄的相對新節的偏移, 拷貝源為name(name本身是個指向字串的地址, 拷貝長度為name字串長度
        memcpy((PVOID)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P), name, strlen(name)+1);   // + 1 名字后面還有 '\0'
        printf("下標:%d,函式名:%s\n", i, (char*)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P));

        //函式名表中的項 = 新的函式名Rva地址  這里用先前記錄的FOA而不用pExportDirectory->AddressOfNames ∵AddressOfNames是RVA,尋址麻煩,先前記錄的FOA能直接找到真正地址         修復名字地址表的地址
        * NamesTableFOA = pNewSectionHeader->VirtualAddress + P;
        //所有操作完畢,偏移可以往前加了
        P += strlen(name)+1;  //復制完一個名字后才知道下一個地址在哪里  因為名字的長度不確定
        NamesTableFOA++; //該值是PDWORD ++會往前走四位元組
        AddressOfNamesFOA += 4;    //往前走4位元組,指向Names地址表下一個元素,即下一個name地址
    }

    //測驗
    printf("*******函式名稱表*******\n");
    //匯出表中的AddressOfNames為Rva,將其轉換為FOA得到AddressOfNamesFOA
    AddressOfNamesFOA = RVA2FOA(pNewFileBuffer, pExportDirectory->AddressOfNames);
    for (int i = 0; i < pExportDirectory->NumberOfNames; i++)
    {    //AddressOfNamesFOA只是Names表的FOA地址,需加上pFileBuffer構成的絕對地址才能取出其中的值,
        //取出的值即Names地址表第i個name的Rva地址,轉成FOA得到name的FOA地址
        nameFOA = RVA2FOA(pNewFileBuffer, *(PDWORD)((DWORD)pNewFileBuffer + AddressOfNamesFOA));
        name = (char*)(nameFOA + (DWORD)pNewFileBuffer);//name的FOA加上pFileBuffer構成絕對地址,該地址才真正指向字串
        printf("下標:%d,函式名:%s\n", i, name);
        AddressOfNamesFOA += 4;    //往前走4位元組,指向Names地址表下一個元素,即下一個name地址
    }

    /***********************************************************************************************/
    //整個匯出表轉移到新節   把匯出表包含的三個表移動完后  最后移動匯出表
    printf("匯出表長度:%d\n",sizeof(IMAGE_EXPORT_DIRECTORY));
    //拷貝匯出表到新節
    memcpy((PVOID)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData + P), pExportDirectory, sizeof(IMAGE_EXPORT_DIRECTORY));
    //改變匯出表地址  
    pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = pNewSectionHeader->VirtualAddress + P;
    MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
    free(pNewFileBuffer);
    free(pFileBuffer); 
}
移動重定位表

重定位表的移動相比匯出表就簡單太多了,因為重定位表相當于只有一個大表,統計一下這個“大表”的大小,全部移動到新節就行了,因為是使用下一塊 VirtualAddress 和 SizeOfBlock 是否全0 來判斷重定位表是否結束的,所以拷貝的時候也把這8位元組的全0內容也加上一起拷貝進新節,

重定位表是由于在代碼中寫入的絕對地址,而 DLL 不能按照設想的 ImageBase 作起始加載位置去了別的地方占坑,那么需要根據重定位表記錄的這些絕對地址在記憶體中的位置(RVA),逐一去到這些位置修復絕對地址,而產生的表,

表中記錄的是代碼中 絕對地址 所在的RVA,需要根據偏移去到這個RVA修改掉這四位元組的絕對地址

修改 DLL 的 ImageBase 來模擬這個程序, 相當于模擬 DLL 加載時 占不住原來的 ImageBase 的情況,所以只能在作業系統安排的其他位置加載,

修復重定位地址:這個被安排的位置就相當于我們修改的 ImageBase根據這個新占坑位置和原來 ImageBase 之間的增量來進行重定位表修正,然后存盤.看DLL是否可以使用,

 //移動重定位表到新添加的節
#include "Currency.h"
#include "windows.h"
#include "stdio.h"

void h3262()        
{
    char FilePath[] = "CRACKME.EXE";    //CRACKME.EXE        CrackHead.exe     Dll1.dll        R.DLL    LoadDll.dll
    char CopyFilePath[] = "CRACKMEcopy.EXE";    //CRACKMEcopy.EXE       CrackHeadcopy.exe
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參

    int SizeOfFileBuffer;
    int SizeOfNewFileBuffer;
    LPVOID pNewFileBuffer = NULL;
    LPVOID* ppNewFileBuffer = &pNewFileBuffer;

    PIMAGE_DOS_HEADER pDosHeader = NULL;
    PIMAGE_FILE_HEADER pFileHeader = NULL;
    PIMAGE_SECTION_HEADER pSectionHeader = NULL;
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
    PIMAGE_BASE_RELOCATION pRelocationTable = NULL;
    PIMAGE_SECTION_HEADER pNewSectionHeader = NULL;        //指向最后一個節表的下一個節表,即不存在的節表作為新開辟的節表
    DWORD nameFOA = NULL;
    DWORD AddressOfNamesFOA = NULL;

    SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);    //pFileBuffer即指向已裝載到記憶體中的exe首部
                                                                /*pFileBuffer = *ppFileBuffer;*/
    if (!SizeOfFileBuffer)
    {
        printf("檔案讀取失敗\n");
        return;
    }
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //可選PE頭      簡化后的處理
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer + pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
    //重定向表
    pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));
    printf("RelocationTable VirtualAddress:%x\n", pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    if (!pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress)
    {
        printf("該檔案沒有重定向表!\n");
        return;
    }
    int i = 1;
    int SizeOfRelocationTable = 0;    //重定向表的大小,單位位元組
    while (pRelocationTable->VirtualAddress && pRelocationTable->SizeOfBlock)
    {
        printf("第%d個塊VirtualAddress:%x\n", i, pRelocationTable->VirtualAddress);
        printf("第%d個塊SizeOfBlock:%x\n", i, pRelocationTable->SizeOfBlock);
        SizeOfRelocationTable += pRelocationTable->SizeOfBlock;
        printf("第%d個塊項數:%d\n", i, (pRelocationTable->SizeOfBlock - 8) / 2);
        pRelocationTable = (PIMAGE_BASE_RELOCATION)(pRelocationTable->SizeOfBlock + (DWORD)pRelocationTable);
        i++;
    }
    SizeOfRelocationTable += 8;    //加上最后全0的八位元組
    printf("重定位表的大小:%d\n", SizeOfRelocationTable);
    //新增節,可能頭抬升,因此全拷貝到pNewFileBuffer,之后上面的尋址得全部來一遍
    SizeOfNewFileBuffer = AddNewSection(pFileBuffer, SizeOfFileBuffer, SizeOfRelocationTable, ppNewFileBuffer); //引數分別為 原filebuffer指標,原大小,新節的大小,指向新節的指標  回傳值為新節區的大小
    //重新對pNewFileBuffer來一遍
    //Dos頭
    pDosHeader = (PIMAGE_DOS_HEADER)pNewFileBuffer;    // 強轉 DOS_HEADER 結構體指標
    //PE頭
    pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNewFileBuffer + pDosHeader->e_lfanew + 4);    //NT頭地址 + 4 為 FileHeader 首址
    //可選PE頭
    pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
    //首個節表
    pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)    //注意這里i從1開始 i < NumberOfSections
    {
    }    //出回圈后pSectionHeader指向最后一個節表,也即新的節表  
    pNewSectionHeader = pSectionHeader;

    //舊重定向表絕對地址
    pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));

    //拷貝重定位表到新添加的節  新filebuffer頭+新節表中的檔案偏移 作為拷貝目的地址     舊重定向表絕對地址為拷貝源 
    memcpy(PVOID((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData ), pRelocationTable, SizeOfRelocationTable);
    //修改重定位表的地址值           節頭 + 偏移
    pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress = pNewSectionHeader->VirtualAddress;

    //輸出測驗
    //計算新的重定向表  注意這里新的重定向表 是原來的VirtualAddress轉為FOA 再加上pNewFileBuffer 新的imagebase 
    pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress));
    printf("RelocationTable VirtualAddress:%x\n", pOptionalHeader
           ->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    if (!pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress)
    {
        printf("該檔案沒有重定向表!\n");
        return;
    }
    i = 1;
    while (pRelocationTable->VirtualAddress && pRelocationTable->SizeOfBlock)
    {
        printf("第%d個塊VirtualAddress:%x\n", i, pRelocationTable->VirtualAddress);
        printf("第%d個塊SizeOfBlock:%x\n", i, pRelocationTable->SizeOfBlock);
        printf("第%d個塊項數:%d\n", i, (pRelocationTable->SizeOfBlock - 8) / 2);
        pRelocationTable = (PIMAGE_BASE_RELOCATION)(pRelocationTable->SizeOfBlock + (DWORD)pRelocationTable);
        i++;
    }

    MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
    free(pNewFileBuffer);
    free(pFileBuffer);
}

流程

1、File-> FileBuffer
2、添加節
3、定位重定位表
4、移動重定位表
5、修復重定位的地址
6、存盤

IAT表與匯入表

IAT表

img

在我們呼叫 dll 函式的時候,發現代碼中的匯編,是通過間接尋址的,也就是不直接 call 函式地址,而是通過一個中間地址再跳轉的,

比如呼叫MessageBox這類系統函式的時候(這也是個 dll 中的函式),那么它的匯編會為 call dword ptr [ (004322d4) ] 通過間接尋址,004322d4里面存的是X就可以跳到X,這個X變數可以任意指定,004322d4是.exe領空的,而間接尋址的X可以不是.exe領空,比如這次運行中X會變為77d5050b,就跳到了.dll 的領空了

前面也講到,因為 dll 是隨著其他程式加載進4GB記憶體的,就可能占不住自己的 ImageBase,因而它提供的匯出函式位置就會隨之變化,所以 call 的時候不能把相應 dll 函式的位置寫死,得根據dll加載情況而動態的變化,所以采用間接尋址的方式來實作動態變化,(因為你不知道這個函式的領空和實際地址在哪,只能先寫自己代碼領空的位置在 call 后面,作業系統根據 dll 實際呼叫情況,把真正函式地址填進類似這個 004322d4 的間接區域)

那么當編譯的.exe 是未運行的檔案狀態時,這個 004322d4 指向的地址(還要根據 RVA 轉 FOA,減去 Imagebase 才能在檔案中找到這個位置),只是一個 MessageBox.USER32.dll 字串,但記憶體中這 004322d4 指向的地址卻是一個 77d5050b 絕對地址,那么他是什么時候改變的呢?是當.exe 和用到的所有.dll 都貼進4GB空間(并且都修復了各自的重定位表)的時候,這個值就從字串被修改為絕對地址,
而這些存放著.dll 和函式資訊并將要被轉換為絕對地址的類似 004322d4 的中間地址,連起來就是IAT表(import name table,匯入名稱表)

注:只有當DDL檔案全部貼到記憶體里后,IAT表里的值才可以真正的確定下來,考慮到dll的地址可能會發生變化

匯入表??

image-20220327161843294

和之前的好多表一樣,VirtualAddress 指向多個一樣的匯入表結構,最后 sizeOf (IMAGE_IMPORT_DESCRIPTOR)個0代表匯入表結束

Name 是指向 dll 名的 RVA,只記錄了一個 dll 名,所以匯入表會有若干張,順序無縫存放,一張匯入表就是一個要使用的 dll,以全0作為結尾,所以一個exe程式會有多個匯入表

OriginalFirstThunk(匯入表結構第一個成員)指向一張叫 INT 的表(import name table,匯入名稱表),存的是若干個 IMAGE_THUNK_DATA 結構,最后以0結尾作結束標志,

FirstThunk(匯入表結構最后一個成員)也指向一張表,IAT表(import address table,匯入地址表),找到這張表有兩種方式,一種就是通過匯入表這里找到,第二種就是通過資料目錄表,倒數第三個項就是指向的 IAT 表,IMAGE_DIRECTORY_ENTRY_IAT,也是以0結束,在檔案加載前,這兩個表存的內容完全一致,都是存盤的IMAGE_THUNK_DATA 結構(IMAGE_THUNK_DATA32)(詳見下下圖),注意雖然一致,但 INT 表和 IAT 表是兩塊不同的空間,分別記錄的都是程式中使用到的 dll 函式,不使用的 dll 函式不會記錄,
image-20220327162025697

注:加載前INT與IAT兩個表存的內容完全一樣

原因:1、留一個函式名稱的備份 INT表,運行時IAT表變為地址,如果還要查找函式名,必須有備份,INT就做了這件事

更新IAT表時:通過INT去找函式名稱,再根據之前寫的GetFunctionAddrByName()函式得到函式地址,填入IAT表

image-20220327163439401

在加載前,IMAGE_THUNK_DATA 只存一個 RVA,這個 RVA 指向上圖的IMAGE_IMPORT_BY_NAME 結構

在加載后,IAT 表變為存盤函式的地址了,所以才有文章最前面提到的,在呼叫 MassageBox 檔案加載前call間接尋址找的是一個字串,而檔案加載后這個間接尋址變為了函式的真正地址,而 INT 表不變,

上圖提到的 IMAGE_THUNK_DATA 結構其實只是一個四位元組的 聯合體union,

typedef struct _IMAGE_THUNK_DATA32 {                        
    union {                        
        PBYTE  ForwarderString;                        
        PDWORD Function;                        
        DWORD Ordinal;                         //序號
        PIMAGE_IMPORT_BY_NAME  AddressOfData;//指向IMAGE_IMPORT_BY_NAME
    } u1;                        
} IMAGE_THUNK_DATA32;                        
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;                        

typedef struct _IMAGE_IMPORT_BY_NAME {                        
    WORD    Hint;                        //可能為空,編譯器決定 如果不為空 是函式在匯出表中的索引
    BYTE    Name[1];                    //函式名稱,以0結尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;        

??注意,IMAGE_IMPORT_BY_NAME 中的 Name 并不只有 1 個位元組,而是一直遍歷到元素為 '\0' 為止,OriginalFirstThunk 和 FirstThunk 的遍歷 具體詳見下圖(這倆的遍歷都一樣的,其實都是遍歷的 IMAGE_THUNK_DATA)

??IMAGE_THUNK_DATA32為DWORD,由于匯出表有按名稱匯出和按序號匯出兩種形式,故定義了這個變數,最高位如果是1,那么去掉最高位剩下的就是函式的匯出序號,如果最高位不為1,該值就是指向按名字匯出的一個RVA

注意下面的系統函式GetProcAddr(m,函式名或函式匯出序號) ,m為module句柄,回傳函式句柄,系統會自動根據傳入引數的不同實施不同的尋找策略

img

注意 IMAGE_IMPORT_BY_NAME 中的 Hint 不是匯出序號,而是當前這個函式在匯出表函式地址表中的索引,但基本沒用,所以不一定是準確的,可以全部為0,而Name并不是一位元組,這里只是存盤了第一個字符,而是以’\0’結尾的不定長字串,

img

// 列印匯入表
VOID PrintImportTable(LPVOID pFileBuffer)
{
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)(pDosHeader->e_lfanew + (DWORD)pDosHeader + 4);
    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
    PIMAGE_SECTION_HEADER pSectionHeader = \
        (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);

    PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + \
        RvaToFoa(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress));

    // 嚴格來說應該是 sizeof(IMAGE_IMPORT_DESCRIPTOR) 個位元組為0表示結束
    while (pImportTable->OriginalFirstThunk || pImportTable->FirstThunk)  //外層回圈列印所有dll
    { 
        // 列印模塊名 pImportTable->Name 也是一個指向字串的RVA,  模塊名即dll名比如說kernel32.dll
        printf("%s\n", (LPCSTR)(RvaToFoa(pFileBuffer, pImportTable->Name) + (DWORD)pFileBuffer));
        // 遍歷INT表(import name table)     OriginalFirstThunkRVA指向INT
        printf("--------------OriginalFirstThunkRVA:%x--------------\n", pImportTable->OriginalFirstThunk);        
        PIMAGE_THUNK_DATA32 pThunkData = https://www.cnblogs.com/SVicen/archive/2022/10/05/(PIMAGE_THUNK_DATA32)((DWORD)pFileBuffer + /
            RvaToFoa(pFileBuffer, pImportTable->OriginalFirstThunk));
        while (*((PDWORD)pThunkData) != 0)  // 記憶體回圈列印所有函式
        {
            // IMAGE_THUNK_DATA32 是一個4位元組資料
            // 如果最高位是1,那么除去最高位就是匯出序號
            // 如果最高位是0,那么這個值是RVA 指向 IMAGE_IMPORT_BY_NAME
            if ((*((PDWORD)pThunkData) & 0x80000000) == 0x80000000)  //取出最高位
            {
                printf("按序號匯入 Ordinal:%04x\n", (*((PDWORD)pThunkData) & 0x7FFFFFFF)); //取出低31位
            }
            else  // 最高位不為1,整個32位作為指向函式名字表的RVA
            {
                PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pFileBuffer, 
                *((PDWORD)pThunkData)) + \(DWORD)pFileBuffer);
                //強轉為(PIMAGE_IMPORT_BY_NAME)后就可以直接 ->Hint  ->Name 了
                printf("按名字匯入 Hint:%04x Name:%s\n", pIBN->Hint, pIBN->Name);//Hint為索引序號
            }
            pThunkData++;
        }
        // 遍歷IAT表(import address table)   FirstThunkRVA指向IAT表
        printf("--------------FirstThunkRVA:%x--------------\n", pImportTable->FirstThunk);        
        pThunkData = https://www.cnblogs.com/SVicen/archive/2022/10/05/(PIMAGE_THUNK_DATA32)((DWORD)pFileBuffer + /
            RvaToFoa(pFileBuffer, pImportTable->FirstThunk));
        while (*((PDWORD)pThunkData) != 0)
        {
            // IMAGE_THUNK_DATA32 是一個4位元組資料
            // 如果最高位是1,那么除去最高位就是匯出序號
            // 如果最高位是0,那么這個值是RVA 指向 IMAGE_IMPORT_BY_NAME
            if ((*((PDWORD)pThunkData) & 0x80000000) == 0x80000000)
            {
                printf("按序號匯入 Ordinal:%04x\n", (*((PDWORD)pThunkData) & 0x7FFFFFFF));
            }
            else
            {
                PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pFileBuffer, 
                    *((PDWORD)pThunkData)) + \(DWORD)pFileBuffer);

                printf("按名字匯入 Hint:%04x Name:%s\n", pIBN->Hint, pIBN->Name);
            }
            pThunkData++;
        }
        pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pImportTable + sizeof(IMAGE_IMPORT_DESCRIPTOR));
    }    
}

IAT 表在檔案中存盤的內容未必是序號和函式名的 RVA,還有可能直接就是上章講的檔案運行加載進記憶體后呈現的真正的函式地址,Notepad就是這樣,而這個本來應該是加載后才做的事,在加載前的檔案狀態就已經做過了,就不用再等 exe 和所有 dll 挨個貼進記憶體空間,貼完后再挨個找 dll 函式地址才把 IAT 表改掉了,加快了程式的加載速度,在加載前的檔案就已經把這個事做了,

其實我們也可以自己做這個程序,完全是可行的,在 exe 檔案加載前根據匯入表已經知道他用哪些 dll,挨個找到這些 dll,查詢他們的匯出表依次得到每個函式的位置,當然還要 RVA 轉 FOA,加上 dll 的 Imagebase(dll 之間的 Imagebase 沖突了也就是需要重定向的時候就沒辦法了),把這些函式絕對地址挨個貼到 IAT 表中,

系結匯入表

我們的PE程式在加載的時候.我們知道. PE中匯入表的子表. IAT表.會填寫函式地址. 但是這就造成了一個問題.PE程式啟動慢.每次啟動都要給IAT表填寫函式地址.  我們可不可以在檔案中就給填寫好. 這樣是可以的,

PE中包含匯入表的優點是程式啟動快,但是其缺點也十分明顯,當存在dll地址重定位和dll修改更新,則系結匯入表也需要修改更新,

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {    
    DWORD   TimeDateStamp;    
    WORD    OffsetModuleName;    
    WORD    NumberOfModuleForwarderRefs;    
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows    
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;    

typedef struct _IMAGE_BOUND_FORWARDER_REF {    
    DWORD   TimeDateStamp;    
    WORD    OffsetModuleName;    
    WORD    Reserved;    
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;    

image-20220327190214873

image-20220327225525092

系結匯入表_IMAGE_BOUND_IMPORT_DESCRIPTOR 第一個成員就是真正的時間戳

在coff頭/標準PE頭 FILE_HEADER 中有個 TimeDateStamp欄位,記錄了檔案生成時的時間戳

exe 系結匯入表中的時間戳,和對應 dll 標準 PE 頭中的時間戳,如果吻合,證明是系結的時候的dll,沒有出錯,*如果不吻合證明dll已經被更新,或者 DLL 需要重定位的時候,exe 的系結匯入可能會出錯,則系統在加載檔案時會重新計算 IAT 中的值,不使用原本記錄的系結資訊,

第二個成員 OffsetModuleName 是 dll 名

第三個成員 NumberOfModuleForwarderRefs 說明了在當前所在這個系結匯入表IMAGE_BOUND_IMPORT_DESCRIPTOR 之后有幾個 _IMAGE_BOUND_FORWARDER_REF (依賴的)結構,這是個整數,如下圖所示,如果是2,則后跟兩個_IMAGE_BOUND_FORWARDER_REF 結構,再之后才是下一個系結匯入表IMAGE_BOUND_IMPORT_DESCRIPTOR,以此類推,最后以系結匯入表長度的內容為全 0 結束,

至于為什么有這個結構呢?是因為一個 exe 會直接使用好幾個 dll,這些 dll 如果系結了就是系結匯入表的內容,但這些直接使用的dll可能還會再使用其他的dll,這個IMAGE_BOUND_FORWARDER_REF 結構就是用來記錄間接使用的dll 的,

因為也是描述的 dll 資訊,所以IMAGE_BOUND_FORWARDER_REF 結構 其實也是和系結匯入表一樣的結構,第一個成員是時間戳,功能和系結匯入表的時間戳一樣,第二個成員也一樣是 dll 名,第三個成員是保留欄位,無意義,
上述表示 dll 名的第二個成員 OffsetModuleName 不是 RVA 也不是 FOA,是使用第一個系結匯入表的RVA(即第一個IMAGE_BOUND_IMPORT_DESCRIPTOR的RVA),加上OffsetModuleName 的值,得到的才是名字字串真正的 RVA,其他的系結匯入表結構和 所有IMAGE_BOUND_FORWARDER_REF 結構都一樣,計算方法都是用當前OffsetModuleName 的值 加上 第一個系結匯入表IMAGE_BOUND_IMPORT_DESCRIPTOR的RVA,注意全都是加第一個匯入表的RVA,

IAT中存盤的函式地址是dll未加載的地址,當PE檔案中匯入表未系結,IAT就與INT一樣,此時匯入表中的時間戳就為0;匯入表中的時間戳為-1時,當前的dll已經系結了,dll系結的真正時間戳存放于系結匯入表中(系結匯入表地址存放在資料目錄的第12項,IAT是第13項),
現在大多數情況,匯入表的TimeDateStamp都為0,而Windows早期的自帶軟體(如WinXP的notepad.exe)基本都采用了TimeDateStamp為-1的情況即包含系結匯入表的情況,

這里用的是 Windows XP 中取出來的notepad.exe,可以去xp虛擬機中搞到這個exe,win10找了很多exe,都是沒有系結匯入表的,沒辦法

#include "Currency.h"
#include "windows.h"
#include "stdio.h"

VOID h330()        //列印系結匯入表所有內容
{
    char FilePath[] = "notepad.exe";    //CRACKME.EXE   CrackHead.exe   Dll1.dll   R.DLL   notepad.exe  LoadDll.dll  PETool.exe 列印dll用最后一個看
    LPVOID pFileBuffer = NULL;                //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;    //傳進函式的形參
    if (!ReadPEFile(FilePath, ppFileBuffer))
    {
        printf("檔案讀取失敗\n");
        return;
    }

    // 頭查找
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;                                    
    PIMAGE_NT_HEADERS32 pImageNts = (PIMAGE_NT_HEADERS32)((DWORD)pFileBuffer + pDos->e_lfanew);
    PIMAGE_DATA_DIRECTORY pDir = 
                        &pImageNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT];
    PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImport = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer,pDir->VirtualAddress));
    //printf("VirtualAddress:%x\n", pDir->VirtualAddress);

    if (pDir->VirtualAddress == 0)
    {
        printf("沒有系結匯入表!\n");
        return;
    }

    //全 0 判斷,如果全 0 那就不回圈了,只要有一個欄位不等于 0 就繼續回圈
    while (pBoundImport->NumberOfModuleForwarderRefs != 0 || pBoundImport->OffsetModuleName != 0 || pBoundImport->TimeDateStamp != 0)
    {
        printf("TimeDateStamp:%x\n", pBoundImport->TimeDateStamp);

        // 當前 OffsetModuleName 的值  加上  第一個系結匯入表的RVA即pDir->VirtualAddress 
        // 上面的加法結果作為 新的 RVA, 新的 RVA 轉換為絕對地址,作為字串地址
        printf("DllName:%s\n", (DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pBoundImport->OffsetModuleName + pDir->VirtualAddress));
        printf("NumberOfModuleForwarderRefse:%d\n", pBoundImport-> NumberOfModuleForwarderRefs);
        printf("***********************************\n");

        for (int i = 0; i < pBoundImport->NumberOfModuleForwarderRefs; i++) //列印每一個系結過匯入表的所有依賴
        {
            pBoundImport++;  //能走到這里的NumberOfModuleForwarderRefs值必不為0,由于系結匯入表和REF大小是一樣的,所以這里相當于一起++
            PIMAGE_BOUND_FORWARDER_REF pREF = (PIMAGE_BOUND_FORWARDER_REF)pBoundImport;    
            printf("       TimeDateStamp:%x\n", pREF->TimeDateStamp);
            printf("       DllName:%s\n", (DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pREF->OffsetModuleName + pDir->VirtualAddress)); //和系結匯入表的字串列印一樣
            printf("       ****************************\n");  
        } 
        pBoundImport++; //外回圈最后別忘了查找下一個系結匯入表
    }
}
匯入表注入

當dll被加載進4GB空間時,會呼叫一次DllMain(入口方法,當程式執行完了要把dll從4GB空間被卸載,也會呼叫一次DllMain

注入的本質就是想方設法把自己的dll扔到別人行程的4GB空間里

img

只有在PE檔案內隱式呼叫(靜態使用)時才會在呼叫者PE檔案的匯入表內出現該 dll 的名字和相關函式,若為顯式呼叫(動態使用),則這個被呼叫的dll名和相關函式不會在呼叫者匯入表內出現,

反過來也就是說,寫入匯入表中的 dll ,必定是被呼叫者隱式呼叫(靜態使用)的,而事實是也確實如此,我們把毫不相關的 dll 寫入被呼叫者PE檔案完整的匯入表中,那打開這個PE檔案時,這個 dll 會當做隱式呼叫(靜態使用)直接被加載!(這是作業系統的規則,也是作業系統干的事)這個程序就叫匯入表注入,下面是一些細節:

我們可以把 dll 通過匯入表注入弄進其他 exe 的4GB空間,只要將一個 dll 相關的資訊以一張新匯入表寫入.exe中,并且具有至少一個函式的 INT 表和 IAT 表,和對應的 IMPORT_BY_NAME 表,也就是整個匯入表結構必須完整!作業系統就會在.exe啟動時加載這個 dll,而當加載 dll,就會先執行一次 DllMain 中的代碼,于是實作我們的注入目的,

若INT表或IAT表為空、或 IMPORT_BY_NAME 表為空、通過INT、IAT無法找對對應的函式名稱或序號,作業系統都不會把 dll 加載進 4GB 空間,總之讓整個匯入表結構正確完整即可,而且只用寫入一個函式就夠了,

匯入表注入流程:

目的無非就是多加一張匯入表,匯入表通常在其他的節,而原匯入表后面大概率沒有空位給我們新增一張表和對應的附表,所以這里直接選擇在新增節中把整個舊匯入表移動過去,并且在其后增加新匯入表和對應附表,其實也可以在各節空白區中找合適的位置添加,

所以首先是移動匯入表到新增節,其次就是增加新的匯入表在緊接著移動后的匯入表之后

也就是增加下面這個東西,在新增節的一眾匯入表之后:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {     
union {        
    DWORD   Characteristics;                
    DWORD   OriginalFirstThunk;              
};          
DWORD   TimeDateStamp;                    
DWORD   ForwarderChain;                   
DWORD   Name;        
DWORD   FirstThunk;       
} IMAGE_IMPORT_DESCRIPTOR;       
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;  

新增這個東西后,有一堆內容需要填充,比如 OriginalFirstThunk 需要填充指向這個dll 的 INT表的 RVA,Name 填充指向 dll 名的 RVA,FirstThunk 填充指向該dll IAT表的RVA, INT 和 IAT 表,只用填寫一個內容,保證至少有一個匯入函式,并且保證第二個內容為全0,即要讓 INT和 IAT不為空(8位元組),作業系統才會加載我們匯入表中的 dll,也就是下圖所示

img

IMAGE_IMPORT_BY_NAME 從2位元組之后直接寫入需要匯入dll 的匯出函式名的字串(Hint的2位元組為空)dll 名直接填 dll 名字串

INT表 內容 就是指向 IMAGE_IMPORT_BY_NAME 的 RVA,這里直接就填充新增節的 VirtualAddress 屬性

下一個 IAT表同理,一樣的內容,

第一步:

//根據目錄項(第二個就是匯入表)得到匯入表資訊:     
typedef struct _IMAGE_DATA_DIRECTORY {         
    DWORD   VirtualAddress;     //指向匯入表結構      
    DWORD   Size;                 //匯入表的總大小         
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;    

第二步:

判斷哪一個節的空白區 > Size(原匯入表的大小) + 20(匯入表大小) + A + B + C + D,如果不夠可能需要擴大最后一個節或新增一個節

第三步:

將原匯入表全部Copy到空白區       

第四步:

在新的匯入表后面,追加一個匯入表.            

第五步:

追加8個位元組的INT表  8個位元組的IAT表        
INT 和 IAT 表,只用填寫一個內容,保證至少有一個匯入函式,并且保證第二個內容為全0,即要讓 INT和 IAT不為空,作業系統才會加載我們匯入表中的 dll

第六步:

追加一個IMAGE_IMPORT_BY_NAME 結構,前2個位元組是0 后面是函式名稱字串         

第七步:

將IMAGE_IMPORT_BY_NAME結構的RVA賦值給INT和IAT表中的第一項        

第八步:

分配空間存盤DLL名稱字串 并將該字串的RVA賦值給Name屬性       

第九步:

修正IMAGE_DATA_DIRECTORY結構的VirtualAddress和Size

代碼如下:

#include "Currency.h"
#include "windows.h"
#include "stdio.h"

VOID h331()        //移動匯入表到新增節
{
    char FilePath[] = "CrackHead.exe";    //CRACKME.EXE   CrackHead.exe   Dll1.dll   R.DLL   notepad.exe  LoadDll.dll  PETool.exe 列印dll用最后一個看
    char CopyFilePath[] = "CrackHeadcopy.exe";    //CRACKMEcopy.EXE       CrackHeadcopy.exe
    LPVOID pFileBuffer = NULL;                    //會被函式改變的 函式輸出之一
    LPVOID* ppFileBuffer = &pFileBuffer;        //傳進函式的形參
    LPVOID pNewFileBuffer = NULL;                //用于新增節后的新 FileBuffer
    LPVOID* ppNewFileBuffer = &pNewFileBuffer;    //傳進函式的形參

    PIMAGE_DOS_HEADER pDos = 0;            //頭相關資訊
    PIMAGE_NT_HEADERS32 pNts = 0;
    PIMAGE_DATA_DIRECTORY pDir = 0;
    PIMAGE_IMPORT_DESCRIPTOR pImportTable = 0;
    PIMAGE_SECTION_HEADER pSection = 0;
    IMAGE_IMPORT_DESCRIPTOR NewImportTable = { 0 };    //新增的匯入表

    int NumOfImport = 0;    //匯入表個數
    int SizeOfImport = 0;    //匯入表長度

    DWORD SizeOfNewFileBuffer = 0;            // NewFileBuffer 的大小 
    DWORD SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);// FileBuffer 的大小

    if (!SizeOfFileBuffer)
    {
        printf("檔案讀取失敗\n");
        return;
    }
    // 頭查找
    pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
    pNts = (PIMAGE_NT_HEADERS32)((DWORD)pFileBuffer + pDos->e_lfanew);
    pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];  //匯入表
    pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pDir->VirtualAddress));

    // 判斷匯入表最保險的是全部為0才結束,但其實不可能沒有dll名,這里就只用INT表和 dll名是否都為0 作為結束條件
    while(pImportTable->OriginalFirstThunk && pImportTable->Name)
    {
        NumOfImport++;
        pImportTable++;
    }
    SizeOfImport = NumOfImport * sizeof(IMAGE_IMPORT_DESCRIPTOR);
    // 新增節,內容為重新移動的匯入表(因為原匯入表后面大概率沒有空位給我們新增)+ 一張我們新增的匯入表和對應的附表
    // 三參為新節大小,用 匯入表個數 * 匯入表大小 + 256 作為 新節大小,因為我們只新增一張匯入表和對應附表,256足矣
    SizeOfNewFileBuffer = AddNewSection(pFileBuffer, SizeOfFileBuffer, SizeOfImport + 256, ppNewFileBuffer);

    free(pFileBuffer);        // copy 完,舊的pFileBuffer就沒用了

    // 因為加上新節了,整個空間都變了,得以pNewFileBuffer重新頭查找,重新找到匯入表
    pDos = (PIMAGE_DOS_HEADER)pNewFileBuffer;
    pNts = (PIMAGE_NT_HEADERS32)((DWORD)pNewFileBuffer + pDos->e_lfanew);
    pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];  //匯入表
    pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pDir->VirtualAddress));
    pSection = IMAGE_FIRST_SECTION(pNts);    //指向第一個節表
    for (int i = 1; i < pNts->FileHeader.NumberOfSections; i++)  //注意這里 i 從1 開始,i<NumberOfSections
    {
        pSection++;        //下一個節表
    } //出回圈后pSection指向最后一個節表,也即新的節表

    // 下面開始構造新節內容,內容按順序排列如下:
    // IMAGE_IMPORT_BY_NAME 結構,30位元組
    // dll 名稱,30位元組
    // OriginalFirstThunk 指向的INT表,8 位元組 (只用一個函式)
    // FirstThunk 指向的IAT表,8 位元組
    // 原匯入表本身,SizeOfImport 位元組
    // 新增的匯入表,20位元組(sizeof(IMAGE_IMPORT_DESCRIPTOR)==20位元組 )

    // 于是往新節表中逐一填充以上內容,只在有資料的地方memcpy,無資料的地方AddNewSection函式已經幫填充0了,所以不需要管
    // 首先填充 IMAGE_IMPORT_BY_NAME ,其中Hint首2位元組為0不管,∴從+2的位置開始填充字串,字串為海東dll的匯出函式名ExportFunction
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 2), "ExportFunction", sizeof("ExportFunction"));

    // 填充 dll名,目的地址從+30 開始,字串為海東dll名,"InjectDll.dll"
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30), "InjectDll.dll", sizeof("InjectDll.dll"));

    // 填充 INT表,從+30+30 開始,內容為IMAGE_IMPORT_BY_NAME 的 RVA,顯然這個RVA值就是新節的記憶體起始位置,長度4
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30), &pSection->VirtualAddress, 4);

    // IAT表,思想和上面一致,也是一樣的 RVA 值
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8), &pSection->VirtualAddress, 4);

    // 移動原匯入表到新節    目的地址:新節表的絕對地址    源地址:匯入表絕對地址    長度:全部匯入表長度
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8), pImportTable, SizeOfImport);
    // 移動完后  別忘了把資料目錄中匯入表的 VirtualAddr 給改了, 改成新節的RVA
    pDir->VirtualAddress = pSection->VirtualAddress + 30 + 30 + 8 + 8;
    pDir->Size += sizeof(IMAGE_IMPORT_DESCRIPTOR);

    // 新增匯入表,由于NewImportTable已經初始化全0,因此時間戳和ForwarderChain沒必要賦0值了
    //指向INT表的RVA,即新節的記憶體起始位置 + 30 + 30
    NewImportTable.OriginalFirstThunk = pSection->VirtualAddress + 30 + 30;  
    // 指向IAT表的RVA
    NewImportTable.FirstThunk = pSection->VirtualAddress + 30 + 30 + 8;
    // Name 指向
    NewImportTable.Name = pSection->VirtualAddress + 30;
    // 最后別忘了拷貝整個 NewImportTable ,別忘了二參要取地址
    memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8 + SizeOfImport), &NewImportTable, sizeof(IMAGE_IMPORT_DESCRIPTOR));

    MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
    free(pNewFileBuffer);
}

注意這里有個坑,就是新增節的屬性,即節表記錄的Characteristics, 必須包含 C000 0040,如果沒有這個屬性,那運行exe 后會報錯0xc0000005,(遇到這個坑后我直接把新增節函式的節屬性改為e000 0060了,既可以執行代碼,也可以放匯入表及其附表)同理如果找節空白區的做法,也要保證添加的匯入表和附表所在節包含這個屬性,可以在所在節表直接或上 C000 0040,

7. ELF檔案

ELF頭,16個位元組

image-20220403133026182

前四個位元組確定檔案型別,決定了該檔案是否可以被加載
第七個位元組01表示ELF版本號,最后的九個位元組沒有定義,以0填充

image-20220403134252540

ELF的其他節

image-20220403134346144

強符號:函式和已初始化的全域變數

弱符號:未初始化的全域變數

注:連接器不允許多個同名的強符號一起出現 編譯時可以加-fno-common選項,遇到多重定義的全域符號時會觸發錯誤

8. 靜態資料鏈接庫與動態資料鏈接庫

靜態資料鏈接庫:起到鏈接程式和函式(或子程序)的作用,*.lib,相當于Linux中的.a檔案,

靜態編譯將匯出宣告和實作都放在lib中,編譯后所有代碼都嵌入到宿主程式

//加入預編譯指令
#pragma comment (lib,"*.lib")
//加入函式宣告
extern "C" __declspec(dllexport) int add(int a, int b);  //按照c的語法去匯出,如果不加為了防止c++的函式多載的影響,編譯器會更改函式名字為特別復雜的,
//直接在當前檔案呼叫函式即可 

靜態編譯的優點是撰寫出來的程式不需要呼叫DLL和載入函式,直接可以當成程式的一部分來使用,

靜態編譯的缺點也是顯而易見的,使用靜態編譯的程式體積會比動態編譯大,原因是函式的實作被嵌入為程式代碼的一部分,

動態資料鏈接庫:動態LIB檔案相當于一個C語言中的h檔案,是函式匯出部分的宣告,而不將實作程序嵌入到程式本身中,編譯后只是將函式地址存在宿主程式中,運行到呼叫函式是呼叫DLL并載入函式來實作函式的具體操作,.dll檔案,相當于linux的*.so檔案

dll其實就是exe,只不過它沒有main函式,所以不能單獨執行而已,事實上, 在實際的使用程序中我們也發現,很多應用程式都并不是一個完整的單獨可執行檔案,它們被分割成一些單獨的相對對立的元件,只有在執行應用程式的時候,用到的dll才會被呼叫,

一個dll工程生成一個dll檔案的時候,總是伴隨著生成一個lib檔案,這個lib檔案其實是一個動態的lib,它的大小比靜態lib要小很多,因為這個lib檔案其實只是包含了一些函式索引資訊,記錄了dll中那些函式的入口和位置,dll中才是具體的函式實作,

(1)lib是編譯時需要的,dll是運行時需要的,如果要完成源代碼的編譯,有lib就夠了,如果要使動態連接的程式運行起來,有dll就夠了,

在動態庫的情況下,有兩個檔案,一個是引入庫(.LIB)檔案,一個是DLL檔案,引入庫檔案包含被DLL匯出的函式的名稱和位置,DLL包含實際的函式和資料,應用程式使用LIB檔案鏈接到所需要使用的DLL檔案,庫中的函式和資料并不復制到可執行檔案中,因此在應用程式的可執行檔案中,存放的不是被呼叫的函式代碼,而是DLL中所要呼叫的函式[記憶體地址,這樣當一個或多個應用程式運行時再把程式代碼和被呼叫的函式代碼鏈接起來,從而節省了記憶體資源,

區別:靜態鏈接庫相當于代碼嵌入到宿主程式的exe程式里,如果需要優化之前的代碼,宿主程式需要重新編譯,

元件并不復制到宿主程式中,存放DLL要呼叫的函式記憶體地址,優化之前的代碼只需要修改DLL即可,不需重新編譯

靜態lib和動態dll使用注意事項

  1. 呼叫靜態lib庫,需要用到的檔案是:

    (1).h檔案,包含函式的宣告,資料結構等東西,在呼叫lib的時候,需要把該頭檔案包含進你的代碼;

    (2).lib檔案,包含具體的實作,

  2. 呼叫動態dll庫,需要用到的檔案是:

    (1).h檔案,如上,同樣需要包含到你的代碼;

    (2).lib檔案,包含一些函式的入口和具體位置,必須在編譯階段引入這個檔案,否則會報錯,【根據查到的資料,如果沒有這個動態lib檔案或者不想用lib檔案,需要用Win32的API函式LoadLibrary和GetProcAddress來裝載】

    (3).dll檔案,實際的實作,在程式運行時動態呼叫,

專案分成獨立的幾個模塊,每個模塊都有一個dll檔案,然后有一個最終的程式入口exe檔案,最后把dll檔案和exe檔案發行給用戶,當用戶每次點擊這個exe檔案的時候,自然會動態呼叫用到的dll檔案,注意這個程序就不再需要什么.h和.lib了,那是別人呼叫你的庫,再進行加工寫代碼時才需要做的事,上面說過dll其實就是個不能單獨打開執行的exe而已,所以你最終發行給用戶的只能是dll和exe,當然你完全可以把所有的東西只打包在一個exe中,但是當你的軟體非常大的時候,這樣進行更新維護就非常不方便,如果有問題就得重新發行一次exe,但是如果把各個模塊單獨弄成dll,你只需要打個補丁,對那些有問題的dll進行更換就行了,

另外一個是把你的dll寫好給別人拿來呼叫,以免別人做重復的作業,這個時候你剛開始提供的時候,就需要把.h檔案,.lib檔案,.dll檔案都提供給對方,然后如果你代碼里面有改動,只需要重新編譯一次dll給對方,替換掉原來的dll就可以了,非常方便,

dll的隱式鏈接和顯示鏈接

撰寫一個dll,選擇win32,然后win32控制臺程式,選擇DLL,然后在檔案名.cpp中寫下代碼:

// testDLL.cpp : 定義 DLL 應用程式的匯出函式,
#include "stdafx.h"//這里保存的是DLL的匯出函式
extern "C" __declspec(dllexport) int add(int a, int b) //dllexport告訴編譯器此函式為匯出函式
{
    return (a + b);
}

extern "C" __declspec(dllexport) int sub(int a, int b) //匯出函式
{
    return (a - b);
}
extern "C" __declspec(dllexport) int msg()
{
    MessageBox(NULL,TEXT("第一個dll程式"),TEXT("MY"),MB_OK);
    return 0;
}
//通過上面的代碼編譯在Debug中,可以得到一個testDLL.lib檔案和一個testDLL.dll檔案,然后在寫程式來呼叫這個dll,

隱式鏈接:讓編譯器去找import的函式,將生成的lib檔案放入loadDLL工程檔案夾下面,并設定編譯器的附加依賴項中增加此lib(也可以直接在代碼中增加使用陳述句如#pragma comment(lib,"testDLL.lib"))

//步驟1、將*.dll,*.lib放到工程目錄下
#include <iostream>
#include <windows.h>
#pragma comment(lib,"testDLL.lib")  //步驟2、將該宣告添加到呼叫檔案
//步驟3、加入函式的宣告
extern "C"_declspec(dllimport) int add(int a, int b);//dllimport告訴編譯器此函式為匯入函式
extern "C"_declspec(dllimport) int sub(int a, int b);
extern "C"_declspec(dllimport) int msg();
int main()
{
    int a = 9;
    int b = 3;
    int nAdd = add(a, b);
    int nSub = sub(a, b);
    std::cout << nAdd << ":" << nSub << std::endl;
    msg();
    system("pause");
    return 0;
}

顯示鏈接:只需要生成的一個dll檔案就能使用了,但是使用較為復雜需要用到指標函式及LoadLibrary等相關知識,在使用dll中的函式時我們首先需要使用匯入函式將dll檔案匯入到行程地址空間再使用GetProcAddress得到dll中的函式地址再使用,

#include <iostream>
#include <windows.h> 
int main()
{
    //步驟1、定義函式指標         步驟2、宣告函式指標變數 _pAdd myadd 
    typedef int (*_pAdd)(int a, int b);
    typedef int (*_pSub)(int a, int b);
    typedef int (*_pmsg)();
    //步驟2、動態加載dll到記憶體中
    HINSTANCE hDll = LoadLibrary(TEXT("testDLL.dll"));
    int nParam1 = 9;
    int nParam2 = 3;
    //步驟4、獲取函式地址
    _pAdd pAdd = (_pAdd)GetProcAddress(hDll, "add");  //不加_stdcall函式名不會改變,原來是什么這里就寫什么
    _pSub pSub = (_pSub)GetProcAddress(hDll, "sub");
     _pmsg pmsg = (_pmsg)GetProcAddress(hDll, "msg");
    int nAdd = pAdd(nParam1, nParam2);
    int nSub = pSub(nParam1, nParam2);
    pmsg();
    std::cout << nAdd << ":" << nSub << std::endl;
    FreeLibrary(hDll);
    system("pause");
    return 0;
}

假設你想呼叫DLL中的一個函式ExportedFn,你可以像這樣很簡單地匯出它:

extern "C" _declspec(dllexport) void ExportedFn(int Param1, char* param2);

必須使用extern "C"鏈接標記,否則C++編譯器會產生一個修飾過的函式名,這樣匯出函式的名字將不再是ExportedFn

假設這個函式從DLL1.dll匯出,那么客戶端可以像這樣呼叫這個函式:

HMODULE hMod = LoadLibrary("Dll1.dll");  //在win32下HMODULE與HINSTANCE無區別
typedef void (*PExportedFn)(int, char*);
PExportedFn pfnEF = (PExportedFn)GetProcAdress("ExportedFn");
pfnEF(1, "SomeString");

如果不想讓別人看到自己寫的函式名字,可以寫一個*.def檔案,里面對函式名進行符號匯出

*.def檔案,使用序號對函式名進行匯出,可以達到隱藏的目的 #名字是一段程式最精華的注釋
EXPORTS
add  @12
sub  @15

9. c++虛函式表

普通函式與虛函式

image-20220329093726267

總結:
1.通過物件呼叫時,virtual函式與普通函式都是E8 Call
2.通過指標呼叫時,virtual函式是FF Call, 也就是間接Call

虛函式大小

class Base
{
public:    
    int x;
    int y;
    virtual void func1()
    {
        printf("func1\n");
    }
    virtual void func2()
    {
        printf("func2\n");
    }    
    virtual void func3()
    {
        printf("func3\n");
    }  
};

int main()
{
    Base base;
    printf("%x\n",sizeof(base));        //輸出結果為C  12位元組 4 + 4 + 4   兩個虛函式占同一塊4位元組地址
                                        //如果上面不是virtual函式,普通的成員函式并不會使結構體大小發生變化
    Base* bs = &base;
    printf("base 的首地址為(即this):%x\n",&base); //注意base的首地址并不是虛表的地址
    printf("base 的虛函式表地址為:%x\n",*(int*)&base); //取前四個位元組
    typedef void(*pFunction)(void);  //宣告新變數  函式指標  因為Function函式回傳值為void型別
    pFunction pFn;
    for (int i = 0; i < 3; i++)
    {
        int temp = *((int*)(*(int*)&base) + i); //把虛函式首地址強轉為int* 型別   分別取虛表的第1、2、3個虛函式
        pFn = (pFunction)temp;                    //型別強轉為函式指標  pFunction
        pFn();
    }
}

image-20220329095932454

總結:

1.當類中有虛函式時,會多一個屬性,4個位元組
2.多出的屬性是一個地址,指向一張表(陣列)的首地址,里面存盤了所有虛函式的地址,

結構體首地址的第一個4位元組就是虛表地址

系結 --函式的呼叫與函式的地址結合的程序

靜態系結:前期系結,又叫早系結,在函式編譯時已經確定地址 E8 call

動態系結:又叫晚系結,在函式運行時才確定地址 ff call 就是多型 --父類指標可以訪問子類方法

10. win32

編碼

ASCII:用八位二進制的低七位,一共規定了128個字符的編碼
擴展ASCII:第八位為1,規定了以1開頭的128個字符
Unicode:通常兩個位元組表示一個字符,而ASCII是一個位元組表示一個字符,    char c = L'中' 顯式告訴編譯器使用Unicode表
UTF-8:是一種變長的編碼方式,它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度,
編碼規則:
1)對于單位元組的符號,位元組的第一位設為0,后面7位為這個符號的 Unicode 碼,因此對于英語字母,UTF-8 編碼和 ASCII 碼是相同的,

2)對于n位元組的符號(n>1),第一個位元組的前n位都設為1,第n + 1位設為0,后面位元組的前兩位一律設為10,剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼,

C語言寬位元組

char x[] = "中國";          // d6 d0 69 fa 00      使用ASCII編碼                strlen(x) = 4  不包含00
wchar_t    x1[] = L"中國";    // 2d 4e fd 56 00 00  使用Unicode編碼     寬位元組   strlen(x1) = 2
printf("%s\n",x);        //使用控制臺默認的編碼 列印出 中國
printf(L"%s\n",x1);        //列印出亂碼  #include<locate.h>  宣告 setlocate(LC_ALL,""); 即可告訴編譯器使用控制臺格式 
TCHAR x2[] = TEXT("中國")        //宏定義,針對不同的char和tchar做了統一,在不同編譯環境中體現不同特性
編碼 大小 支持語言
ASCII 1個位元組 英文
Unicode 2個位元組(生僻字4個) 所有語言
UTF-8 1-6個位元組,英文字母1個位元組,漢字3個位元組,生僻字4-6個位元組 所有語言

WIN32

主要存放在C:\windows\system32 非常重要的幾個DLL

Kernel32.dll:最核心的功能模塊,比如管理記憶體,行程和執行緒相關的函式等      但并不是內核模塊
User32.dll:是Windows用戶界面相關應用程式介面,如創建視窗和發送資訊等
GDI32.dll:Graphical Device Interface(圖形設備介面),包含用于畫圖和顯示文本的函式,顯示視窗

使用WIN32 API 需要添加頭檔案#include<windows.h>

WIN32入口函式

WinMain  四個引數
(HINSTANCE hInstance,  //imagebase可以理解為程式實體的handle,當程式裝入記憶體中時,作業系統需要利用這個值來標識exe
HINSTANCE hPrevInstance,                     //永遠為空
LPSTR lpCmdLine,                             //包含了命令列引數,
int mCmdShow)                                //一個標志,表明視窗是否要最大化,最小化,或者是正常顯示,
win32程式出錯后不會報錯  可以在程式下加一個 GetLastError  回傳一個DWORD編碼,取MSDN查

Windows為了描述應用程式什么時候產生的,什么位置產生的,提供了一個結構體:MSG,記錄事件的詳細資訊

typedef struct tagMSG{
    HWND hwnd;                 // 訊息所指視窗句柄,比如滑鼠點擊的訊息由那個視窗處理
    UINT message;             //訊息名稱,形式如WM_SIZE
    WPARAM wParam;             //32位訊息的特定附加資訊,具體表示什么取決于message
    LPARAM lParam;             //32位訊息的特定附加資訊,具體表示什么取決于message,對訊息的進一步說明
    DWORD time;             //訊息創建時間
    POINT pt;                 //訊息創建時的滑鼠位置  二維坐標
} MSG;

系統訊息佇列與應用程式訊息佇列

應用從佇列中取出訊息,判斷訊息的型別是不是我關心的,是就處理,不是就讓Windows去處理

事件(動作)-->封存在訊息中MSG-->系統訊息佇列-->應用訊息佇列-->回圈取出訊息-->處理訊息

???????????????????????????????????

Windows視窗

typedef struct tagWNDCLASSEXW {
    UINT        cbSize;           //WNDCLASSEXA 結構體的大小(sizeof(WNDCLASSEX))
    /* Win 3.x */
    UINT        style;            //視窗類的樣式
 ★ WNDPROC     lpfnWndProc;      //視窗處理函式的指標    
    int         cbClsExtra;       //為視窗類的額外資訊做記錄,初始化為0,
    int         cbWndExtra;       //記錄視窗實體的額外資訊,系統初始為0
    HINSTANCE   hInstance;        //本模塊的實體句柄,當前視窗屬于的應用程式
    HICON       hIcon;            //視窗類的圖示,為圖示資源句柄
    HCURSOR     hCursor;          //視窗類的滑鼠樣式,為滑鼠樣式資源的句柄
    HBRUSH      hbrBackground;    //視窗類的背景刷,為背景刷句柄
    LPCWSTR     lpszMenuName;     //指向選單的指標
    LPCWSTR     lpszClassName;    //指向類名稱的指標
    /* Win 4.0 */
    HICON       hIconSm;          //小圖示的句柄,在任務欄顯示的圖示
} WNDCLASSEXW, *PWNDCLASSEXW, NEAR *NPWNDCLASSEXW, FAR *LPWNDCLASSEXW;

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/510963.html

標籤:其他

上一篇:優化 JS 程式的一個小方法

下一篇:疫情背景下,如何選擇管理軟體?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more