Windows提供了大量的API接口,这极大地提高了程序员软件开发的效率,但是通过逆向分析及API的提示下就可获取到大量信息,从而定位到目标程序的关键代码段,大大减少了逆向分析的工作难度。
所以隐藏对API 的调用可以有效地提高程序的抗分析能力,同时也增加了软件破解的难度。下面逐一介绍隐藏API调用的方法。
首先,看一下最简单的情况:
MessageBox(NULL, “Hello World!”, “Test”, MB_OK);
用IDA查看:
.text:004113C0 push 0 ; uType
.text:004113C2 push offset Caption ; “Test”
.text:004113C7 push offset Text ; “Hello World!”
.text:004113CC push 0 ; hWnd
.text:004113CE call ds:__imp__MessageBoxW@16 ; MessageBoxW(x,x,x,x)
这是我们最常用的形式,IDA等反汇编工具会轻易地对他们进行标识。
好了,我们来看一下稍微复杂点的情况:
HMODULE hDll = LoadLibrary(L”user32.dll”);
PFUN pMsg = (PFUN)GetProcAddress(hDll,”MessageBoxW”);
pMsg(NULL, L”Hello World!”, L”Test”, MB_OK);
对应的汇编代码如下:
.text:00411A30 push offset LibFileName ; “user32.dll”
.text:00411A35 call ds:__imp__LoadLibraryW@4 ; LoadLibraryW(x)
.text:00411A3B cmp esi, esp
.text:00411A3D call j__RTC_CheckEsp
.text:00411A42 mov [ebp+hModule], eax
.text:00411A45 mov esi, esp
.text:00411A47 push offset ProcName ; “MessageBoxW”
.text:00411A4C mov eax, [ebp+hModule]
.text:00411A4F push eax ; hModule
.text:00411A50 call ds:__imp__GetProcAddress@8 ; GetProcAddress(x,x)
.text:00411A56 cmp esi, esp
.text:00411A58 call j__RTC_CheckEsp
.text:00411A5D mov [ebp+var_14], eax
.text:00411A60 mov esi, esp
.text:00411A62 push 0
.text:00411A64 push offset aTest ; “Test”
.text:00411A69 push offset aHelloWorld ; “Hello World!”
.text:00411A6E push 0
.text:00411A70 call [ebp+var_14] //调用MessageBoxW
可以看到,IDA 没有识别我们 的MessageBox(),这是值得庆贺的,但它识别除了LoadlLibrary(),GetProcAddress().
这在 某种程度上也揭示我们将要调用的API ,所以我们需要寻找更好的方法。
请看以下代码段:
_asm
{
push MB_OK
push StringAddress
mov eax,0
push eax
push eax
mov eax,offset Label001
push eax
jmp [dwFunctionAddress] //call MessageBox()
Label002:
Label001:
nop
nop
}
以上代码段中的
mov eax,offset Label001
push eax
jmp [dwFunctionAddress]
实现了CALL 指令的功能,且功能有所增强,如果使用CALL 指令,API函数的返回地址必定是紧接CALL 指令的下一条指令的地址。
而此代码段可返回到任意指定地址,形成更具迷惑性的代码。
_asm
{
push MB_OK
push StringAddress
mov eax,0
push eax
push eax
mov eax,offset Label001 ;;返回到Label001
push eax
jmp [dwFunctionAddress] ;;call MessageBox()
Label002:
//Insert invalid instructions here to confuse the code analysiser
//Instructions between the Label001 and Label002 will never be executed.
nop
nop
call dwFunctionAddress
Label001:
nop
nop
}
由IDA 得到的汇编代码如下:
.text:00401175 push 0
.text:00401177 push [ebp+var_318]
.text:0040117D mov eax, 0
.text:00401182 push eax
.text:00401183 push eax
.text:00401184 mov eax, offset loc_401198
.text:00401189 push eax
.text:0040118A jmp [ebp+var_314]
.text:0040118A main endp
.text:0040118A
.text:0040118A ; —————————————————————————
.text:00401190 db 2 dup(90h) ;;我们的NOP,
;;可以插入任意数据
.text:00401192 ; —————————————————————————
.text:00401192 call dword ptr [ebp-314h]
.text:00401198
.text:00401198 loc_401198: ; DATA XREF: main+174 o
在这,IDA 已经不能正确分析,说明此方法非常有效。<>
的作者对这种思想有以下描述:
It can be very difficult to identify such functions (especially if they have no prolog);
a context search gives no result because the body of any program contains plenty
of JMP instructions used for near jumps.
How, then, can we analyze all of them? If we don’t identify the functions, two of them will drop
out of sight — the called function and the function to which control is passed just upon returning.
Unfortunately, there is no quick and easy solution to this problem;
the only hook here is that the calling JMP practically always goes beyond the boundaries of the function
in whose body it’s located.
We can determine the boundaries of a function by using an epilog.
翻译为中文大意如下:
识别这样的函数是非常困难的(特别是当这些函数没有 prolog的时候);因为任何程序体都包含大量 的用来实现近跳转的 JMP 指令,
所以联系上下文进行搜索不会有结果。那我们怎样识别这些函数呢?如果我们不识别出这些函数
,被调用的函数以及函数的返回地址将会逃出我们的视线。不幸的是,这个问题没有快速且简单的解决方法。这里唯一的线索是:
调用跳转总是会跳转到调用跳转指令所在的函数体的外部。我们可以通过使用epilog来检测函数的边界。
好了,下面我们回到LoadLibrary(),GetProcAddress().
如果我们使用这种方法来获取 API 函数地址,在程序中必定会出现字符串,
如user32.dll,kernel32.dll,MessageBoxA…等。这些信息可以通过分析工具轻易地得到,
这对于增强程序的抗分析能力 是无益的。
所以,加密字符串并在代码使用他们是进行动态解密是十分必要的。
我第一次看到这样的示例是在一个Downloader 中:
PS______:0040361C ; LPCSTR lpString2
PS______:0040361C lpString2 dd offset dword_403620 ; DATA XREF: CreateBOLE_INI:loc_403B5B r
PS______:0040361C ; CreateBOLE_INI+83 r …
PS______:00403620 dword_403620 dd 1D151517h, 16081708h, 5050505h, 5551514Dh, 4C0A0A1Fh
PS______:00403620 ; DATA XREF: PS______:lpString2 o
PS______:00403620 dd 460B4C4Ch, 554C564Dh, 51404B0Bh, 564C490Ah, 5D510B51h
PS______:00403620 dd 5050551h, 8 dup(5050505h), 37h dup(20202020h), 0
PS______:00403B51 xor ecx, ecx ; 0
PS______:00403B53 cmp dword_403618, ebx ; compare 4CH with 0
PS______:00403B59 jbe short loc_403B7D
PS______:00403B5B
PS______:00403B5B loc_403B5B: ; CODE XREF: CreateBOLE_INI+98 j
PS______:00403B5B mov eax, lpString2
PS______:00403B60 xor byte ptr [eax+ecx], 25h
PS______:00403B64 add eax, ecx ; useless
PS______:00403B66 mov eax, lpString2
PS______:00403B6B add eax, ecx
PS______:00403B6D cmp byte ptr [eax], 20h
PS______:00403B70 jnz short loc_403B74
PS______:00403B72 mov [eax], bl ;;BL==0
PS______:00403B74
PS______:00403B74 loc_403B74: ; CODE XREF: CreateBOLE_INI+8D j
PS______:00403B74 inc ecx
PS______:00403B75 cmp ecx, dword_403618
PS______:00403B7B jb short loc_403B5B
PS______:00403B7D
此代码段的功能非常简单,循环将一长度为4ch的加密字符串解密,使用 XOR ,如果与25H 异或后得到20H,则用0替换,
0010 0101 ;;(0010 0101)=25h
0000 0101 XOR ;;(0000 0101)=5h
0010 0000 ;;(0010 0000)=20h
也就是说,原字符串中数值为05H的字节均被0替换
为了生动地将这一过程展献给读者,有了以下程序:
#include
#include
void main()
{
int i=0;
DWORD String[13]={
0x1D151517, 0x16081708, 0x5050505, 0x5551514D, 0x4C0A0A1F,
0x460B4C4C, 0x554C564D, 0x51404B0B, 0x564C490A, 0x5D510B51,
0x5050551,0x5050505,0x5050505};
char *pString=NULL;
char DestinationString[0x4c];
//////////////////////////////////////////////////////////////////////////////////////////
CopyMemory(DestinationString,String,0x4c);
for(i=0;i<0x4c;i++)
{
*(DestinationString+i)=*(DestinationString+i)^0x25;
if(*(DestinationString+i)==0x20)
{
*(DestinationString+i)=0;
}
if(*(DestinationString+i)==’\0′)
{
printf(“\n”);
}
else
{
printf(“%c”,*(DestinationString+i));
}
}
printf(“\n\n”);
//////////////////////////////////////////////////////////////////////////////////////////
CopyMemory(DestinationString,String,0x4c);
for(i=0;i<0x4c;i++)
{
*(DestinationString+i)=*(DestinationString+i)^0x25;
if(*(DestinationString+i)==0x20)
{
*(DestinationString+i)=0;
}
if(*(DestinationString+i)==’\0′)
{
printf(“\n”);
printf(“%d\n”,i);
}
else
{
printf(“%c”,*(DestinationString+i));
}
}
}
此程序有如下输出:
2008-2-3
8
9
10
11
http://iii.chsip.net/list.txt
41
42
43
44
45
46
47
48
49
50
51
其中,http://iii.chsip.net/list.txt 是Downloader要用到的列表文件,他会将解密生成的字符串写入BOLE.INI文件中。
好了,不要扯的太远,我们要调用的API 名称字符串也可以用相同的方法来操作。
事实上,在Win32病毒技术中,kernel32.dll 在进程地址空间中的基址,GetProcAddress()函数地址都是通过搜索kernel32.dll 的内存空间得到的。
所以病毒体中的API调用相对比较隐蔽,但也完全可以应用calling jmp 及字符串动态解密技术增强他们的抗分析能力。
这种方法在壳中是非常常见的,有人甚至连LoadLibrary和GetProcAddress这两个函数完全重写,让你在用IDA分析时,看不到一个API的调用。