0x00 前言

免杀中,杀软经常盯上的几个位置里,常常会发现输入表的影子。闲来动手尝试一下手工隐藏输入表的方法,网上已经利用,我也是作为测试,把过程记录下来与大家分享。

0x01 利用的关键函数

实现输入表隐藏,需要kernel32.dll中两个很常用的函数。LoadLibraryA和GetProcAddress。

LoadLibrary函数作用为将指定的可执行模块映射进调用进程的地址空间。可以这样看:我们把一个dll文件丢给LoadLibrary,让他返回给我们dll的句柄,有了这个句柄,我们就进一步利用之找到dll中的函数。不难发现,LoadLibrary需要一个参数,就是dll文件名,返回值为模块句柄。

GetProcAddress函数返回指定的输出动态链接库内的函数地址。英汉互译一下很简单:就是取得(Get)函数/过程(Proc)地址(Address)。

可以这样看:我们通过LoadLibrary得到dll的模块句柄,现在我们要得到dll里面某个函数的地址,就将函数名称丢给这个函数,GetProcAddress就忙里忙外的搞定这个函数地址,返回给我们。同样的可以看到,GetProcAddress需要两个参数,一是dll的模块句柄,上面已经获得;另一个是要查看的函数名称,这个当然我们自己要查什么就丢给他什么。返回值便是此函数的地址了。

0x02 如何利用

知道了上面两个关键函数,我们实际怎样实现隐藏过程呢?总不能直接和函数说:Hi,老兄,帮我隐藏一下输入表吧,他们听不懂。那么我们要做的就是:一步一步的指点函数执行。

大家知道PE程序文件存放在磁盘下,是有一定结构的,因为这些结构,只认识01的计算机才知道,这个文件是个PE可执行程序。把程序看成一个简单的文件,他们也不过是一串和我们文档一样的01,01我们看不懂,我们可以看16进制。然后我们就请来C32Asm查看程序的16进制。

但不得不说,有些朋友可能还不知道,输入表是什么?

输入表也被称为“导入表”。我们常会看见一些大型软件有很多的动态链接库文件(DLL文件),这些文件中有很多导入函数,这些函数不会直接被执行。当一个程序(EXE)运行时,导入函数是被程序调用执行的,其执行的代码是不在主程序(EXE)中的一小部分函数,真正的代码在DLL文件中。那么EXE主程序是如何找到这些需要导入的函数呢,这就要归结于“输入表”了。

输入表就相当于EXE文件与DLL文件沟通的钥匙,所有的导入函数信息都会写入输入表中。在PE文件映射到内存后,windows将相应的DLL文件装入,EXE文件通过“输入表”找到相应的DLL中的导入函数的地址,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。

既然这样,输入表必然是在程序自身文件中了。我们要隐藏输入表中的函数,就要在文件中把要隐藏的输入表函数的内存指针给砍掉,但在软件启动时自动恢复。

0x03 动工

在C32Asm中打开测试的目标程序,一个自己写的不知名的小软件。我们要隐藏的函数就是shell32.dll下的ShellExecuteA。这个函数与WinExec功能类似,都是调用外部程序执行。软件中就多处调用,比如通过这个API函数直接打开系统自带的cmd命令提示符窗口。如图1

img

那么怎么查看输入表是否有此函数呢?拿出Stud_PE看一下吧,如图2。

img

很容易看到,在shell32.dll下有个输入函数为ShellExecuteA,此函数对应的内存指针存放在程序偏移地址为001048A0的地方。既然输入表是文件的一部分,那么这个调用的函数在C32Asm中必然也可以找到咯。通过Ctrl+F搜索,找到如图3之处。

img

我们记录下这几个地址:要调用的dll文件名为shell32.dll,文件偏移地址是00103966,要隐藏的API名为ShellExecuteA,文件偏移地址是00103988。但是要知道,我们手工修改的方式是,通过程序启动时修改代码,这样,我们需要得到这两个文件偏移地址对应的内存地址。这个用偏移量转换器OC就可以解决。得到两个地址分别为00506766和00506788。收集的这些信息还是不足以实现的,缺什么呢?当然是LoadLibraryA和GetProcAddress这两个函数了。但我们利用这两个函数,是需要他们执行的,所以我们要得到的是这两个函数对应的内存指针地址,从而方便调用。这个也好解决,将程序载入到OD中,按下Ctrl+N找到“模块中的标签窗口”。在这里找到LoadLibraryA和GetProcAddress函数的内存指针地址,分别是005042BC和005041CC,如图4。有了这些,还缺少什么呢?不知道?那么我们的目的是修改什么呢?

img

到这里应该可以明白整体思路了,我们要在文件中将ShellExecuteA这个API的内存指针给抹掉,这样工具就不会查到输入表中有调用此API,但程序还是需要这个API的,所以我们在程序启动前添加一段命令,恢复该API函数的内存指针即可。同样在OD中按下Ctrl+N的“模块中的标签窗口”,找到ShellExecuteA这个API的内存指针地址是005048A0,细心的你会发现,这个内存地址就是用Stud_PE查看的RVA偏移地址加上基地址00400000。

下面我们要做的便是写一串汇编指令,使其在程序执行到原入口点之前完成输入表函数内存指针的恢复操作。

首先,在程序中找一段无用的空间,只要足以写下命令即可,如果空间不足,可以用zeroadd增加区段。这里直接找现成的空间,在004FE2A0处发现大量的00,但在图5中,这段00已经被我无情的征用

img

但从上面的00还可以看到他的遗迹...双击此处开始写下面的汇编指令:

004FE2A0   >$  60                  pushad//1
004FE2A1   .  E8 00000000     call   004FE2A6
004FE2A6   $  5D                    pop  ebp
004FE2A7   .  83ED 06             sub  ebp, 6//2
004FE2AA   .  8BDD                 mov  ebx,ebp
004FE2AC   .  81C3 C6840000 add   ebx, 84C6//3
004FE2B2   .  53                       push  ebx//4
004FE2B3   .  8BDD                  mov  ebx, ebp
004FE2B5   .  81C3 1C600000  add   ebx, 601C
004FE2BB   .  FF13                    call   near dword ptr ds:[ebx]
004FE2BD   .  8BF8                   mov  edi, eax//5
004FE2BF   .  8BDD                   mov ebx, ebp
004FE2C1   .  81C3 E8840000   add   ebx, 84E8
004FE2C7   .  53                        push  ebx
004FE2C8   .  57                        push  edi
004FE2C9   .  8BDD                   mov  ebx, ebp
004FE2CB   .  81C3 2C5F0000   add   ebx, 5F2C
004FE2D1   .  FF13                     call   near dword ptr ds:[ebx]//6
004FE2D3   .  8BDD                   mov  ebx, ebp
004FE2D5   .  81C3 00660000   add   ebx, 6600
004FE2DB   .  8903                    mov  dword ptr ds:[ebx], eax//7
004FE2DD   .  61                        popad
004FE2DE   .^ E9 35FFFFFF        jmp  004FE218

下面一点一点进行分析:

第一处:pushad与004FE2DD处的popad对应,为了保存环境,使得跳转回原入口点时寄存器等恢复程序载入时的状态。

第二处:这里有点忽悠人的感觉,首先Call调用的只是004FE2A6的命令,仿佛没有必要,但pop ebp又是什么意思呢?这得看Call指令实现过程了。在执行Call指令时实际做了两件事Push EIP,然后转移。EIP对应的是下一条指令的位置,故执行Call后,004FE2A6被压入堆栈。这样pop ebp就有解释了,将压入堆栈的004FE2A6弹出到ebp寄存器中。跟着,sub指令将地址减去6,得到004FE2A0,正是我们修改后的程序入口点位置。代码这样设计有什么作用呢?其实作用的确不大,写出这个代码的童鞋,可能只是用这样一种有趣的方法来得到当前修改后的OEP。到了后面代码大家会发现,这其实的确没有必要。

第三处:将EBP寄存器存储的地址存放到EBX,add ebx, 84C6得到的正是00506766,也就是shell32.dll的内存地址。现在知道了,代码编写者是用相对偏移的方式找到对应的内存地址。当然,我们可以简单一些,不用这种方法。比如直接将地址00506766赋给,EBX大家可以自己实践一下。执行完这一行,我们可以看到,ebx对应的地址处赫然显示着shell32.dll,如图6。

img

第四处:将ebx中的地址压入堆栈。作用很简单,为LoadLibrary的执行提供参数。

第五处:类似的代码使得ebx指向LoadLibrary内存指针(add ebx, 601C得到的正是内存指针005042BC)。接着Call调用了LoadLibrary,从而得到shell32.dll的模块句柄,默认返回的参数存放在EAX寄存器中。因此使用 mov edi, eax将得到的结果保存到EDI寄存器中。

第六处:和上面类似的代码,大家可以猜到作用了:利用GetProcAddress得到ShellExecuteA这个API指向的内存地址。这里得到的这个地址,正是本来正常后来却被我们隐藏的API内存指针指向的地址75F89BA5。如图7。

img

第七处:经过上面一番折腾,搞定了ShellExecuteA的地址,下面就是“修复”被隐藏的内存指针的过程了, add ebx, 6600后得到005048A0,也就是那个内存指针的位置。如果一切正常,没有我们的干预,内存指针存放的应该正是75F89BA5,但我们要隐藏,所以事先已经将这个内存指针给清空为0了,这样,我们就用mov dword ptr ds:[ebx], eax将EAX寄存器中存放的75F89BA5还给苦逼的内存指针。

完成了上面的步骤,就是恢复环境,跳转向程序的原入口点004FE218正式执行咯。我们通过OD的dump或是其他像LordPE之类的工具将程序的入口点修改为004FE2A0。保存一下,一个半成品就诞生了。

0x04 手工隐藏

为什么前面说是半成品呢?因为我们还没删掉对应的内存指针啊。现在我们已经知道,要隐藏的内存指针地址是005048A0,用偏移量转换器OC查看一下发现对应的文件偏移地址是001048A0。用C32Asm打开修改保存后的文件,按下Ctrl+G来到001048A0处,将其对应的四个字节填充为0。如图8。

img

这样,内存指针被清理了。用Stud_PE和OD的参考都不会发现有这个API了。

img

但是程序调用ShellExecuteA执行外部程序的功能并无异常。其实有想法的童鞋会发现,用ImportREC载入已经运行的程序,会发现的确还是有这个API的,也就是说,我们实现的隐藏只是在程序未启动时的隐藏。

0x05 延伸

这里的隐藏仍然有一定的局限性,比如不容易实现大范围的隐藏,而当杀软定位到某个输入表函数时,这种方法比较适用。对于大范围的输入表移位,通过ImportREC的输入表重建功能就可以实现。简单的解释过程就是:使用LordPE得到输入表的RVA和大小,在ImportREC中填入后单击获取输入表,删除无效指针,修复转存一下,然后用C32asm找到先前的输入表用0填充即可。此外,对于输入表的加密,我们还可以换一下方法,用异或算法将内存指针处先异或一遍得到密文,然后同样在程序启动前加一段代码进行解密,这样就免除了调用LoadLibraryA和GetProcAddress找API地址的麻烦。当然,异或的方法在输入表免杀以外依旧是适用的。尝试总结一番,必然有自己的收获。

Comments
Write a Comment