[出处]http://www.osix.net/modules/article/?id=594
[说明]第一篇是胡乱的译,胡乱的贴上去的,竟......啥也不说了,
感谢kanxue老大的lift和linhanshi大哥的encourage.
为此又贴一篇,
同样简单,但因为我是个新手,译得肯定有很多漏洞和争议,因此严重希望博得大伙窃笑的同时,
别忘了批我, 帮助我提高:).
[译者]aalloverred
[译文]
击败调试器
这次我们看看如何使别人跟踪你的代码变得困难,将要学习的是调试器相关知识,它们是如何工作
的,及你的程序如何检测到它们.
-----------------------
躲避调试器-保护你的程序
-----------------------
作者 Giovanni Tropeano 于11/2004
为 OSIX 而作
<<文章目录<<
...前言
...调试器简史
...调试器是如何工作的
...跟踪,及如何战胜它
...躲过断点
:::前言:::
以下面这句话作为开始--
引用:
/你不可能也不会编出无法被破解的程序./
但这并不是说不能将此变得非常非常困难.
闲言少叙,书归正传.本文中,我们将要学习一些反调试技巧,你可以将它们用于你自己的程序中.
我为OSIX写的第一篇文章中讨论了自修改代码,并试图摆脱调试器.现在要讨论的是击败(或至少是
其变得困难)调试器.
:::调试器简史:::
以前有一个Debug.com,它是第一个Windows调试器.它属于标准MS-DOS包中的一部分,而今天除了用
于学习汇编很少有其他用途了.勉强适合破解用的调试器最早是随80286出现的,但它们不能造成什
么实际的伤害.
但是,80386出现后情况有了变化,主要的因素是软件(Windows?)需要更好的调试器.就是这个时
候,随着一些调试器的不断强大,它们开始成为程序员的威胁.80年代末期,Softice出现在舞台上,给受
保护程序及它们的开发者带来不少的麻烦.从那时候起,Softice就作为黑客调试工具传了下来.但最近
Olly在年轻一代破解者中越来越常用.
现在由于分析软件的可能性,与黑客对抗就成了无用的挣扎.而与此同时还有威胁来自于那些原本是
新手,但读过了许多"如何破解程序"问题集后(幸运的是每个人都可以得到这些东西),现在正在寻找
一些东西要来练练手的人.我们想要摆脱的就是这些在寻找新的挑战的新手.
:::调试器如何工作:::
不知道调试器如何工作就要对抗它会是空谈.因此理解调试器究竟是如何工作的很重要.
所有的调试器都无非属于下面两类:
.使用处理器的调试能力
.独立的模仿处理器,监视被测试程序的运行
迄今还没有高水平的可以不被代码检测到或者避过代码的检测的模仿型的调试器,而且现在看来在
未来的一段时间内也不会出现.
关键是,开发这样一个仿真器值得么?因为我们已经能够步入代码,控制某个地址的指令的执行,监视
某个内存地址(或输入输出端口)的指向,改变信号任务,等等.我认为不值得.不管怎样,继续下去...
好,下面将要进行深入点的研究,试着跟上.
调试器会检查标志寄存器的陷阱位是否为1.如果是,就在每条指令后自动生成一个INT 1 调试异常,
并且将控制权交给调试器.由此可知,代码可以通过分析标志寄存器来检测跟踪(调试).所以调试器为
了不被发现,它必须识别出读取标志寄存器的指令,模仿其运行,并且为陷阱标志返回零.说起来容易
做起来难,是吧?
有四个调试寄存器:
1. DR0
2. DR1
3. DR2
4. DR3
它们存储了四个检测点的线性地址.当然还有一个寄存器保存了每个点的条件,它就是DR7.当任何
一个条件为真时,处理器就会抛出INT1异常,控制权也就交给了调试器,有四个条件:
1.一条指令被执行
2.某个内存地址的内容被改变了
3.某个内存地址被读取或改变,但没有被执行
4.一个输入输出端口被引用
现在要讨论的是软件断点.软件断点是唯一只有用处理器的完全仿真器才能被隐藏的东西.如果将一
个字节的代码--0xCC插入到一条指令前,再试图执行它就会引发INT 0x3异常.为了发现是否至少有一
个点被设置了断点,程序仅需计算其校验和就可以了.为了做到这一点,可以使用MOV, MOVS, LODS,
POP, CMP, CMPS或其他任何指令;没有任何调试器能够跟踪模仿其中的任何一个(据我所知!).
:::跟踪,以及如何战胜它:::
因为完全不可见的调试器还仅仅是一种"可能",大部分还都是可以被检测到的.
大部分调试器使用一字节的0xCC代码.
让我们看一个简单的保护方法:
列表1.C++中一种简单的保护方案
int main(int argc, char* argv[])
{
// 加密后的字符串 "Hello, Free World!"
char s0[]="\x0C\x21\x28\x28\x2B\x68\x64\x02\x36\
\x21\x21\x64\x13\x2B\x36\x28\x20\x65\x49\x4E";
__asm
{
BeginCode: ; 正在被调试的
; 代码的开始
pusha ; 所有的通用
; 寄存器都被保护起来.
lea ebx, s0 ; ebx=&s0[0]
GetNextChar: ; 开始
xor eax, eax ; eax = 0;
lea esi, BeginCode ; esi = &BeginCode
lea ecx, EndCode ; 代码的长度
sub ecx, esi ; "正在被调试"也被计算在内
HarvestCRC: ; 计算
lodsb ; 下一字节载入到al.
Add eax, eax ; 计算校验和.
loop HarvestCRC ; 直到(--cx>0)
xor [ebx], ah ; 下一个字符被解密.
Inc ebx ; 指向下个字符的指针
cmp [ebx], 0 ; 直到字符串结束
jnz GetNextChar ; 继续解密
popa ; 恢复所有的寄存器.
EndCode: ; 被调试代码的结尾
nop ; 这里的断点是安全的.
}
printf(I s0); ; 显示字符串.
return 0;
}
仔细看看这段代码(注意注释).程序启动后,句子"Hello,Free World!"将会出现在屏幕上.但是当它运行
在调试器下时,哪怕只有一个断点设置在了BeginCode和EndCode之间,一些像"Jgnnm."Dpgg"Umpnf#0"
这样毫无意义的垃圾信息就会出现在屏幕上.不赖,是么?现在才有些到了点子上了.你甚至可以把计
算校验和的过程放在另一个线程中来加强这种保护.
说到线程,它需要对事物使用专门的方法.对我们这些凡人来说,要实现同时运行在几个地方的程序
有点难.而常用的调试器都有一个弱点,即它们是分别调试每个进程的,从来不同时调试.下面的例
子演示了如何将此用于保护.
表2 . 分别地调试线程的弱点
// 这个函数将要在另外一个线程内执行.
// 它的作用是神不知鬼不觉地改变用户名字符串中字符的大小写 .
void My(void *arg)
{
int p=1;
// 这个指针指向正在加密的字节.
// 注意加密并没有从第一个字节开始执行,
//因为那样的话,断点就能设在缓存的开始
//而躲过检测.
//如果碰到的不是换行符'\n'就执行.
while ( ((char *) arg)
!='\n')
{
// 下一个字符是否未被初始化? 同时也是在等待.
while( ((char *) arg)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)