前言
一個風(fēng)和日麗的下午,鳥兒在枝頭高唱,花兒在草中盛放,而我,在電腦前暴怒。本來滿懷欣喜的脫了個簡單的upx殼,沒想到卻讓我暴跳如雷。
“這是什么啊,怎么用了官方的脫殼命令還運(yùn)行不了?!睂ψ约喊l(fā)自靈魂的拷問,再一次從旁印證,菜,真的是菜的摳腳。

于是乎我瘋狂點(diǎn)擊x32dbg,那一瞬間我以為我瘋了,自己反編譯調(diào)試自己的代碼,這總感覺有點(diǎn)令人抓狂。
在一兩下的F9運(yùn)行之后,我找到了問題所在:

異常斷在了紅框的語句,咋一看似乎沒有什么問題,但多次與脫殼前的程序?qū)Ρ龋覀兙桶l(fā)現(xiàn)了一個問題:被賦值的地址是變動的
“啊哈!”,我掩蓋不住心中的狂喜。既然已經(jīng)找到了問題的所在,只要把問題解決了那不就行了。
于是乎我用StudyPE載入了脫殼后的文件,輕輕點(diǎn)了一下鼠標(biāo),隨著清脆的點(diǎn)擊聲,問題迎刃而解。

一問到底
滿臉欣喜的我,笑容很快從臉上褪去,剩下的只有難以言表的寂寞和空虛,總感覺缺了點(diǎn)什么,我不禁的問自己,剛剛做了什么。
問題解決了嗎?是的,從結(jié)果上來看我們解決了問題,但從過程來看,哦不,我們完全沒有解決。
某些時候,借助一些工具亦或者投機(jī)取巧解決問題,總感覺不踏實(shí),不知道各位如何,反正我有這種感覺。
尚在學(xué)習(xí)階段的我,于是乎決定一探究竟,找到根源之所在,連根拔起。這次,我又一次點(diǎn)開了x32dbg,與前幾分鐘不同的是,我多了幾分平靜,多了幾分祥和。
或許有足夠耐心的你,看著文筆尚不算太好,甚至意義不明的“技術(shù)文章”,至此仍然沒看到問題的說明,感到了不耐煩。客官先別著急,且聽我細(xì)細(xì)道來——這次問題的觸發(fā)歸根到底是原程序的重定位表沒有被寫到脫殼后程序的重定位表中來才使得涉及到內(nèi)存地址的變量發(fā)生了錯誤。
讓我們言歸正傳,再一次定位到異常觸發(fā)的地方——那個賦值語句。既然脫殼后程序的內(nèi)存地址沒有改變,就側(cè)面證明了加了殼的程序中,這個地址是由一段代碼更改而不是根據(jù)加殼程序的重定位表更改的。也就是說upx殼他模仿了Window系統(tǒng)根據(jù)重定位表改寫地址的方法,通過自己儲存原程序重定位表,再用自己代碼改寫。
盡管用了一段不算太長也不算太短的文字來描述了一下整個推理過程,但機(jī)智的我僅用了一瞬間就已經(jīng)想到這些東西(自夸)。同時我又迅速的記住了發(fā)生錯的代碼的rva(其實(shí)是復(fù)制),然后打開脫殼前的程序,定位到了發(fā)生錯誤的地方,并對這個地址打下了硬件寫斷點(diǎn):

顯然,此時的代碼還沒被upx解壓,但我們沒有關(guān)系,直接來到對應(yīng)的內(nèi)存地址并打下了硬件寫斷點(diǎn),在幾次F9后,我們來到了一個改寫這個地方的代碼:

excellent!是循環(huán)!它出現(xiàn)了!或許在大家眼里,它只是一個微不足道的循環(huán)體,但我卻看到了勝利的曙光。這么大的重定位表,并且要對它操作,有循環(huán)基本是八九不離十的。當(dāng)執(zhí)行到紅框語句,對應(yīng)地址就被更改了。隨后在一些微小的分析下,我梳理出了如下過程:
- edi指向一個記有偏移量的表,每次循環(huán)根據(jù)情況取1個或2個字節(jié),直到遇到0字節(jié)。

- ebx對上一步結(jié)果進(jìn)行累加,ebx最初值為第一個區(qū)段的內(nèi)存虛擬地址減去4

3.ebx累加的結(jié)果其實(shí)就是需要變化的地址的地址,也就是重定位表結(jié)構(gòu)中的VA和offset低位,以及基址的相加(不熟悉的學(xué)友萌可以去看看重定位表的結(jié)構(gòu),我也看了好多遍)。
typedef struct _MyReloadtion {
DWORD virtualAddress;
DWORD sizeOfBlock;
vector<WORD> typeOffset; //高4位表類型,一般都是3000 低12位表示偏移量
//所以這里ebx實(shí)際上就是 ebx = virtualAddress + typeOffset低12位
}MyReloadtion, *pMyReloadtion;
所以我們只要把ebx的結(jié)果分離出來,符合重定位表的結(jié)構(gòu),最后再寫進(jìn)我們脫殼程序的重定位表中,便可大功告成。
頭皮發(fā)麻
既然我們都知道怎么做了,剩下所面臨的就是解決方法的問題?;蛟S我們可以選擇hook,把循環(huán)里每一個ebx值記錄下來,經(jīng)過變化就可以獲取到重定位表。
而作為老實(shí)人的我,由于并不知道怎么hook這種沒有在Call里面的代碼,所以選擇了第二種令人頭皮發(fā)麻的方法——模擬PE加載到內(nèi)存中,獲取第一區(qū)塊的虛擬地址,再把位移表提取出來,通過模擬上述的步驟,來獲取重定位表。
這對于尚在襁褓之中的我來說無疑是具有十分的挑戰(zhàn)性的,可我都分析到這個地步了,豈能惹急你慫。此時我心中暗暗下了個毒誓,寫不出來王x蛋。
就這樣,我開始了漫長的C++之旅。
勝利之光
又一個陽光明媚的下午,葉兒在空中飛舞,蝶兒在花中跳舞,而我的頭發(fā)在空中飄落。
不得不說,打代碼是令人煩躁的,要用一成不變的言語來表示我千變?nèi)f化高超的思維,這簡直就是侮辱我的靈魂(開個玩笑)。為什么腦袋一團(tuán)混沌,為什么情緒一向暴躁,很大的原因可能是我們解決問題的時候沒有分步看待并解決。
以繁化簡,讓復(fù)雜的問題分成幾個簡單的問題。而我在這里就分成以下幾個步驟:
- 提取位移表
- 模擬系統(tǒng)將PE文件載入內(nèi)存。
- 模擬匯編中的循環(huán),算出重定位表
- 將新的重定位表加到脫殼的程序里
當(dāng)我理清了四個方向并逐個問題加以百度輔助解決后,心態(tài)就有了明顯的恢復(fù),而臉上也露出了久違的猥瑣的笑容。
廢話說完了,接下來看看三個步驟的實(shí)施過程:
-
提取位移表:
提取位移表很簡單,在x32dbg中,用文章之前的方法,定位到循環(huán)塊,找到edi指向的地址,右鍵提取,保存成bin文件即可。
8.png 模擬系統(tǒng)將PE文件載入內(nèi)存并獲取第一個節(jié)區(qū)的內(nèi)存地址:
整體思路:
2.1 通過讀取文件來獲取NT頭中FileHeader的區(qū)塊數(shù)量屬性以及OptionHeader的PE頭大小屬性,OptionHeader大小屬性,內(nèi)存中節(jié)區(qū)對齊量屬性。
2.2 將映像大小根據(jù)對齊量對齊后,載入整個PE頭。
2.3 通過FileHeader大小,Signature大小,OptionHeader大小相加,最后得出區(qū)塊表的開始地址。
2.4 遍歷區(qū)塊表,加載對應(yīng)的區(qū)塊并記錄第一個節(jié)區(qū)內(nèi)存地址。
DWORD getReloadBase() {
//1.獲取FileHeader和OptionHeader的一些關(guān)鍵變量
HANDLE hfile = CreateFileA(targetName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
LPDWORD receiveSize = NULL;
if (hfile == INVALID_HANDLE_VALUE || GetLastError() == ERROR_FILE_NOT_FOUND)
{
std::cout << "找不到目標(biāo)文件" << GetLastError() << std::endl;
return NULL;
}
// 讀取dos頭
IMAGE_DOS_HEADER dosHeader;
DWORD dosHeaderSize = sizeof(dosHeader);
BOOL dosReadResult = ReadFile(hfile, &dosHeader, dosHeaderSize, receiveSize, NULL);
if (!dosReadResult)
{
std::cout << "讀取dos頭錯誤" << GetLastError() << std::endl;
return NULL;
}
// 讀取NT頭
IMAGE_NT_HEADERS32 ntHeader;
DWORD pointerResult = SetFilePointer(hfile, dosHeader.e_lfanew, NULL, FILE_BEGIN);
if (pointerResult == INVALID_SET_FILE_POINTER)
{
std::cout << "設(shè)置讀取指針為NT頭時錯誤" << GetLastError() << std::endl;
return NULL;
}
BOOL ntHeaderResult = ReadFile(hfile, &ntHeader, sizeof(ntHeader), receiveSize, NULL);
if (!dosReadResult)
{
std::cout << "讀取NT頭錯誤" << GetLastError() << std::endl;
return NULL;
}
WORD peHeaderSize = ntHeader.OptionalHeader.SizeOfHeaders;
WORD setctionNums = ntHeader.FileHeader.NumberOfSections;
DWORD imageSize = ntHeader.OptionalHeader.SizeOfImage;
DWORD sectionAlign = ntHeader.OptionalHeader.SectionAlignment;
//2. 對齊鏡像并載入PE頭
int mImageSize = alignSize(imageSize, sectionAlign);
mImageBase = new char[mImageSize];
memset(mImageBase, 0, mImageSize);
SetFilePointer(hfile, 0, NULL, FILE_BEGIN);
ReadFile(hfile, mImageBase, peHeaderSize, receiveSize, NULL); //將文件頭寫入
//3. 計(jì)算并獲取區(qū)塊表起始地址
PIMAGE_NT_HEADERS mpNtheader = (PIMAGE_NT_HEADERS)((DWORD)mImageBase + dosHeader.e_lfanew);
int mNtHeadersSize = sizeof(ntHeader.FileHeader) + sizeof(ntHeader.Signature) + ntHeader.FileHeader.SizeOfOptionalHeader;
PIMAGE_SECTION_HEADER mpSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)mpNtheader + mNtHeadersSize);
DWORD keyBaseAddress = NULL;
//4. 遍歷區(qū)塊表加載區(qū)塊
for (int index = 0; index < setctionNums; ++index)
{
DWORD va = mpSectionHeader->VirtualAddress;
if (index == 0)
{
keyBaseAddress = va;
}
DWORD rawSize = mpSectionHeader->SizeOfRawData;
DWORD vaSize = mpSectionHeader->Misc.VirtualSize;
DWORD rawOffset = mpSectionHeader->PointerToRawData;
if (rawSize == 0)
{
continue;
}
else
{
SetFilePointer(hfile, rawOffset, NULL, FILE_BEGIN);
ReadFile(hfile, &mImageBase[va], rawSize, receiveSize, NULL);
}
mpSectionHeader++;
}
keyBaseAddress -= 4;
CloseHandle(hfile);
return keyBaseAddress;
}
- 模擬匯編中的算法得出重定位表
整體思路:
3.1 從位移表拿取一個字節(jié),若大于EF取后兩個字節(jié)。
3.2 將拿取到的字節(jié)與第一個節(jié)區(qū)內(nèi)存地址-4相加。
3.3 通過一些位運(yùn)算拿到重定位表中的變量。
void buildReloadtionTable(DWORD baseAddress, char* pRelateTable) {
//1. 位移+基值,變化后拿到offset
vector<MyReloadtion> reloadtionTable;
MyReloadtion reloadtion;
int index = 0;
int size = 0;
// 記錄上一個計(jì)算出來的重定位表項(xiàng)的虛擬內(nèi)存地址
DWORD lastVirtualAddress = NULL;
//為第一個區(qū)塊內(nèi)存地址-4
DWORD pReload = baseAddress;
while (true)
{
//1.拿取位移表一個字節(jié)
WORD relate = (BYTE)*pRelateTable;
pRelateTable++;
if (relate == 0)
{
reloadtion.sizeOfBlock = calSizeofBlock(size);
reloadtionTable.push_back(reloadtion);
break;
}
//2. 判斷拿到的字節(jié)是否大于EF
if (relate > 0xEF)
{
relate = *pRelateTable;
BYTE hightRelate = *(pRelateTable + 1);
BYTE lowRelate = relate;
relate = ((hightRelate << 8) | lowRelate);
pRelateTable += 2;
}
pReload += relate;
//3. 位運(yùn)算獲取offset
DWORD virtualAddress = pReload & 0xF000;
WORD typeOffset = pReload & 0xFFF | 0x3000;
if (lastVirtualAddress == NULL)
{
reloadtion = MyReloadtion();
reloadtion.virtualAddress = virtualAddress;
}
else if(lastVirtualAddress != virtualAddress)
{
reloadtion.sizeOfBlock = calSizeofBlock(size);
reloadtionTable.push_back(reloadtion);
size = 0;
reloadtion = MyReloadtion();
reloadtion.virtualAddress = virtualAddress;
}
reloadtion.typeOffset.push_back(typeOffset);
size++;
lastVirtualAddress = virtualAddress;
}
//保存到文件
saveToFile(reloadtionTable);
}
- 將新的重定位表加到脫殼的程序里:
整體思路:
4.1 為程序添加一個新的區(qū)塊(使用工具如PEStudy)
4.2 將重定位表復(fù)制過去(HEX WORKSHOP)
4.3 修改PE文件的重定位表指向(LordPE)



遺憾收場
那一天顯得特別寧靜,我如往常一樣坐在那里?!敖K于做出來了...”我輕輕的嘆了口氣,神態(tài)略顯輕松,滑動著鼠標(biāo)的滾輪??粗矍翱焖倩瑒拥拇a行,心中百感交集。想想當(dāng)初零行的代碼到現(xiàn)在上百行,對于剛?cè)腴TWIN32以及C++來說確實(shí)是不容易。
再用了幾次寫的代碼修復(fù)程序后,心中卻開始感到了不滿。“為什么我每次都得自己找位移表,為什么我每次都得用工具新建區(qū)塊,為什么每次都要我修改重定位表的rva,為什么我的代碼不能一次實(shí)現(xiàn)呢?”
帶著這個遺憾,我寫下了這篇文章,不為別的,只為和大家分享。
那段匯編的循環(huán)代碼
31 C0 8A 07 47 09 C0 74 22 3C EF 77 11 01 C3 8B 03 86 C4 C1 C0 10 86 C4 01 F0 89 03 EB E2 24 0F C1 E0 10 66 8B 07 83 C7 02 EB E2
大家如果想探索文章提到的upx還原重定位表的部分,可以直接在調(diào)試器中用上面數(shù)據(jù)搜索直接定位到相關(guān)代碼。
關(guān)于代碼使用
將exe放到加了upx殼的程序的目錄下,將提取出來的位移表命名為rva.bin,加殼程序命名為target.exe,成功運(yùn)行后,會生成一個叫result.bin的文件,這個就是重定位表。
本程序并沒有做多版本的window測試,也沒有做upx多版本測試,目前只在自家的win10下測試并通過,使用的upx版本為3.96w。

結(jié)語
這篇文章廢話較多,作為技術(shù)文來說或許是不及格的。其實(shí)一直都想用一種敘事的手法來寫,這可能是新的嘗試呢,所以希望各位學(xué)友不嫌煩,能較為有趣的看完。
關(guān)于剛剛提到的遺憾,筆者也會著手嘗試,讓一切自動起來。
