lyl610abc 发表于 2021-4-6 20:28

PE文件笔记十三 合并节

继续PE系列笔记的更新

PE其它笔记索引可前往:
(https://www.52pojie.cn/thread-1391994-1-1.html)

------

前面在(https://www.52pojie.cn/thread-1410348-1-1.html)学习了关于节的操作之新增节,接下来继续学习节的操作之合并节

# 合并节

## 为什么要合并节

在前面新增节中,要判断最后一个节表后面是否有空间用于新增节表,只有当最后一个节表后40个字节全为0时,才能进行新增节的操作;但当条件不满足时又想要新增节,该如何操作?

答案便是:合并节,合并节就是用一个节表描述多个节,这样省下来的节表空间就可以用于新增节了

于是合并节的目的便是:节省节表空间,这样就能实现新增节

------

## 合并节涉及的结构体成员

| 涉及的节表成员   | 含义                     |
| :--------------- | :----------------------- |
| Name             | 节名称                   |
| VirtualAddress   | 节在内存中的偏移 (RVA) |
| Misc             | 节的实际大小             |
| SizeOfRawData    | 节在文件中对齐后的尺寸   |
| PointerToRawData | 节区在文件中的偏移       |
| Characteristics| 节的属性               |

| 涉及的标准PE头成员 | 含义   |
| :----------------- | :------- |
| NumberOfSections   | 节的个数 |

## 合并节的流程

1. 修正内存对齐
2. 修改第一个节的大小
3. 修改第一个节的权限
4. 修改节数量为1
5. 清空后面的节(可选)

------

## 按流程合并节

### 修正内存对齐

关于修正内存对齐的内容在上一篇笔记 (https://www.52pojie.cn/thread-1410996-1-1.html)中已经详细说明了

这次就以上一篇修正过内存对齐的结果:EverEdit_修正.exe直接进行合并节,在这里省略修正内存对齐的步骤;不了解如何修正内存对齐的可以回顾上一篇笔记

给出修正完内存对齐后 节的信息

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406135858359.png)

------

| Name   | Misc   | SizeOfRawData | PointerToRawData | Characteristics |
| :----- | :------- | :------------ | :--------------- | --------------- |
| .text| 0x19a000 | 0x19a000      | 0x400            | 0x60000020      |
| .rdata | 0x38000| 0x38000       | 0x19a400         | 0x40000040      |
| .data| 0x4b000| 0x4b000       | 0x1d2400         | 0xc0000040      |
| .rsrc| 0x63000| 0x63000       | 0x21d400         | 0x40000040      |
| .reloc | 0x17000| 0x17000       | 0x280400         | 0x42000040      |

### 修改第一个节的大小

修改第一个节的大小为后面 为所有节内存对齐后的大小的和

即修改第一个节的大小为 0x19a000+0x38000+0x4b000+0x63000+0x17000=0x297000

仍然使用PE工具:DIE进行修改

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406191646661.png)

------

修改后

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406191729130.png)

------

### 修改第一个节的权限

既然要用一个节表概括所有的节,那么该节表就必须具备先前所有节表的权限

也就是第一个节的权限 = 所有节的权限 **相或**

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406192516941.png)

------

得到新的权限为0xE2000060

使用PE工具:DIE修改

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406192958994.png)

------

修改后

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193139515.png)

------

### 修改节数量为1

找到标准PE头中的NumberOfSections成员,将其修改为1

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193233571.png)

------

修改后

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193332082.png)

------

### 清空后面的节(可选)

其实做完上面一步就已经完成了合并节,但合并节是为了腾出节表空间,于是这里再将后面无用的节表清空掉

使用WinHex找到节表处

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193717767.png)

------

选中要清空的部分,编辑→填充选块(快捷键 Ctrl+L)

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193921600.png)

------

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406193818020.png)

------

清空后保存即可

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406194003256.png)

------

### 测试运行

程序仍然可以正常运行

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406194149811.png)

------

并且此时再用PE工具:DIE查看节的信息,也只有一个节了

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406194230742.png)

------

# 代码实现合并节

## 完整代码

```c
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD640x8664

//向文件中指定位置追加数据
//第一个参数为文件路径
//第二个参数为要追加的数据指针
//第三个参数为要追加的数据大小
//第四个参数为位置偏移
//第五个参数为hMap的指针
//第六个参数为pFile的指针
BOOL appendFile(LPCSTR filePath, PVOID writeData, DWORD sizeOfWriteData, DWORD offset, HANDLE* phMap, PVOID* ppFile) {
        HANDLE hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
        char newPath;
        strcpy(newPath, filePath);

        strcat(newPath, ".exe");
        HANDLE hFile2 = CreateFileA(newPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);


        //WriteFile用于接收实际写入的大小的参数
        DWORD dwWritenSize = 0;

        //根据文件句柄创建映射
        HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
        //映射内容
        LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);

        BYTE* content = (BYTE*)pFile;
        content += offset;

        //写入要插入数据前的数据
        DWORD size = SetFilePointer(hFile, NULL, NULL, FILE_END);
        BOOL bRet;
        bRet = WriteFile(hFile2, pFile, offset, &dwWritenSize, NULL);
        if (!bRet)return false;
        //写入要插入的数据
        SetFilePointer(hFile, NULL, NULL, FILE_END);
        bRet = WriteFile(hFile2, writeData, sizeOfWriteData, &dwWritenSize, NULL);
        if (!bRet)return false;
        //写入要插入数据后的数据
        SetFilePointer(hFile, NULL, NULL, FILE_END);
        bRet = WriteFile(hFile2, content, size - offset, &dwWritenSize, NULL);
        if (!bRet)return false;
        CloseHandle(hFile);
        CloseHandle(hMap);
        CloseHandle(*phMap);
        UnmapViewOfFile(pFile);
        UnmapViewOfFile(*ppFile);
        bRet = DeleteFileA(filePath);
        if (!bRet)return false;

        hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
        //根据文件句柄创建映射
        hMap = CreateFileMappingA(hFile2, NULL, PAGE_READWRITE, 0, 0, 0);
        //映射内容
        pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
        SetFilePointer(hFile, NULL, NULL, FILE_BEGIN);
        bRet = WriteFile(hFile, pFile, sizeOfWriteData + size, &dwWritenSize, NULL);
        if (!bRet)return false;
        CloseHandle(hFile);
        CloseHandle(hFile2);
        CloseHandle(hMap);
        UnmapViewOfFile(pFile);
        bRet = DeleteFileA(newPath);
        if (!bRet)return false;
        hFile = CreateFileA(filePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, 0, 0);
        //根据文件句柄创建映射
        hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
        //映射内容
        *ppFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
        *phMap = hMap;
        CloseHandle(hFile);
        return true;
}

//根据pFile获取PE文件结构
void GetPeStruct32(LPVOID pFile, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
        dos = (_IMAGE_DOS_HEADER*)pFile;

        //创建指向PE文件头标志的指针
        DWORD* peId;
        //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
        peId = (DWORD*)((UINT)dos + dos->e_lfanew);



        //创建指向可选PE头的第一个成员magic的指针
        WORD* magic;
        //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
        magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));


        //根据magic判断为32位程序还是64位程序


                //让PE文件头指针指向其对应的地址
        nt = (_IMAGE_NT_HEADERS*)peId;
        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while (cnt < nt->FileHeader.NumberOfSections) {
                //创建指向块表的指针
                _IMAGE_SECTION_HEADER* section;
                //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
                section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
                //将得到的块表指针存入数组
                sectionArr = section;

        }
}

//修正节表的Misc和SizeOfRawData
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
//第四个参数为文件路径
//第五个参数为文件映射
//第六个参数为文件映射内容指针
//第七个参数为要修正的节表在数组中的下标
void sectionAlignment(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr, LPCSTR filePath, HANDLE* phMap,LPVOID* ppFile, int n) {
       
        //获得最后一个节的实际大小
        DWORD VirtualSize = sectionArr->Misc.VirtualSize;
        //获得最后一个节的文件对齐后的大小
        DWORD SizeOfRawData = sectionArr->SizeOfRawData;
        //计算上一个节内存对齐后的大小
        UINT SizeInMemory = (UINT)ceil((double)max(VirtualSize, SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;
        printf("%X\n", SizeInMemory);
        //计算差值= 内存对齐后大小 - 文件对齐后大小
        UINT offset = SizeInMemory - sectionArr->SizeOfRawData;
        printf("%X\n", offset);
        //根据节在文件中的偏移 + 文件对齐后的大小 得到节的末尾
        UINT end = sectionArr->PointerToRawData + sectionArr->SizeOfRawData;
        printf("end:%X\n", end);
       

        //申请要填充的空间
        INT* content = (INT*)malloc(offset);
        //初始化为0
        ZeroMemory(content, offset);
        //WriteFile用于接收实际写入的大小的参数
        DWORD dwWritenSize = 0;

        BOOL bRet=appendFile(filePath, (PVOID)content, offset, end,phMap,ppFile);
        GetPeStruct32(*ppFile, dos, nt, sectionArr);
        if (bRet) {
                //开始修正Misc和SizeOfRawData
                sectionArr->Misc.VirtualSize = SizeInMemory;
                sectionArr->SizeOfRawData = SizeInMemory;
                //修正后面受到影响的节的PointerOfRawData和VirtualAddress
                int i;
                while (n + 1 <= nt->FileHeader.NumberOfSections - 1) {
                        n++;
                        sectionArr->PointerToRawData += offset;
                }
        }

       
       

}


//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
        //所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
        DWORD allSectionSize = 0;
        //所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
        DWORD allSectionCharacteristics = sectionArr->Characteristics;
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
                allSectionSize += sectionArr->SizeOfRawData;
                allSectionCharacteristics = allSectionCharacteristics | sectionArr->Characteristics;
        }
        printf("allSectionSize:%X\n", allSectionSize);
        printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

        sectionArr->Misc.VirtualSize = allSectionSize;
        sectionArr->SizeOfRawData = allSectionSize;
        sectionArr->Characteristics = allSectionCharacteristics;
       
       
        //清零后面的节
        for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
                ZeroMemory(sectionArr, sizeof(_IMAGE_SECTION_HEADER));
        }

        //节个数设置为1
        nt->FileHeader.NumberOfSections = 1;
}


int main(int argc, char* argv[])
{
        //创建DOS对应的结构体指针
        _IMAGE_DOS_HEADER* dos;
        //读取文件,返回文件句柄
        HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);
        //根据文件句柄创建映射
        HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
        //映射内容
        LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);
        //类型转换,用结构体的方式来读取
        dos = (_IMAGE_DOS_HEADER*)pFile;
        //输出dos->e_magic,以十六进制输出
        printf("dos->e_magic:%X\n", dos->e_magic);


        //创建指向PE文件头标志的指针
        DWORD* peId;
        //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
        peId = (DWORD*)((UINT)dos + dos->e_lfanew);
        //输出PE文件头标志,其值应为4550,否则不是PE文件
        printf("peId:%X\n", *peId);


        //创建指向可选PE头的第一个成员magic的指针
        WORD* magic;
        //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
        magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
        //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
        printf("magic:%X\n", *magic);
        //根据magic判断为32位程序还是64位程序
        switch (*magic) {
        case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
        {
                printf("32位程序\n");
                //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
                //创建指向PE文件头的指针
                _IMAGE_NT_HEADERS* nt;
                //让PE文件头指针指向其对应的地址
                nt = (_IMAGE_NT_HEADERS*)peId;
                printf("Machine:%X\n", nt->FileHeader.Machine);
                printf("Magic:%X\n", nt->OptionalHeader.Magic);
                //创建一个指针数组,该指针数组用来存储所有的节表指针
                //这里相当于_IMAGE_SECTION_HEADER* sectionArr,声明了一个动态数组
                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

                //创建指向块表的指针
                _IMAGE_SECTION_HEADER* sectionHeader;
                //让块表的指针指向其对应的地址
                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
                //计数,用来计算块表地址
                int cnt = 0;
                //比较 计数 和 块表的个数,即遍历所有块表
                while (cnt < nt->FileHeader.NumberOfSections) {
                        //创建指向块表的指针
                        _IMAGE_SECTION_HEADER* section;
                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
                        //将得到的块表指针存入数组
                        sectionArr = section;
                        //输出块表名称
                        printf("%s\n", section->Name);
                }
                CloseHandle(hFile);
               
       
                int i;
                //sectionAlignment(dos, nt, sectionArr, "C:\\Users\\sixonezero\\Desktop\\EverEdit\\EverEdit.exe",hMap, pFile,2);
               
                for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
                        sectionAlignment(dos, nt, sectionArr, "C:\\Users\\lyl610abc\\Desktop\\EverEdit\\EverEdit_修正.exe", &hMap, &pFile,i);
                }
               
                combineSection(dos, nt, sectionArr);

                break;
        }

        case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
        {
                printf("64位程序\n");
                //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
                //创建指向PE文件头的指针
                _IMAGE_NT_HEADERS64* nt;
                nt = (_IMAGE_NT_HEADERS64*)peId;
                printf("Machine:%X\n", nt->FileHeader.Machine);
                printf("Magic:%X\n", nt->OptionalHeader.Magic);

                //创建一个指针数组,该指针数组用来存储所有的节表指针
                //这里相当于_IMAGE_SECTION_HEADER* sectionArr,声明了一个动态数组
                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

                //创建指向块表的指针
                _IMAGE_SECTION_HEADER* sectionHeader;
                //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
                //计数,用来计算块表地址
                int cnt = 0;
                //比较 计数 和 块表的个数,即遍历所有块表
                while (cnt < nt->FileHeader.NumberOfSections) {
                        //创建指向块表的指针
                        _IMAGE_SECTION_HEADER* section;
                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
                        //将得到的块表指针存入数组
                        sectionArr = section;
                        //输出块表名称
                        printf("%s\n", section->Name);
                }

                break;
        }

        default:
        {
                printf("error!\n");
                break;
        }

        }
        return 0;
}
```

------

## 合并节代码

```c
//合并节
//第一个参数为指向dos头的指针
//第二个参数为指向nt头的指针
//第三个参数为存储指向节指针的数组
void combineSection(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
        //所有节内存对齐后的大小的和,这里要求已经修正过内存对齐,只有这样文件对齐大小才会等于内存对齐大小
        DWORD allSectionSize = 0;
        //所有节的权限,初始为第一个节的权限,和后面的每个节的权限进行或操作
        DWORD allSectionCharacteristics = sectionArr->Characteristics;
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections;i++) {
                allSectionSize += sectionArr->SizeOfRawData;
                allSectionCharacteristics = allSectionCharacteristics | sectionArr->Characteristics;
        }
        printf("allSectionSize:%X\n", allSectionSize);
        printf("allSectionCharacteristics:%X\n", allSectionCharacteristics);

        sectionArr->Misc.VirtualSize = allSectionSize;
        sectionArr->SizeOfRawData = allSectionSize;
        sectionArr->Characteristics = allSectionCharacteristics;
       
       
        //清零后面的节
        for (i = 1; i < nt->FileHeader.NumberOfSections; i++) {
                ZeroMemory(sectionArr, sizeof(_IMAGE_SECTION_HEADER));
        }

        //节个数设置为1
        nt->FileHeader.NumberOfSections = 1;
}
```

------

## 运行结果

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406200009298.png)

------

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406194149811.png)

------

# 说明

- 合并节就是用一个节表来包括多个节表的信息
- 可以看到合并节除了修正内存对齐,其余部分都十分简单
- 合并节之后多出了的节表空间可以用来新增节

------

# 附件

附上本笔记中分析的EverEdit文件:[点我下载](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/EverEdit合并节.zip?versionId=CAEQHhiBgMCwzMycxRciIDhmMmI2ZTY4MTExMDRiMWE5MjZjYTA2YjFjZWE0ZTUz)

此次附件中添加了合并完节后的exe

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210406202251712.png)

------

aswcy815174418 发表于 2021-4-16 23:28

本帖最后由 aswcy815174418 于 2021-4-16 23:32 编辑

研究了楼主代码一两天,查了许多资料,修改了一些代码,看起来更简化一些,可能写法不是很规范
https://wwe.lanzouj.com/iuh6yo72x7i

lyl610abc 发表于 2021-4-6 21:09

sam喵喵 发表于 2021-4-6 20:58
大佬,请问你在哪里学到的这么多新技能

自学
首先要知道自己想要实现什么
然后要知道实现功能涉及的内容
接着要利用各种搜索引擎,包括但不限于百度、谷歌等检索自己所需要的
然后就是边查边写
拿这次的用C语言 appendFile,在指定位置插入数据为例
想要实现的功能:在指定位置插入数据为例
实现功能涉及内容:C语言 文件读写插入数据
搜索关键词:C语言插入数据到指定位置
查完发现没有现成的代码,但有看到有用的信息:文件是连续存储的,无法直接插入
于是就想到要复制 然后插入数据 最后再复制剩下的
最后实现功能等等
很多人在不会的情况下,不知道如何检索有用的信息,这才是不会的根本
我个人觉得,学习不只是要学知识本身,更是要学习处理问题的方法
虽然我也只是个小菜鸡,但是莫名的大道理倒是能讲不少,你就随便看看就好{:301_978:}

sam喵喵 发表于 2021-4-6 20:58

大佬,请问你在哪里学到的这么多新技能

sam喵喵 发表于 2021-4-6 21:00

没见到书上提合并节这一神技

sam喵喵 发表于 2021-4-6 21:09

sam喵喵 发表于 2021-4-6 20:58
大佬,请问你在哪里学到的这么多新技能

找到了,滴水

sam喵喵 发表于 2021-4-6 21:14

lyl610abc 发表于 2021-4-6 21:09
自学
首先要知道自己想要实现什么
然后要知道实现功能涉及的内容


听君一席话,胜读十年书!

莫莫 发表于 2021-4-6 22:12

非常感谢

wangshouyin 发表于 2021-4-7 06:28

您好。希望以后代码段您可以用编辑功能里的代码段标记起来,不然看的有点乱。谢谢分享

Spa495 发表于 2021-4-7 08:39

学习了,大神

lifz888 发表于 2021-4-7 10:07

非常好 的实习资料,支持分享
页: [1] 2 3
查看完整版本: PE文件笔记十三 合并节