lib文件在windows下有兩種形式出現,第一種就是普通的靜態庫,第二種是作為dll的導入庫。
接下來我來分享一下如何在這兩種lib文件中注入后門代碼,使程序編譯后生成的exe在運行時候自動。
執行我們后門,并且不影響正常lib的功能。
1. 注入原理
首先大致說一下lib的文件格式,用壓縮文件可以打開lib文件,里面可以看到許多的obj文件(幾個cpp就有幾個obj)。

lib實際上就是一堆Obj文件打包在了一起,當然還有一些額外的信息,這個之后再說。

只要我們把我們的后門程序backdoor.lib 和 正常的lib合并到一起,這樣我們的后門代碼就有機會被執行。
但是這樣做還有幾個問題:
① 包含后門代碼的Obj文件在生成exe的過程中會被鏈接嗎?
如果obj中有符號被引用的話,這個obj才會被鏈接。否則不會。
② 如果obj被鏈接進去 ,怎么做才能使程序啟動時自動執行我們的代碼?
在我們的后門代碼中寫一個全局對象,利用全局對象的構造函數,使它在main等函數之前自動執行。
2.實現
現在我們要解決的問題是,想辦法使程序鏈接我們的后門代碼的obj。
首先來了解一下obj文件格式。

這里我們只分析這種的coff格式,在研究的過程中還發現了另一種 Microsoft CLI ByteCode 的obj文件,我們這里不討論。(vs編譯時候選擇鏈接時生成代碼,最后生成的lib文件中包含的obj就是這種)
COFF格式總覽:

這個相比于PE格式還是簡單了很多。我們可以使用CoffViewer這個工具來查看obj文件:

我們可以在原本的lib文件中的每一個obj文件中加入一段代碼,在這段代碼中引用后門代碼中的函數。這樣程序在編譯的時候,在處理正常的obj文件時發現引用了一個外部符號,最終在包含后門代碼的obj文件中找到了,這樣這個obj就會被鏈接進程序。
雖然obj文件格式不算復雜,但是相比于上面這種方法還有一種更簡單的方法,就是直接在SymbolTable 里面加入一個外部符號即可,(這個外部符號的定義需要在我們的后門代碼中)。
筆者在實驗的過程中發現,只要添加一個外部符號,不論有沒有使用到這個符號,都無法通過編譯。
所以我們現在的目標就是在符號表里面添加一項。
相關的數據結構:
typedef struct _FILEHEADER{ unsigned short machine; // 平臺名 unsigned short numberOfSections;// 區段數 unsigned long timeDateStamp; // 時間戳 unsigned long pointerToSymbolTable; // 符號表文件偏移 unsigned long numberOfSymbols; // 符號總個數 unsigned short sizeOfOptionalHeader; // 可選頭長度 unsigned short characteristics; // 文件標記 } FILEHEADER, *PFILEHEADER; /*最大為8個字節的,以’\0’為結尾的ASCII字符串.用于記錄區段的名字.區段的名字有些是特定意義的區段. 如果區段名的數量大于8個字節,則name的第一字節是一個斜杠字符:’/’,接著就是一個數字,這個數字就是字符串表的一個索引.它將索引到一個具體的區段名.*/ typedef struct _SECTIONHEADER{ char name[8]; // 段名 unsigned long virtualSize; // 虛擬大小 沒有用 unsigned long virtualAddress; // 虛擬地址 沒有用 unsigned long sizeOfRawData; // 區段數據的字節數 unsigned long pointerToRawData; // 區段數據偏移 unsigned long pointerToRelocations; // 區段重定位表偏移 unsigned long pointerToLinenumbers; // 行號表偏移 unsigned short numberOfRelocations; // 重定位表個數 unsigned short numberOfLinenumbers; // 行號表個數 unsigned long characteristics; // 段標識}SECTIONHEADER, *PSECTIONHEADER; #pragma pack(push, 2)typedef struct _SYMBOL{ union { char name[8]; // 符號名稱 struct { unsigned long zero; // 字符串表標識 unsigned long offset; // 字符串偏移 }; }; unsigned long value; // 符號值 short section; // 符號所在段 unsigned short type; // 符號類型 unsigned char Class; // 符號存儲類型 unsigned char numberOfAuxSymbols;// 符號附加記錄數} SYMBOL, *PSYMBOL;#pragma pack(pop) typedef struct _STRIGTABLE{ unsigned int Size; char Data[1];} STRIGTABLE, *PSTRIGTABLE;
我們可以通過FileHeader找到符號表的位置,然后在符號表的后面 與 字符串表之前再插入一項。
注意:
① FileHeader里面的NumberOfSymbols 包含了numberOfAuxSymbols。
② 字符串表 = FileHeader→numberOfSymbols * sizeof(SYMBOL) + FileHeader→pointerToSymbolTable。
③ 一個auxSymbol和SYMBOL大小相同,都是18個字節。
先從原本的lib中提取出obj文件,然后在修改了obj之后,重新打包為lib (利用lib程序)。這時候編譯就無法通過了,會提示鏈接時找不到符號。這時候把包含后門代碼的obj也打包到這個lib里面,就能通過編譯了,而且包含后門的obj文件也會被鏈接進去。
3. DLL導入庫注入后門代碼
前文提到lib有兩種,一種是普通的lib文件,一種是dll的導入庫。網上都是這么說的。
然而在研究的過程中,筆者發現這兩種lib根本沒有啥區別,只不過dll導入庫里面的obj文件不包含代碼段。而且dll的導入庫里面還包含了一些其他的數據(也是COFF文件,但不是本文提到的Intel 80386 COFF object file)。
既然里面也包含obj文件,那么上文的方法能否也同樣適用于這種lib文件,答案是可以的。
但是我們無法將該lib文件里面的數據用lib解壓出來重新打包,因為lib程序只能打包obj文件。
有一種辦法就是,手動解析lib文件,遍歷其成員,如果是intel 80386 COFF Object File,我們就在它的符號表里面加一項。
lib文件格式分析:
lib文件格式還是很簡單的,下面筆者帶大家分析一下lib的格式。
lib格式總覽
(1)8個字節的magic "!”
(2)后面跟著許多的members
每一個member的開頭是一個header:
struct ar_hdr { char ar_name[16]; char ar_date[12]; char ar_uid[6]; char ar_gid[6]; char ar_mode[8]; char ar_size[10]; char ar_fmag[2];};
都是字符串形式,但是要注意的是這些字符串未使用的部分不是字節’\x00’,而是空格符。
其中的ar_size指出和后面跟著的數大小,如果這個數是個奇數,后面的數據對齊到偶數,以’\x0a’ 去補齊。
之后跟著的就是文件數據。
Member分析:
每一個成員開頭都有一個這樣的header:
struct ar_hdr { char ar_name[16]; char ar_date[12]; char ar_uid[6]; char ar_gid[6]; char ar_mode[8]; char ar_size[10]; char ar_fmag[2];};
ar_name以字符’/’ 開頭,接著跟著是文件的名稱。
如果ar_name 為’//’ 的話,這意味著這是一個目錄信息,它后面的數據信息中包含了該目錄下所有的文件的名稱 。(它的數據區是一個字符串表)

假設數據區包含了N條路徑信息。那么之后的N個Members是具體的文件,并且每一個Member的name都是’/’ + 文件名稱在字符串表中的偏移。

大部分的Member都是用來描述lib內包含的obj文件,但是lib的前兩個Member是比較特殊的。經過對比多個lib文件后,筆者終于猜測出前兩個Member數據區的格式:
第一個Member:
(1)ar_name 為 ‘/’
(2)數據區的格式:
DWORD NumberOfSymbols; //該lib文件中的符號個數DWORD ObjOffset; //每個符號所在的obj 在lib文件中的偏移 //(注意是obj文件在lib中的偏移,Member的偏移)//注意上面這些數據都是按大端方式存儲
字符串表,包含NumberOfSymbols個字符串。
第二個Member:
(1)ar_name為 ‘/’
(2)數據區的格式:
DWORD NumberOfObjs; //lib文件中包含的coffDWORD OffsetOfObjs[NumberOfObjs]; //每一個coff文件在lib文件中的偏移DWORD NumberOfSymbols; //符號個數WORD index[NumberOfSymbols]; //每一個符號位于第幾個obj里面,從1開始
字符串表,包含NumberOfSymbols個字符串。
好了,在知道了lib文件格式之后,我們可以隨便修改里面的包含的obj文件了。在修改了lib之后,再利用lib程序將正常的lib和包含后門代碼的靜態庫鏈接到一起就可以了。
這里就不詳細說明具體做法了。
總結
前文dll導入庫的注入方法中是通過手動修改lib文件,這種方法也是適用于一般的lib靜態庫的。
在手動修改lib文件之后,然后利用lib程序將正常的lib文件 與我們的后門程序打包到一起。這樣就成功的將后門注入到lib中了。
筆者在實驗過程中還發現了一些問題:
(1)c++代碼有二進制兼容性問題,不同版本的編譯器生成的lib文件無法使用。如果我們的后門程序是C++實現的話,如果用戶在編譯時候使用了被注入后門的lib文件, 這會導致用戶在編譯時候無法正常通過。所以我們的后門程序最好是用c語言去實現。而且盡量不要調用c語言運行庫內的函數,最好使用windows 原生的API。
(2)如果是使用c語言的話,現在又出現了一個問題,如何在mian之前執行我們的后門代碼?在gcc里面是有arrtibute屬性的,但是msvc里面恐怕不行。這里筆者提供兩種方法 (來源于網絡):
C語言
#pragma data_seg(".CRT$XIU") void * _ctor_ = func;#pragma data_seg()
C++
#pragma data_seg(".CRT$XCU") void * _ctor_ = func;#pragma data_seg()
注意如果是C語言的話,func格式返回0代表成功,非0代表失敗,這回導致程序退出。
效果展示:
dll文件:
#include extern "C" __declspec(dllexport)void add(int a,int b){ printf("Hello World");}
編譯之后生成兩個文件: TestDll.dll和TestDll.lib。
用壓縮軟件看一下 TestDll.lib。

實際上有四個文件,里面的txt是前文提到的前兩個Member。
我們使用CoffViewer查看一下第一個TestDll.dll文件。

(可以思考一下為什么符號表是這些東西)
接著修改這個TestDll.lib,可以看到,寫進去三個外部符號。

看一下TestDll2.lib:

看起來是沒啥區別的,提取出第一個TestDll.dll來看一下:

這時候已經多了一個符號了。
vs編譯一下看看效果:

現在當然不會通過編譯,因為這個符號是定義在我們的后門代碼里面的。
利用lib程序把后門和正常的lib文件打包到一起:

重新編譯運行,可以看到后門程序上線:

聚銘網絡
黑白之道
看雪學苑
D1Net
安全內參
聚銘網絡
GoUpSec
一顆小胡椒
看雪學苑
看雪學苑
看雪學苑