最近迷上了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所示。
接着第二类strace和ltrace则都是利用ptrace来跟踪子程序的,anti-ptrace估计有经验的都知道,只要在进程中调用ptrace跟踪自己(PTRACE_TRACEME),如果返回-1即证明被跟踪了,以这个条件可以判断是否被调试,同时调用过PTRACE_TRACEME后,gdb、strace
就不能再以进程调式(gdb–-pid,strace–p)等方式加载已运行的进程了。
最麻烦的要数第三类静态调试了。ida这类静态反汇编令我们的反调试代码赤裸裸地暴露,但由于是流式字节反汇编,因此我们能用xor编码和插入垃圾指令来混淆欺骗,但我们也知道,这样做只能提高反汇编的成本而已。
既然反调试的方法已经清楚,那么接下来就是如何实现了。对付第一类,我们仅需编辑elf文件就能实现,如图2所示。
代码很简单吧?函数传入参数memap,是把二进制文件mmap到内存的指针。这里用到一些elf数据结构,下面简单介绍,不熟悉的可以查下manpage和看看/usr/include/elf.h。
一个elf文件的简略组成如图3所示,其中的sectionhead、programhead位置表述可能并不准确,但通过上图可以知道,我们能通过elf_head查找到sectionhead的位置(代码132行,通过elfhead中的e_shoff+文件基址),别忘记我们的目的仅仅是破坏它(代码139-142行)。直接向sectionhead中填充随机数,这样binutils系列工具就拒绝加载文件了。
第二类和第三类的反调试,我们需要将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所示的信息。
根据manpage的解释,NOTE_SEGMENT只是用来保持附加信息的,也就是kernel在加载elf文件时并不会用到里面的数据。再“readelf–l”看看noteSEGMENT,长度是0x44=68字节,完全能放下我们的shellcode。如图5所示。
从图6可以看出的确没什么重要数据,只是用来标识elf文件是由gnutools生成的。好,可以向NOTESEGMENT加入shellcode了。
现在万事具备,只欠代码了,片段如图7所示。
第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要修改其中的内容。
历尽千辛万苦,我们看看效果吧,如图9所示,能正常执行,Strace、objdump、gdb都不能正常执行程序,ida连原入口点的代码都逆不出来了,如图10所示。
以上的代码,大致思路都明确了,能过动态调试,可还是很难瞒过ida,有经验的逆向者只要花点时间写xoe编码的idc脚本,再把程序静态patching下就会被解密掉。至于深层次的利用,既然我们可以对静态二进制程序插入shellcode,那我们可以写加用户,生成suid文件等shellcode来帮助我们欺骗Root执行提权。而对于反调试方面,还能继续优化shellcode,就能实现类似buruneye那样的对程序加密码等功能,同时还应该对shellcode加密,如数字字母化shellcode。
(完)