汪道之

有人的地方就有江湖

0%

病毒_Win_DLL+导出表

如何让病毒调用系统的API函数

如何获取API的入口地址

  1. 找到提供这个API函数的DLL的加载基址
  2. 从DLL的导出表中拿到API函数地址

DLL导出表机制:

在这个表中根据函数名找到函数入口地址

一个简单方法:

原理:一个系统中,所有进程加载的同一个DLL的加载基址是相同的

image-20210519193440959

病毒代码的处理:

  1. 在病毒的数据区添加3个字段,分别存储MessageBox的地址(4字节)、函数参数“test”(5字节,留一个字节给结尾‘\0’字节)和“hello”(6字节)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
          ;MessageBox Entry 4 bytes,偏移为13字节(之前数据区13字节)
    nop;
    nop;
    nop;
    nop;
    ;hello串,要留一个字节给结尾0字节,共6字节,偏移为17字节
    nop;
    nop;
    nop;
    nop;
    nop;
    nop;
    ;test串,要留一个字节给结尾0字节,偏移为23字节
    nop
    nop
    nop
    nop
    nop
  2. 获取MessageBox的地址,将函数地址和函数参数填入数据区

    1
    2
    3
    4
    5
    6
    7
    //填写MessageBox的入口地址
    //该数据在数据区偏移13字节处
    *(void **)(virusData + 13) = MessageBox;
    //填写参数,hello字符串
    strcpy(virusData + 17, "hello");
    //填写参数,test字符串
    strcpy(virusData + 23, "test");
  3. 在真正寄生的病毒代码中调用MessageBox函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    call yy;
    数据区;
    yy: pop eax; //eax为数据区的实际起始内存地址
    ……
    //调用MessageBox
    push 0; //传递最后一个参数
    mov ebx, eax; //ebx指向数据区起始内存地址
    add ebx, 23; //ebx指向test串
    push ebx; //压栈,传参test
    sub ebx, 6; //hello串在test串前6个字节
    push ebx; //压栈,传参hello串
    push 0; //传递最后一个参数
    add eax, 13; //MessageBox地址在数据区偏移13个字节处
    mov eax, [eax]; //获取MessageBox的地址
    call eax; //调用MessageBox函数
    ……

如何判断是否是控制台程序

在PE文件可选映像头中,字段subsystem描述了该属性

Windows GUI和Windows CUI (G——Graphic,C——Console)

病毒真正获取API函数地址的方法

获取DLL基址

  1. FS寄存器在偏移0x30处保存了一个指针,指向PEB结构
  2. PEB结构的偏移0x0c处保存了另外一个指针,指向PEB_LDR_DATA结构
  3. PEB_LDR_DATA偏移0c处是加载模块链表的头指针,8个字节,头4个字节指向LDR_MODULE结构体(代表一个模块,每个模块都对应一个这样的结构体),后4个字节指向下一个结构体组成链表,该链表是循环链表
  4. LDR_MODULE偏移0x2c有个成员BaseDllName,8个字节,后4个字节为地址,指向纯模块名的unicode串

代码:

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
#include <stdio.h>
#include <windows.h>
void main(int argC, char ** args)
{
//s1为要查找的DLL名,不包含结束符,Unicode码,一个字符占2个字节,所以是push 18
//相关代码在win7下可用
OLECHAR * s1 = L"ntdll.dll";
_asm{
push 18 //传参,字符串长度
push s1 //传参,DLL名字
call finddll //调用函数finddll
jmp end
finddll: //finddll函数:找到DLL,返回eax为基址;否则为0
……
end:
}

void * p;
_asm mov p, eax
wprintf(L“the dll %s base is %x\n”, s1, p); //打印DLL基址
}

finddll: ;参数1为DLL的名字,参数2位DLL长度,返回EAX为DLL基址或0(未找到)
mov ebx, fs:[30h] ;定位PEB
mov ebx, [ebx+0ch] ;定位PEB_LDR
mov ebx, [ebx+0ch] ;定位第一个模块(MODULE)
push ebx ;保存第一个模块地址到栈上
loop_finddll:
push [esp + 12] ;把DLL长度传参
push [esp + 12] ;把DLL名字传参
push [ebx + 30h] ;传参module的BaseName
call strcompare ;比较BaseName和s1是否相等
test eax,eax ;返回eax,0为假,1为真
jnz found ;找到跳转,未找到继续遍历
mov ebx,[ebx];模块头4个字节指向下一个模块
mov eax,[ebx];指向当前模块的下下个模块
cmp [esp],eax;判断下下个模块是否是头模块
jz not_found ;遍历完说明没找到DLL
jmp loop_finddll ;没遍历完,继续查找下一个模块
found:
mov eax,[ebx+18h];找到了,在模块偏移18拿基地址,放eax
jmp finddll_end
not_found:
xor eax, eax ;没找到DLL,finddll函数返回eax为0
finddll_end:
pop ebx ;平衡栈
ret 8 ;平衡栈

strcompare: ;参数1 字符串1, 参数2 字符串2, 参数3 字符串长度
;返回eax,为0表示不相等,为1表示相等
push esi
push edi
push ecx
mov esi, [esp + 16] ;获得参数1
mov edi, [esp + 20] ;获得参数2
mov ecx, [esp + 24] ;获得参数3
xor eax, eax ;返回值eax设初值0
cld ;设置比较方向为地址增长方向
repz cmpsb ;如果ecx为0或比较的字节不相同则退出循环
test ecx,ecx
jnz strnotequ ;如果ecx不为0,说明比较字符不等,没比较完嘛
inc eax ;找到了,设置返回值eax为1,否则,不执行本语句,为0

strnotequ:
pop ecx
pop edi
pop esi
ret 0ch ;平衡栈,传递了3个参数

获取DLL中的函数地址

DLL对外暴露自己的函数的方式:

  1. 函数名
  2. 序号

注:函数名和序号并非一一对应

序号查找

好处:快、高效

不足:不够直观,不够稳定

做法:用一个数组存放函数的入口地址,这样存放和读取都方便,从n开始就做一个减法

函数名查找

好处:直观,具体

做法:让函数名表和函数地址表的索引一一对应

不足:有的函数名不暴露出来,会导致函数名表和函数地址表的索引不一致

解决方法:增加一个表描述对应关系,如图

image-20210519203923430

导出表的关键信息汇总

  1. 序号查找需要知道序号的最小值n,利用他可以直接计算函数地址表的索引
  2. 函数名表:函数名的字串表,每个名字以\0结尾
  3. 函数名表的元素个数X
  4. 函数地址索引表:元素个数就是函数名表的个数X,函数名表的第x项对应函数地址索引表的x项,其中存储的是该函数在函数地址表中的索引值
  5. 由函数名查找的方法是:找到函数名在函数名表的索引x,然后读函数地址索引表的第x项,假设该项的值为y,那么就在函数地址表的第y项拿到函数地址
  6. 函数地址表:存储的是函数的入口地址,不论用序号还是名字导出的函数,相应的函数地址都存在其中,函数地址表是按序号增序排列。序号和函数地址表索引的对应关系是:i = 序号 - n(最小序号)

PE格式中导出表的信息

在模块可选头中有个数据目录表,其中每项包括导出表、导入表、重定位表等;在导出表头中有如下信息:

  1. Address Table RVA就是函数地址表的RVA
  2. Ordinal Table RVA就是函数地址索引表的RVA,PE格式叫它序号表
  3. Ordinal Base就是最小的序号
  4. Number of names就是函数名表的条数
  5. Name Pointer Table RVA是函数名指针表,每项4个字节,存放一个RVA,指向一个函数名的字符串

image-20210519205042946

导出函数查找算法总结

  1. 从DLL加载的实际基址获取可选头,从其中数据目录表的第一项找到导出表入口RVA

  2. 从导出表的表头获取Number of names,即查找的最大循环次数

  3. 循环遍历函数名指针表,比对每项RVA指向的字串是否为要找的函数名

  4. 如果找到,记下此时函数名指针表项的索引,设为 i

  5. 根据索引 i,在序号表中找到对应项,获取其内容为n

  6. 以n为索引在函数地址表中找到函数入口的RVA,加上DLL的实际基址即为函数的实际入口地址

  7. 注:以上算法中,所有访问实际地址的地方,就用DLL的实际加载基址+RVA即可

程序设计——查找DLL中的函数地址

代码:

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
//为各个所需的偏移量定义宏
#define OFFSET_NTHDR_START_IN_DOSHDR 0x3c //DOS头偏移0x3c为NT头的偏移
#define OFFSET_NTHDR_EXPORT_DIR 0x78 //NT头偏移0x78为导出表的RVA
#define OFFSET_NAME_NUM_EXPORT 0X18 //导出表偏移0x18为函数名的数量
#define OFFSET_NAME_PTR_EXPORT 0X20 //导出表偏移0x20为函数名指针表RVA
#define OFFSET_ORDINAL_EXPORT 0X24 //导出表偏移0x24为序号表RVA
#define OFFSET_PROC_PTR_EXPORT 0x1c //导出表偏移0x1C为函数地址表RVA

//该函数需要3个参数,DLL基址,函数名,函数名长度,注意传参是从右向左压栈
__declspec(naked) _stdcall unsigned long
getProcEntry(char * base, char * procName, int procNameLen) {
_asm
{
push ebp
mov ebp, esp
mov edi, [ebp + 8]; //edi存放DLL基址(base)
mov ebx, edi
add ebx, OFFSET_NTHDR_START_IN_DOSHDR; //ebx: DOS头的e_lfanew字段内存地址(base+0x3C)
mov ebx, [ebx]; //ebx: NT头的RVA的值
add ebx, edi; //ebx: NT头的内存地址(RVA + base)

add ebx, OFFSET_NTHDR_EXPORT_DIR //ebx: 导出表RVA字段的内存地址
mov ebx, [ebx]; //ebx: 导出表的RVA值
add ebx, edi; //ebx: 导出表的内存地址(RVA + base)

mov ecx, ebx; //ecx: 导出表内存地址
add ecx, OFFSET_NAME_NUM_EXPORT //ecx: 导出表中函数名数目字段的内存地址
mov ecx, [ecx]; //ecx: 函数名数目的值,即设置好了循环次数

mov edx, ebx; //edx: 导出表内存地址
add edx, OFFSET_NAME_PTR_EXPORT //edx: 函数名指针表RVA字段的内存地址
mov edx, [edx]; //edx: 函数名指针表RVA的值
add edx, edi; //edx: 函数名指针表的内存地址(RVA+base)

push 0; //找到函数名,就放其在函数名指针表的索引i,初始为0

//以下循环比较函数名字符串
;//ebx 导出表内存地址 ecx 函数名数目的值 edx 函数名指针表内存地址 edi DLL基址

find_proc_loop:
mov esi, [edx]; //esi: 获得函数名指标表当前项的值,即函数名字符串的RVA的值
add esi, edi; //esi: 获得函数名字符串的内存地址
;//传参,调用我们之前写的strcompare函数
;//第1和第2个参数为两个字符串,第3个参数为字符串长度,参数从右往左传
push [ebp + 10h]; //压栈字符串长度
push esi; //压栈字符串2,函数名指针表项指向的字符串
push [ebp + 0ch]; //压栈字符串1,需要查找的函数名
call strcompare //字符串比较,调用完后清栈
test eax, eax //字符串比较结果,相等eax为1
jnz find_proc_found //找到了,跳转
add edx, 4 //没找到,edx指向下一个函数名指针表的表项
mov eax, [esp] //调用完strcmp后,esp指向之前的push 0,所以[esp]是循环值i
inc eax //i++
mov [esp], eax //继续在esp存循环变量的值,也是当前项的索引
dec ecx; //循环次数减1
jnz find_proc_loop //ecx不为0,继续遍历
pop eax; //没找到,清除栈内i,平衡栈
xor eax, eax; //eax置0,表示未找到
jmp find_proc_end
find_proc_found
……
find_proc_end
…… //平衡栈的相关操作

//以下为找到字符串后获取函数地址
;//eax 所查找函数名在函数名指针表的索引i
;//ebx 导出表内存地址
;//edi DLL基址

find_proc_found:
pop eax; //获得函数名指针表的索引

mov edx, ebx; //edx: 导出表内存地址
add edx, OFFSET_ORDINAL_EXPORT; //edx: 序号表RVA字段的内存地址
mov edx, [edx]; //edx: 序号表RVA的值
add edx, edi; //edx: 序号表的内存地址(RVA + base)

//序号表每项2字节,第i项为eax * 2,lea指令将该项的地址放入edx
lea edx, [edx + eax * 2];
xor eax, eax
mov ax, [edx]; //eax: 序号表中对应项的值,即为函数地址表的索引值,注意只放2字节

mov edx, ebx; //edx: 导出表内存地址
add edx, OFFSET_PROC_PTR_EXPORT; //ebx: 函数地址表RVA字段的内存地址
mov edx, [edx]; //ebx: 函数地址表的RVA的值
add edx, edi; //ebx: 函数地址表的内存地址(RVA + base)

//函数地址表每项4字节,eax为函数地址表的索引值,获得对应项的地址
lea eax, [edx + eax * 4];
mov eax, [eax]; //eax: 函数地址表中对应项的值,即为函数入口RVA的值
add eax, edi; //eax: 所查找函数的内存地址(RVA + base)

//Main函数

void main(int argC, char ** args)
{
OLECHAR * s1 = L“USER32.dll”; //DLL名字
void * p = MessageBox; //函数名字
unsigned long base; //DLL基址
//将前面写的获取DLL基址的汇编代码封装成函数,传参分别是DLL名字和DLL名字的长度
//注意名字长度为Unicode码,一个字符占2个字节
base = getDllBaseW((char *)s1, 20);
printf(“user32.dll base address is %x\n”, base); //打印获得的DLL基址
printf("messagebox address is %x\n",
getProcEntry((char *)base, “ActivateKeyboardLayout”, 22)); //打印函数地址
}

将finddll和getproc函数放入寄生的病毒代码

数据区改进:

image-20210519211017574

增加的代码是:

1
2
3
4
5
6
//填写user32.dll的unicode串
OLECHAR * s1 = L“USER32.dll”; //unicode码
//user32.dll放到寄生代码数据区偏移24字节处
memcpy(virusData + 24, s1, 20);
//MessageBoxA放到寄生代码数据区偏移44字节处
strcpy(virusData + 44, "MessageBoxA");

程序设计图:

image-20210519211156523

例题

  1. 关于导出表的说法,以下正确的是( )
    A. 函数地址表中存放的是函数入口VA(RVA)
    B. 导入表中具有函数名表,通过函数名表的索引直接就可以定位到函数地址表中相应的项
    C. DLL中所有的函数都有函数名和序号
    D. 序号表中存储的是函数的序号
    E. 以上都不正确

参考答案:E

解析:

A. 函数地址表中存的是函数的入口地址;

B. 通过函数名表索引定位函数地址索引表索引,再根据函数地址索引表内容定位函数地址表的索引;

C. 有的函数名不暴露;

D. 序号表存的是函数名表和函数地址表的对应关系