首页
论坛
课程
招聘
[我的代码传记]upx脱壳重定位表修复
2020-11-1 21:39 2431

[我的代码传记]upx脱壳重定位表修复

2020-11-1 21:39
2431

前言

一个风和日丽的下午,鸟儿在枝头高唱,花儿在草中盛放,而我,在电脑前暴怒。本来满怀欣喜的脱了个简单的upx壳,没想到却让我暴跳如雷。

 

“这是什么啊,怎么用了官方的脱壳命令还运行不了。”对自己发自灵魂的拷问,再一次从旁印证,菜,真的是菜的抠脚。

 

 

于是乎我疯狂点击x32dbg,那一瞬间我以为我疯了,自己反编译调试自己的代码,这总感觉有点令人抓狂。
在一两下的F9运行之后,我找到了问题所在:

 

 

异常断在了红框的语句,咋一看似乎没有什么问题,但多次与脱壳前的程序对比,我们就发现了一个问题:被赋值的地址是变动的

 

“啊哈!”,我掩盖不住心中的狂喜。既然已经找到了问题的所在,只要把问题解决了那不就行了。

 

于是乎我用StudyPE载入了脱壳后的文件,轻轻点了一下鼠标,随着清脆的点击声,问题迎刃而解。

 

一问到底

满脸欣喜的我,笑容很快从脸上褪去,剩下的只有难以言表的寂寞和空虚,总感觉缺了点什么,我不禁的问自己,刚刚做了什么。

 

问题解决了吗?是的,从结果上来看我们解决了问题,但从过程来看,哦不,我们完全没有解决。

 

某些时候,借助一些工具亦或者投机取巧解决问题,总感觉不踏实,不知道各位如何,反正我有这种感觉。

 

尚在学习阶段的我,于是乎决定一探究竟,找到根源之所在,连根拔起。这次,我又一次点开了x32dbg,与前几分钟不同的是,我多了几分平静,多了几分祥和。

 

或许有足够耐心的你,看着文笔尚不算太好,甚至意义不明的“技术文章”,至此仍然没看到问题的说明,感到了不耐烦。客官先别着急,且听我细细道来——这次问题的触发归根到底是原程序的重定位表没有被写到脱壳后程序的重定位表中来才使得涉及到内存地址的变量发生了错误。

 

让我们言归正传,再一次定位到异常触发的地方——那个赋值语句。既然脱壳后程序的内存地址没有改变,就侧面证明了加了壳的程序中,这个地址是由一段代码更改而不是根据加壳程序的重定位表更改的。也就是说upx壳他模仿了Window系统根据重定位表改写地址的方法,通过自己储存原程序重定位表,再用自己代码改写。

 

尽管用了一段不算太长也不算太短的文字来描述了一下整个推理过程,但机智的我仅用了一瞬间就已经想到这些东西(自夸)。同时我又迅速的记住了发生错的代码的rva(其实是复制),然后打开脱壳前的程序,定位到了发生错误的地方,并对这个地址打下了硬件写断点:

 

 

显然,此时的代码还没被upx解压,但我们没有关系,直接来到对应的内存地址并打下了硬件写断点,在几次F9后,我们来到了一个改写这个地方的代码:

 

 

excellent!是循环!它出现了!或许在大家眼里,它只是一个微不足道的循环体,但我却看到了胜利的曙光。这么大的重定位表,并且要对它操作,有循环基本是八九不离十的。当执行到红框语句,对应地址就被更改了。随后在一些微小的分析下,我梳理出了如下过程:

  1. edi指向一个记有偏移量的表,每次循环根据情况取1个或2个字节,直到遇到0字节。

  2. ebx对上一步结果进行累加,ebx最初值为第一个区段的内存虚拟地址减去4

3.ebx累加的结果其实就是需要变化的地址的地址,也就是重定位表结构中的VA和offset低位,以及基址的相加(不熟悉的学友萌可以去看看重定位表的结构,我也看了好多遍)。

1
2
3
4
5
6
typedef struct _MyReloadtion {
    DWORD virtualAddress;
    DWORD sizeOfBlock;
    vector<WORD> typeOffset;  //4位表类型,一般都是3000  12位表示偏移量
//所以这里ebx实际上就是 ebx = virtualAddress + typeOffset低12
}MyReloadtion, *pMyReloadtion;

所以我们只要把ebx的结果分离出来,符合重定位表的结构,最后再写进我们脱壳程序的重定位表中,便可大功告成。

头皮发麻

既然我们都知道怎么做了,剩下所面临的就是解决方法的问题。或许我们可以选择hook,把循环里每一个ebx值记录下来,经过变化就可以获取到重定位表。

 

而作为老实人的我,由于并不知道怎么hook这种没有在Call里面的代码,所以选择了第二种令人头皮发麻的方法——模拟PE加载到内存中,获取第一区块的虚拟地址,再把位移表提取出来,通过模拟上述的步骤,来获取重定位表。

 

这对于尚在襁褓之中的我来说无疑是具有十分的挑战性的,可我都分析到这个地步了,岂能惹急你怂。此时我心中暗暗下了个毒誓,写不出来王x蛋。

 

就这样,我开始了漫长的C++之旅。

胜利之光

又一个阳光明媚的下午,叶儿在空中飞舞,蝶儿在花中跳舞,而我的头发在空中飘落。

 

不得不说,打代码是令人烦躁的,要用一成不变的言语来表示我千变万化高超的思维,这简直就是侮辱我的灵魂(开个玩笑)。为什么脑袋一团混沌,为什么情绪一向暴躁,很大的原因可能是我们解决问题的时候没有分步看待并解决。

 

以繁化简,让复杂的问题分成几个简单的问题。而我在这里就分成以下几个步骤:

  1. 提取位移表
  2. 模拟系统将PE文件载入内存。
  3. 模拟汇编中的循环,算出重定位表
  4. 将新的重定位表加到脱壳的程序里

当我理清了四个方向并逐个问题加以百度辅助解决后,心态就有了明显的恢复,而脸上也露出了久违的猥琐的笑容。

 

废话说完了,接下来看看三个步骤的实施过程:

  1. 提取位移表:
    提取位移表很简单,在x32dbg中,用文章之前的方法,定位到循环块,找到edi指向的地址,右键提取,保存成bin文件即可。

  2. 模拟系统将PE文件载入内存并获取第一个节区的内存地址:
    整体思路:
    2.1 通过读取文件来获取NT头中FileHeader的区块数量属性以及OptionHeader的PE头大小属性,OptionHeader大小属性,内存中节区对齐量属性。
    2.2 将映像大小根据对齐量对齐后,载入整个PE头。
    2.3 通过FileHeader大小,Signature大小,OptionHeader大小相加,最后得出区块表的开始地址。
    2.4 遍历区块表,加载对应的区块并记录第一个节区内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
DWORD getReloadBase() {
    //1.获取FileHeader和OptionHeader的一些关键变量
    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 << "找不到目标文件" << 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 << "设置读取指针为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. 计算并获取区块表起始地址
    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. 遍历区块表加载区块
    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;
}
  1. 模拟汇编中的算法得出重定位表
    整体思路:
    3.1 从位移表拿取一个字节,若大于EF取后两个字节。
    3.2 将拿取到的字节与第一个节区内存地址-4相加。
    3.3 通过一些位运算拿到重定位表中的变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void buildReloadtionTable(DWORD baseAddress, char* pRelateTable) {
    //1. 位移+基值,变化后拿到offset
    vector<MyReloadtion> reloadtionTable;
    MyReloadtion reloadtion;
 
    int index = 0;
    int size = 0;
    // 记录上一个计算出来的重定位表项的虚拟内存地址
    DWORD lastVirtualAddress = NULL;
    //为第一个区块内存地址-4
    DWORD pReload = baseAddress;
    while (true)
    {
        //1.拿取位移表一个字节
        WORD relate = (BYTE)*pRelateTable;
        pRelateTable++;
        if (relate == 0)
        {
            reloadtion.sizeOfBlock = calSizeofBlock(size);
            reloadtionTable.push_back(reloadtion);
            break;
        }
        //2. 判断拿到的字节是否大于EF
        if (relate > 0xEF)
        {
            relate = *pRelateTable;
            BYTE hightRelate = *(pRelateTable + 1);
            BYTE lowRelate = relate;
            relate = ((hightRelate << 8) | lowRelate);
            pRelateTable += 2;
        }
        pReload += relate;
 
        //3. 位运算获取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);
}
  1. 将新的重定位表加到脱壳的程序里:
    整体思路:
    4.1 为程序添加一个新的区块(使用工具如PEStudy)
    4.2 将重定位表复制过去(HEX WORKSHOP)
    4.3 修改PE文件的重定位表指向(LordPE)

遗憾收场

那一天显得特别宁静,我如往常一样坐在那里。“终于做出来了...”我轻轻的叹了口气,神态略显轻松,滑动着鼠标的滚轮。看着眼前快速滑动的代码行,心中百感交集。想想当初零行的代码到现在上百行,对于刚入门WIN32以及C++来说确实是不容易。

 

再用了几次写的代码修复程序后,心中却开始感到了不满。“为什么我每次都得自己找位移表,为什么我每次都得用工具新建区块,为什么每次都要我修改重定位表的rva,为什么我的代码不能一次实现呢?”

 

带着这个遗憾,我写下了这篇文章,不为别的,只为和大家分享。

那段汇编的循环代码

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还原重定位表的部分,可以直接在调试器中用上面数据搜索直接定位到相关代码。

关于代码使用

将exe放到加了upx壳的程序的目录下,将提取出来的位移表命名为rva.bin,加壳程序命名为target.exe,成功运行后,会生成一个叫result.bin的文件,这个就是重定位表。
本程序并没有做多版本的window测试,也没有做upx多版本测试,目前只在自家的win10下测试并通过,使用的upx版本为3.96w。

 

结语

这篇文章废话较多,作为技术文来说或许是不及格的。其实一直都想用一种叙事的手法来写,这可能是新的尝试呢,所以希望各位学友不嫌烦,能较为有趣的看完。

 

关于刚刚提到的遗憾,笔者也会着手尝试,让一切自动起来。

 

#附件
https://share.weiyun.com/wTws5RD3


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

收藏
点赞1
打赏
分享
最新回复 (5)
雪    币: 9217
活跃值: 活跃值 (34253)
能力值: (RANK:105 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2020-11-2 10:10
2
0
楼主文笔风趣
雪    币: 298
活跃值: 活跃值 (1665)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Zard_ 活跃值 2020-11-2 10:40
3
0
雪    币: 5384
活跃值: 活跃值 (3203)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
Risks 活跃值 2 2020-11-2 15:43
4
0
是挺奇葩的,很少遇到exe带有重定位的,另外楼主文笔有意思~
雪    币: 3509
活跃值: 活跃值 (3058)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
psycongroo 活跃值 2 2020-11-2 17:38
5
0
Risks 是挺奇葩的,很少遇到exe带有重定位的,另外楼主文笔有意思~
哈哈谢谢,我还怕废话太多
雪    币: 12496
活跃值: 活跃值 (1943)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
白菜大哥 活跃值 2020-11-3 09:20
6
0
不错不错。真正的脱壳教程
游客
登录 | 注册 方可回帖
返回