探索黑客技术攻防,实战研究与安全创新

导航菜单

编程实现Linux环境的二进制文件反调试

 最近迷上了crackmes.me上的linuxcrackme,在相对高级的crackme中大多包含了反调试功能。这令我想起之前玩过的forensicchallenge,好多攻击者都把自己写的攻击程序留在服务器上,这样取证人员只要把攻击者留下的程序拖进ida或者用strace、gdb动态跟踪一下,很快就能通过程序调用的函数对程序定性。如不断调用socket、connect连接一个c段IP地址的多是端口扫描,如果其中包括某些常见的密码且端口号固定21、22、23、3306之类的,很可能是passwordcracktool。查看字符串,如果用了xx_irc,就很可能就是IRC马。

因此,掌握反调试技术是非常必须的。在google上以关键字搜索linuxantidebug就会找到不少相关文章,可是提到的trick大多都是需要改源代码实现的,很不方便,因此我想通过对二进制直接插入shellcode自动实现反调试的功能。

首先还是列举下我常用的调试程序的工具,有攻才有防嘛。首先是GNU的binutils系列工具,gdb、objdump、nm等;之后是strace和ltrace,用于动态跟踪系统调用和函数调用;最后是ida和ndisasm,用于静态反调试。

接下来就是对它们逐一击破了。首先对于第一类gnu的binutils类工具,默认标准都是以elf的section角度来识别elf文件的(linkingview),但Linux内核是以segment角度,elfsection的作用都是用于链接文件和调试程序的,不是内核加载执行文件所必须,因此只要破坏elf的sectionhead,就能使binutils类的工具拒绝加载。破坏sectionhead后的效果如图1所示。

C1.png

接着第二类strace和ltrace则都是利用ptrace来跟踪子程序的,anti-ptrace估计有经验的都知道,只要在进程中调用ptrace跟踪自己(PTRACE_TRACEME),如果返回-1即证明被跟踪了,以这个条件可以判断是否被调试,同时调用过PTRACE_TRACEME后,gdb、strace

就不能再以进程调式(gdb–-pid,strace–p)等方式加载已运行的进程了。

最麻烦的要数第三类静态调试了。ida这类静态反汇编令我们的反调试代码赤裸裸地暴露,但由于是流式字节反汇编,因此我们能用xor编码和插入垃圾指令来混淆欺骗,但我们也知道,这样做只能提高反汇编的成本而已。

既然反调试的方法已经清楚,那么接下来就是如何实现了。对付第一类,我们仅需编辑elf文件就能实现,如图2所示。

C2.png

代码很简单吧?函数传入参数memap,是把二进制文件mmap到内存的指针。这里用到一些elf数据结构,下面简单介绍,不熟悉的可以查下manpage和看看/usr/include/elf.h。

一个elf文件的简略组成如图3所示,其中的sectionhead、programhead位置表述可能并不准确,但通过上图可以知道,我们能通过elf_head查找到sectionhead的位置(代码132行,通过elfhead中的e_shoff+文件基址),别忘记我们的目的仅仅是破坏它(代码139-142行)。直接向sectionhead中填充随机数,这样binutils系列工具就拒绝加载文件了。

C3.png

第二类和第三类的反调试,我们需要将shellcode插到elf中。我的做法是先写带有反调试功能的shellcode,把程序的入口点指向shellcode,让shellcode下反调试陷阱后,再jmp跳回执行源程序。以下是shellcode的代码。


[root@localhostsc]#objdump-dsc2.osc2.o:
fileformatelf32-i386
Disassemblyofsection.text:
00000000anti_debug-0x5:
0:
1:
3:
4:
60
pusha
jmp
eb02
cc
5anti_debug
%ebx,%ebx
int3
cc
int3
00000005anti_debug:
5:
31db
xor
7:
9:
a:
c:
d:
6a1a
58
push
pop
int
$0x1a
%eax
cd80
40
$0x80
inc
%eax
7409
je
18decode
0000000fmainloop:
f:
68aaaa0000
push
pop
$0xaaaa
%edi
14:
15:
17:
5f
6a70
59
push
pop
$0x70
%ecx
00000018decode:
18:
1b:
1e:
21:
22:
8b040f
83f041
89040f
49
mov
xor
(%edi,%ecx,1),%eax
$0x41,%eax
mov
dec
jne
%eax,(%edi,%ecx,1)
%ecx
75f4
18decode
00000024exit:
24:
25:
2a:
61
popa
push
ret
68aaaa0000
c3
$0xaaaa
[root@localhostsc]#python-c'print0x2a'


shellcode实现了anti-ptrace、垃圾数据和xor解密的功能,有别样需求的可以自己编写。

通过计算可以看到,我们的shellcode只有43字节哦(ret只占一字节)。简单解释下shellcode,开始pusha和结尾的popa用于保护寄存器的值,第3、4的int3需要改为其他垃圾数据,这样就能稍微欺骗下流式反汇编工具了。第5~10行是调用ptrace,根据manpage,只要ebx是PTRACE_TRACEME(0)就能忽略后三个参数了。

Ptrace成功后到第f-22的解密段,注意,不成功时我选择直接跳到一个错误的地址,这样会引起段错误。问题是,shellcode应该插在哪里呢?在翻manpage的时候,我发现了如图4所示的信息。

C4.png

根据manpage的解释,NOTE_SEGMENT只是用来保持附加信息的,也就是kernel在加载elf文件时并不会用到里面的数据。再“readelf–l”看看noteSEGMENT,长度是0x44=68字节,完全能放下我们的shellcode。如图5所示。

C5.png

从图6可以看出的确没什么重要数据,只是用来标识elf文件是由gnutools生成的。好,可以向NOTESEGMENT加入shellcode了。

C6.png

现在万事具备,只欠代码了,片段如图7所示。

C7.png

第176~184行是用于备份原来程序入口点到entry中,因为后面需要把入口点指向NOTESEGMENT,代码接下来的工作就是在PROGRAMHEAD数组中寻找NOTESEGMENT了。

其实类似于寻找SECTIONHEAD,NOTESEGMENT存在于SEGMENTHEAD中。参考elf.h,只要一个循环+判断语句就可以实现(代码188~205行)。如果循环后寻找失败就退出程序(代码207~211行)。我验证了大部分的/bin/下的文件,全都含有NOTESEGMENT。

如图8所示,代码217行调用函数padding_sc,把原程序入口点替换为shellcode中的0xAAAA了。还记得shellcode中的pushret和xor解密的数据吗?接着222~224行是把修改好的shellcode复制到NOTESEGMENT中,之后就如代码中注释所言,把NOTESEGMENT和LOADSEGMENT权限值改为0x7,即rwx,修改入口点到NOTESEGMENT(234行)。最后把原入口点的内容xor一下,对应shellcode中的decode,同样是为了反流式调试程序。要记得的是,必须把LOADSEGMENT也赋予写权限,因为程序执行时shellcode要修改其中的内容。

C8.png

历尽千辛万苦,我们看看效果吧,如图9所示,能正常执行,Strace、objdump、gdb都不能正常执行程序,ida连原入口点的代码都逆不出来了,如图10所示。

C9.png

C10.png

以上的代码,大致思路都明确了,能过动态调试,可还是很难瞒过ida,有经验的逆向者只要花点时间写xoe编码的idc脚本,再把程序静态patching下就会被解密掉。至于深层次的利用,既然我们可以对静态二进制程序插入shellcode,那我们可以写加用户,生成suid文件等shellcode来帮助我们欺骗Root执行提权。而对于反调试方面,还能继续优化shellcode,就能实现类似buruneye那样的对程序加密码等功能,同时还应该对shellcode加密,如数字字母化shellcode。

(完)