首页
社区
课程
招聘
[翻译][也基础][也简单][译]击败调试器
2005-5-6 08:11 9423

[翻译][也基础][也简单][译]击败调试器

2005-5-6 08:11
9423
[出处]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)

<0x20 );

            // 第五位发生翻转.
            // 这也就转换了拉丁字符的大小写.
            ((char *) arg)

^=0x20;

            // 指向将被处理的下一个字节.
            p++;
      }
}

int main(int argc, char* argv[])
{
      char name[100];
      //存储用户名的缓存

      char buff[100];
      //存储密码的缓存

      //用户名缓存中填充0.
      // (有的编译器会这样做,但并不是所有的.)
      memset (&name[0], 0, 100);

      //子程序My在另外一个线程里执行.
      _beginthread(&My, NULL, (void *) &name[0]);

      //需要输入用户名.
      printf("Enter name:"); fgets(&name[0], 66, stdin);

      //需要输入密码.
      //注意: 用户输入密码的过程中, 第二线程有充分的时间
      // 将用户名中所有字符转换大小写.这很隐蔽,对程序的分析也不会
      // 跟踪至此.在一个不善于反应程序各部分间的影响的调试器下
      // 调试时尤其如此.

      printf("Enter password:"); fgets(&buff[0], 66, stdin);

      // 用户名和密码通参考值作比较.
      //  
      if (! (strcmp(&buff[0], "password\n")
      //注意: 因为输入的用户名被改变了,所以
      //用strcmp(&name[0], "Osix\n"),
      //而不是strcmp(&name[0], "OSIX\n")来比较它.
      // (这乍一看时很不明显.)

      || strcmp(&name[0], "OSIX\n")))
      // 正确的用户名和密码
      printf("USER OK\n");
             else
      // 错误:错误的用户名或密码
      printf("Wrong user or password!\n");

      return 0;
}

看一下列表中的程序,我们关心是程序看来要接受OSIX:password的, 但实际的答案却
是Osix:password.我们来研究的稍微仔细些.用户输入用户名后,第二个线程就处理存有用
户名的缓存,并且转换了大小写(除了第一个字符).所以就知道了,当一个线程被调试的
时候,所有其他线程都在各自独立的工作着.而这些其他线程也可以随意的干涉正在被调
试的线程的工作(比如说更改它的代码).啊...到现在这才开始有些可能了!

这里又有一些东西得考虑了.因为已经知道线程可以被控制,但是如果保护开发者放置了
多于四个的断点,调试寄存器就不再可信了,我们就不得不使用0xCC字节,而这样做的话,由上文
可知是很容易被检测到的.

若是被调试的程序里使用了结构化异常处理(structural exception handling ,SEH ),调试器就会把
情况搞得更糟,著名的Softice也不例外.那样正在被调试的指令要么会"击败"调试器,将自己
由调试器的控制中释放出来,要么将控制权交给库异常,而它只不过是在调用几个代码足以淹没
破解者的服务程序之后将控制权交给程序处理.

然而,与以前的Softice版本相比,这种情况有了进步.以前,Softice严格控制某些中断,比如它
不允许程序独立执行除零的操作.

让我们再看看另外一些代码吧?!当下面的例子在Softice 包括4.05及以下的任何版本下运行,调试
器到达行int c=c/ (a-b)时都将会突然中止执行,失去对程序的控制权.但是这种情况可以得到
改正,只要提前在__except块的第一条指令处设置断点即可.那么问题是如何如不用深入源代
码就找到这个块的位置呢?而黑客是不可能有源代码的.

列表3. 使用结构化异常处理(structural exception handling)的一个例子
int main(int argc, char* argv[])
{

// 受保护的块
__try{
         int a=1;
         int b=1;
         int c=c/ (a-b);
         // 这里是做执行除零的操作.
         // 使用了多条指令是因为
         // 大部分编译器遇到形如
         // int a=a/0的表达时都会返回错误;
         // 当SoftIce执行下面的指令时, 它就失去了
         // 对被调试程序的控制权. 它落到了一些
         // 从不会获得控制权却可能产生误导的代码上.
         // 如果变量a和b被赋予的是某些函数的返回值,
         // 而不是立即数, 二者的相等的关系
         // 在程序被反汇编了之后
         // 就不那么明显了. 结果就是, 黑客可能会浪费时间
         // 去分析那些无用的代码 ,嘿嘿呵!
}

__except(EXCEPTION_EXECUTE_HANDLER)
{
         // 发生除数为零的异常时,这里的代码将会获得
         // 控制权 , 但是SoftIce意识不到这种情况。
         // 取而代之,要求手动在__except块的第一条指令
         // 处设置断点 .
         // 要确定块__except的地址, 黑客必须准确地指出
         // SEH支持是如何在某个特定的
         // 编译器中实现的.
}
}

对于破解者来说,他们处理这样的保护时必须深入的研究结构化异常在系统级别上和在调试器级别
上是如何进行的.对于一个业余的破解者来说工作量太大了,不是么?

因为SEH在不同的编译器中实现方法是不同的,这也就难怪SoftIce不支持它了.这对程序员来说是
好事,对破解者来说就太糟糕了!

所以前面的例子都是强烈对抗中断的,同时也容易实现.它在从Windows95开始的Windows家族的所
有操作系统下都能很好的起作用.

:::躲过断点:::

在系统函数上下断点是破解者拥有的强大武器.假设某个保护试图打开key文件.在Windows下,唯一
一个在文献中记录着的做法就是调用CreateFile函数(实际上,CreateFileA和CreateFileW是分别对应文件
名的ASCII和UNICODE形式而已). 所有其他由早期的Windows版本继承而来的函数都只不过是封装
了CreateFile而已.

知道了这些,黑客就会在函数开始的起始地址设置断点(这个地址黑客是知道的),从而迅速的定位调

用了这个函数的保护代码.

尽管如此,并不是所有的黑客都知道打开文件可以有另外的方法:通过调用由NTDLL.DLL输出的

ZwCreateFile (或者NtCreateFile)函数,或者通过调用INT 0x2Eh中断直接定位kernel.这不仅仅对CreateFile
成立,kernel中的所有函数都是这样的.还有有用一点是做到这些不需要什么权限.这样的调用甚至可

以是源于程序代码的.

这些小把戏不会阻止破解者太长时间.这很糟糕.但是将这个小定时炸弹放在那里是值得的(在块

__try中调用INT 0x2E中断).

现在,怎么处理USER和GDI模块中那些用来读取用户输入的注册信息(按惯例,是一个序列号或者密
码)的函数(比如,GetWindowsText)呢?因为我们知道这些函数都是以指令PUSH EBP\MOV EBP, ESP开
始的,指令代码可以另外的执行它:即不将控制权直接的传到函数开始,而是开始处三个字节后(因为
PUSH EBP会改变堆栈,传递控制权时必须使用JMP而不是CALL.)这样设置在函数开始出的断点就不
会产生任何作用.这样的技巧可能会使一个熟练的黑客都暂时的误入歧途.

断点可被分为两类:开发者设置在程序内的断点和调试器本身设置的动态断点.第一类很清楚:要将
控制权在某个必要的地方转移给调试器就必须使用__asm{int 0x3}.

在程序的任意位置设置的断点就有些复杂了.调试器会保存指定位置处内存地址的当前值,然后再
在那里写入代码0xCC.退出调试中断之前调试器会将全部位置都恢复原样,并修改堆栈中存储的IP
的值使其指向已恢复的指令的开始处.(否则,它就会指向它的中间.)

图1:进入中断子程序时堆栈的内容
--------
8086处理器的断点机制的缺点是什么呢?最让人感觉不好的就是调试器设置断点时必须直接修改代
码.

SoftIce中使用步过(F10键)方式跟踪程序时只是隐式的将断点设在下一条指令的前面,这样就会破
坏保护代码中用到的校验和.

最简单的解决方法是一条指令一条指令的跟踪--当然,这是在说笑;这种情况必须设置硬件断点.碰到

类似的情况时,我们的前辈们(1980年代的黑客)通常都是手动将程序解密,再结合NOP指令将解密过

程替换掉.这样,调试程序时就不会再出现问题了(如果保护中没有使用其它陷阱的话).在IDA出现以

前,解密程序都是作为一个独立的程序在C(Pascal,BASIC)中编制的.现在这项工作已经变得简单了,因

为解密在反汇编器内部实现已经成为了可能.

解密实际上就是用IDA-C语言对解密程序重新编制.这种情况下,从BeginCode到EndCode的校验和必
须计算出来,算上每个字节的和再用校验和的低字节载入下一个字符.获得的值用来使用异或操作
处理s0字符串.所有这些都可以使用下面的代码实现(假设反汇编代码中已经存在了适当的标签):

列表 239. 用IDA-C重新实现解密功能

auto a; auto p; auto crc; auto ch;
for (p=LocByName("s0"); Byte(p) !=0; p++)
{
      crc = 0;

      for(a=LocByName("BeginCode"); a<(LocByName("EndCode")); a++)
      {
            ch = Byte(a);
            // 因为IDA不支持byte和word类型
            // (是个遗憾), 必须让它参与位运算.
            //  CRC低字节被清除,
            //然后CH的内容拷贝到那里.

            crc = crc & 0xFFFFFF00;
            crc = crc | ch;
            crc=crc+crc;
      }
      //从CRC中取出高字节.
      crc = crc & 0xFFFF;
      crc = crc / 0x100;

      // 字符串的下一个字节被解密.
      PatchByte(p, Byte(p) ^ crc);
}

如果没有IDA,在HIEW中也可以实现这个过程,如下:

NoTrace.exe ?W PE 00001040 a32 <Editor> 28672 ? Hiew 6.04 (c)SEN
00401003: 83EC18 sub esp, 018 ;"$"
00401006: 53 push ebx
00401007: 56 push esi
00401008: 57 push edi
00401009: B905000000 000005
0040100E: BE30604000 [Byte/Forward ] 406030 ;" @'0"
00401013: 8D7DE8 1>mov bl, al | AX=0061 p][-0018]
00401016: F3A5 2 add ebx, ebx | BX=44C2
00401018: A4 3 | CX=0000
run from here: 4 | DX=0000
00401019: 6660 5 | SI=0000 [0FFFFFFE8]
0040101B: 8D9DE8FFFF 6 | DI=0000
00401021: 33C0
.0040101B: 8D9DE8FFFFFF
.00401021: 33C0 xor eax, eax
.00401023: 8D3519104000 lea esi, [000401019]; < BeginCode
.00401029: 8D0D40104000 lea ecx, [000401040]; < EndCode
.0040102F: 2BCE sub ecx, esi
.00401031: AC lodsb
00401032: 03C0 add eax, eax
00401034: E2FB loop 000001031
00401036: 3023 xor [ebx], ah
00401038: 43 inc ebx
00401039: 803B00 cmp b, [ebx], 000 ;" "
0040103C: 75E3 jne 000001021
0040103E: 6661 popa
to here:
00401040: 90 nop
00401041: 8D45E8 lea eax, [ebp][-0018]
00401044: 50 push eax
00401045: E80C000000 call 000001056
0040104A: 83C404 add esp, 004
1Help 2Size 3Direct 4Clear 5ClrReg 6 7Exit 8 9Store 10Load

第一步,计算校验和.文件载入HIEW后,要找到所需的代码片断.然后按两次<Enter>键将其转换到汇编

模式,在按下组合键<F8>+<F5>跳到入口点,这就找到了开始代码主过程.下一步,按<F3>键进入文
件编辑状态,用组合键<Ctrl>+<F7>调出解密编辑窗口(这个组合键随着版本的不同而不同).然后输入
下面的代码:

mov bl, al
add ebx, ebx

可以用其他的寄存器代替EBX,但是不能用EAX,因为HIEW每次读取下一个字节前都会将EAX清除.
现在鼠标到了0x401019这一行,按<F7>键运行解密过程到0x401040这一行(但不包括这行).如果这些都
作的没错的话,高字节BX中的值0x44正是校验和.

第二步里,找到的加密行(它的偏移,载入了ESI),然后与0x44异或.(按<F3> 键转到编辑模式,按
<Ctrl>+<F8> 键指定加密用的key, 0x44, 再按 <F8>键按行执行解密程序.)

NoTrace.exe ?W PE 00006040 <Editor> 28672 ? Hiew 6.04 (c)SEN
00006030: 48 65 6C 6C-6F 2C 20 46-72 65 65 20-57 6F 72 6C Hello, Free World
00006040: 20 65 49 4E-00 00 00 00-7A 1B 40 00-01 00 00 00 eIN z$@ $

现在剩下的就是将行0x401036处的XOR用NOP指令换掉,否则程序运行时,XOR会破坏解密代码(会再
加密一次),程序也就不再工作了.

去除了这个保护后,再调试这个程序时,在需要的范围内就不会产生严重的后果了.

好了,到此为止了.又一篇关于汇编和调试的文章,很长,但希望还算有用.

再见!

Trope

阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

收藏
点赞7
打赏
分享
最新回复 (11)
雪    币: 85498
活跃值: (198820)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
linhanshi 2005-5-6 08:41
2
0
辛苦了
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
nbw 24 2005-5-6 10:03
3
0
支持!
以前看这个看了一半后来资料给搞丢了,这次又看到了,嘿嘿
雪    币: 100
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zmh 2005-5-6 12:33
4
0
这个可是好东西哦 我早想见识一下这方面的知识了啊
雪    币: 0
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
caonima 2005-5-7 13:20
5
0
果然不错啊
雪    币: 232
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
warcraft 2005-5-10 20:20
6
0
辛苦了
雪    币: 255
活跃值: (175)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
lee 3 2005-6-6 17:22
7
0
主要的复杂情况是软件(Windows?)需要更好的调试器.

应该译为主要因素是软件(Windows?)需要更好的调试器

我决定帮你校正一遍。。。呵呵~~~~~~~~~~~~~~~~~~~~~~~~`
雪    币: 342
活跃值: (318)
能力值: ( LV12,RANK:740 )
在线值:
发帖
回帖
粉丝
aalloverred 18 2005-6-6 20:54
8
0
最初由 lee 发布
主要的复杂情况是软件(Windows?)需要更好的调试器.

应该译为主要因素是软件(Windows?)需要更好的调试器


........


已改正,多谢多谢!!!   
雪    币: 255
活跃值: (175)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
lee 3 2005-6-6 21:24
9
0
我已经看完了。。

我是看一句英文,然后看一句你翻译的中文!!!

你翻译的不错!!!
雪    币: 328
活跃值: (925)
能力值: ( LV9,RANK:1010 )
在线值:
发帖
回帖
粉丝
liyangsj 25 2005-6-10 16:08
10
0
不错,支持一下!
雪    币: 255
活跃值: (175)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
lee 3 2005-6-15 16:55
11
0
才发现《黑客反汇编揭密》上面有啊。。。。。
雪    币: 409
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
king2004 2005-6-15 23:41
12
0
下载了很多资料都还没看呢!要找个时间好好看点东西了!
游客
登录 | 注册 方可回帖
返回