今天我们来讲一个简单但很有效的绕过 Kaspersky Internet Security 7.0 网络监控的方法,KIS 7.0 的网络监控是现如今个人电脑上最流行、最著名的保护程序之一。对于这个产品,人们有褒有贬。用或不用完全在您,这里只是阐述客观事实。我们来看下时下最新版本 7.0.0.125。绕过的方法类似于利用缓冲区溢出漏洞。概念上的证明这里就不进行了,免得我们的小兄弟——script kidds——太过兴奋。要写代码的话,连带调试有 30 分钟就能搞定。闲话少说,开始动手。
有一种技术极其著名而且常用,这就是注入(inject)。关于这种技术的介绍已经很多了,而且所有已经公开的注入技术都能被我们这里用作小白鼠的卡巴检测出来。注入技术的本质就是用某种合适的方法在已信任的程序中执行代码。大多数用户在上网时都要使用浏览器,所以浏览器程序一定被列在网络监控的信任列表里,这时网络监控会默认允许浏览器向任何服务器的 80 端口发送数据。我们所利用的就是这一点——将代码注入到浏览器里,并通过浏览器来发送和接受数据。喏,这点大家都知道 =) 可是如何实现注入呢?如何绕过 KIS 主动防御这道门槛呢?办法居然十分简单。
注入过程总共有两步 - 1) 写入代码 2) 将控制权传给这段代码。写入代码嘛……办法可以想出很多,但我只讲一个独用于 KIS 的秘法。一般来说,向另外一个进程写入代码用的都是 KERNEL32!WriteProcessMemory 函数,更底层些的还有 Native 版的 NTDLL!NtWriteVirtualMemory。我们这里就不讲它的参数了,要查看参数信息,可以用 IDA 看反汇编代码,=) 这当然是开玩笑了,去看 Micro$oft 的 MSDN 吧。通常向其它进程写入代码的行为都会被 KIS 拦截,但也不一定。破解任何一种保护方式的基础是破解它实施保护所依赖的前提。KIS 的前提是可以向没有标记为可执行(即没有 EXECUTABLE 属性)的虚拟内存中写入。如果内存没有这个属性,那就可以任意向其中写入,而 KIS 则放手不管。这一点纯粹是通过试验手段得来的。很好。我们开始写入代码。但是向哪里写呢?有一个简单的办法——写入堆栈内存中。但是一般的方法太没意思了,用一个一箭双雕的办法向堆栈中的指定位置写入代码才最有意思,而这里的“双雕”正是进程里的两个注入点。大家可能都有所意会了。还没明白的就接着往下看吧。对于写入部分已经讲明白了,代码的执行也将在堆栈中进行——在接下来将要执行我们程序的线程的堆栈中。线程在调用函数时,函数的返回地址通常也是在堆栈中。把这个返回地址换成我们程序的地址,我们就劫持了线程,使其执行我们的同样位于堆栈中的代码。我知道,由于一些现代的技术,堆栈中的代码并不总是能执行的。喏,我们的写入位置可以不仅仅在堆栈中。这个问题就留给大家了。这个简单注入的方案可以很明晰地分为以下几步:
1. 创建进程,以默认方式运行浏览器文件。
2. 执行一段时间后停止浏览器线程。
3. 获得其上下文——我们需要堆栈指针。
4. 分析堆栈中的返回地址。
5. 写入我们代码的地址。
6. 恢复线程执行。
在这个算法里有一个地方最不容易实现——如何分析堆栈得出某函数返回的地址。我使用了下面的方法——沿堆栈向下搜索,会取得一个个的 DWORD。每个 DWORD 都可能是返回地址。是否是返回地址可用如下方法验证——读取 DWORD,取得 DWORD 表示的地址,并沿此地址往回进行反汇编。如果遇到了 CALL 指令,那就意味着这个地址就是返回地址。简单的说明如下:
CALL X
RET_ADDRESS:
PROC X
….
假设我们在执行 X 函数时停止了线程,在堆栈中就会存在着保存在寄存器中的局部变量(STDCALL 调用方式)、保存在堆栈中的临时数据,当然还有函数返回地址。这个返回地址就是 RET_ADDRESS。它的值我们应该可以通过分析堆栈来找到。这里我们可能会想,分析过程中我们可以依赖 EBP 的值,这个值一般也是保存在堆栈中的,大家可以回忆一下多数函数的标准的 prologue。EBP 的值我们可以通过上下文取得。但是,实践证明这一点并不是完全可以信赖的,因为在某个具体函数调用的上下文中被停止的线程的堆栈里可能仅仅有一个返回地址,别的就再没有什么了。实现这个技术需要一个指令长度反汇编器以及对指令的分析。结果我们得到了一个具备绕过 KIS 7 功能的最小函数的伪代码:
CreateProcess(BrowserFilePath,…,ProcessInformation); //创建进程
Sleep(1000); // 睡会儿
SuspendThread(ProcessInformation.hThread); // 停掉最初生成的线程
GetThreadContext(ProcessInformation.hThread,&Context);// 获取上下文
ReadProcessMemory(ProcessInformation.hProcess,Context.Esp,StackChunk,1000*4);// 从 ESP 里读 1000 个 DWORD
for (i=0;i<1000;i++) // 分析线程堆栈
{
ReadProcessMemory(ProcessInformation.hProcess,StackChunk[i]-10,CodeBuffer,10);
// 读 10 字节 – 如果是返回地址则这 10 个字节里就会包含着 CALL 指令
for (j=0;j<10;j++)
{
disasm(&(CodeBuffer[9-j]),&Instr); // 分析指令? 向后?
if ( ( Instr.Length == j+1 ) &&
(Instr.OpCode == 0xe8 || Instr.OpCode == 0xff || Instr.OpCode == 0x9a)
) // 是 CALL 指令吗?
{
WriteProcessMemory(ProcessInformation.hProcess,
(Context.Esp+StackChunk[i] - StackChunk),&ShellCodeAddress,4);
// 写我们的 shellcode 的地址
ResumeThread(ProcessInformation.hThread); // 恢复线程执行
}
}
}
WASM 小组的 *dead_body* 提出了这个绕过的思想,谨向他表示敬意。永远怀念已逝的程序天才 Ms-Rem。我们等待 KIS 研发团队的官方回应。再见。
mailto –mental_mirror@freed0m.org
[C] Mental_Mirror
有一种技术极其著名而且常用,这就是注入(inject)。关于这种技术的介绍已经很多了,而且所有已经公开的注入技术都能被我们这里用作小白鼠的卡巴检测出来。注入技术的本质就是用某种合适的方法在已信任的程序中执行代码。大多数用户在上网时都要使用浏览器,所以浏览器程序一定被列在网络监控的信任列表里,这时网络监控会默认允许浏览器向任何服务器的 80 端口发送数据。我们所利用的就是这一点——将代码注入到浏览器里,并通过浏览器来发送和接受数据。喏,这点大家都知道 =) 可是如何实现注入呢?如何绕过 KIS 主动防御这道门槛呢?办法居然十分简单。
注入过程总共有两步 - 1) 写入代码 2) 将控制权传给这段代码。写入代码嘛……办法可以想出很多,但我只讲一个独用于 KIS 的秘法。一般来说,向另外一个进程写入代码用的都是 KERNEL32!WriteProcessMemory 函数,更底层些的还有 Native 版的 NTDLL!NtWriteVirtualMemory。我们这里就不讲它的参数了,要查看参数信息,可以用 IDA 看反汇编代码,=) 这当然是开玩笑了,去看 Micro$oft 的 MSDN 吧。通常向其它进程写入代码的行为都会被 KIS 拦截,但也不一定。破解任何一种保护方式的基础是破解它实施保护所依赖的前提。KIS 的前提是可以向没有标记为可执行(即没有 EXECUTABLE 属性)的虚拟内存中写入。如果内存没有这个属性,那就可以任意向其中写入,而 KIS 则放手不管。这一点纯粹是通过试验手段得来的。很好。我们开始写入代码。但是向哪里写呢?有一个简单的办法——写入堆栈内存中。但是一般的方法太没意思了,用一个一箭双雕的办法向堆栈中的指定位置写入代码才最有意思,而这里的“双雕”正是进程里的两个注入点。大家可能都有所意会了。还没明白的就接着往下看吧。对于写入部分已经讲明白了,代码的执行也将在堆栈中进行——在接下来将要执行我们程序的线程的堆栈中。线程在调用函数时,函数的返回地址通常也是在堆栈中。把这个返回地址换成我们程序的地址,我们就劫持了线程,使其执行我们的同样位于堆栈中的代码。我知道,由于一些现代的技术,堆栈中的代码并不总是能执行的。喏,我们的写入位置可以不仅仅在堆栈中。这个问题就留给大家了。这个简单注入的方案可以很明晰地分为以下几步:
1. 创建进程,以默认方式运行浏览器文件。
2. 执行一段时间后停止浏览器线程。
3. 获得其上下文——我们需要堆栈指针。
4. 分析堆栈中的返回地址。
5. 写入我们代码的地址。
6. 恢复线程执行。
在这个算法里有一个地方最不容易实现——如何分析堆栈得出某函数返回的地址。我使用了下面的方法——沿堆栈向下搜索,会取得一个个的 DWORD。每个 DWORD 都可能是返回地址。是否是返回地址可用如下方法验证——读取 DWORD,取得 DWORD 表示的地址,并沿此地址往回进行反汇编。如果遇到了 CALL 指令,那就意味着这个地址就是返回地址。简单的说明如下:
CALL X
RET_ADDRESS:
PROC X
….
假设我们在执行 X 函数时停止了线程,在堆栈中就会存在着保存在寄存器中的局部变量(STDCALL 调用方式)、保存在堆栈中的临时数据,当然还有函数返回地址。这个返回地址就是 RET_ADDRESS。它的值我们应该可以通过分析堆栈来找到。这里我们可能会想,分析过程中我们可以依赖 EBP 的值,这个值一般也是保存在堆栈中的,大家可以回忆一下多数函数的标准的 prologue。EBP 的值我们可以通过上下文取得。但是,实践证明这一点并不是完全可以信赖的,因为在某个具体函数调用的上下文中被停止的线程的堆栈里可能仅仅有一个返回地址,别的就再没有什么了。实现这个技术需要一个指令长度反汇编器以及对指令的分析。结果我们得到了一个具备绕过 KIS 7 功能的最小函数的伪代码:
CreateProcess(BrowserFilePath,…,ProcessInformation); //创建进程
Sleep(1000); // 睡会儿
SuspendThread(ProcessInformation.hThread); // 停掉最初生成的线程
GetThreadContext(ProcessInformation.hThread,&Context);// 获取上下文
ReadProcessMemory(ProcessInformation.hProcess,Context.Esp,StackChunk,1000*4);// 从 ESP 里读 1000 个 DWORD
for (i=0;i<1000;i++) // 分析线程堆栈
{
ReadProcessMemory(ProcessInformation.hProcess,StackChunk[i]-10,CodeBuffer,10);
// 读 10 字节 – 如果是返回地址则这 10 个字节里就会包含着 CALL 指令
for (j=0;j<10;j++)
{
disasm(&(CodeBuffer[9-j]),&Instr); // 分析指令? 向后?
if ( ( Instr.Length == j+1 ) &&
(Instr.OpCode == 0xe8 || Instr.OpCode == 0xff || Instr.OpCode == 0x9a)
) // 是 CALL 指令吗?
{
WriteProcessMemory(ProcessInformation.hProcess,
(Context.Esp+StackChunk[i] - StackChunk),&ShellCodeAddress,4);
// 写我们的 shellcode 的地址
ResumeThread(ProcessInformation.hThread); // 恢复线程执行
}
}
}
WASM 小组的 *dead_body* 提出了这个绕过的思想,谨向他表示敬意。永远怀念已逝的程序天才 Ms-Rem。我们等待 KIS 研发团队的官方回应。再见。
mailto –mental_mirror@freed0m.org
[C] Mental_Mirror