当前位置: 代码迷 >> 综合 >> Soft-ICE实例起步
  详细解决方案

Soft-ICE实例起步

热度:54   发布时间:2024-01-03 21:13:56.0
Soft-ICE实例起步
 
作者:Jiang Hong 文章来源:csdn       更新时间:2004-6-25
Soft-ICE实例起步(Windows版)

● 文 / Jiang Hong

--------------------------------------------------------------------------------

  为了以后说话方便, 这里把 Soft-ICE 的一些简单使用方法说一下, 以免不通 E 文的同志们找不到中文的 Soft-ICE 说明而抓瞎.

   Soft-ICE 由三部分 (以后说的 Soft-ICE, 如果不加特殊说明, 均指 Soft-ICE for Windows 95 的 2.0 版本以上) 组成: WINICE.EXE, WLDR.EXE (在 3.0 中这个文件叫做 LOADER32.EXE) 和显示驱动程序 SIWVID.386.

  另外, Soft-ICE 在启动的时候要装入一些 DLL/EXE 的函数名信息, 你必须手工指定这些 DLL, 按照:

  exp=d:/path/name.ext

  的格式写在 WINICE.DAT 文件里. 本文附录里面有俺用的 WINICE.DAT, 你可以直接用起来, 省得自己写那么多行了. 注意, 一定要把下面几行包括进去, 否则 WINICE 可能什么东东也拦不到:

  exp=c:/win95/system/kernel32.dll
  exp=c:/win95/system/user32.dll
  exp=c:/win95/system/gdi32.dll
  exp=c:/win95/system/comctl32.dll


   一般我们使用 WLDR (以后把 LOADER32 也称为 WLDR) 来装入一个 EXE 文件或者一个 DLL 文件, 大多数的时候, 我们也可以直接执行 EXE 文件, 通过跟踪它的各种消息来找到它. 启动 WINICE 的热键是 Ctrl+D. 先介绍常规的办法:

   启动 WLDR, 然后选择你要跟的程序, 单击 Load 按钮, 屏幕上一阵乱闪后就进入了文本模式, 这就是 Soft-ICE 的跟踪界面, 虽然简单了点, 但是很友好. 可以像 DOSKEY 那样用光标上下键重复上次输入的内容, 也可以输入上次输入内容的一部分, 然后按光标上键, 上次输入内容就完整地贴了出来.

  一般情况下, 如果装入的一个 NE 程序, WINICE 会直接找到它的入口点, 并且把当前的光标定在 EXE 的头一条指令上; 如果是 PE 程序, WINICE 会停在一个 INVALID 区, 按下 F10 后可以到 EXE 头部.

  比较重要的功能键:

  (1) F10 : 单步执行; CALL, INT 会被跳过;
  (2) F8 : 单步执行; CALL, INT 会被切入;
  (3) F4 : 查看程序画面;
  (4) F11 : 对于 CALL 形式的子程序, 直接执行完毕, 在 RET(F) 之后
       回到 CALL 的下一条指令;


  比较重要的几条命令是:

  (1) G: 执行程序, 后面如果加地址, 则执行到该地址为止, 比如:

    2400:0480    MOV   AH,30
    2400:0482    INT   21
    2400:0484    CMP   AL,09
    2400:0486    JZ   04F9
    2400:0488    MOV   AH,4C
    2400:048A    INT   21


  假设当前 CS:IP (EIP) 为 2400:0480, 如果我们直接执行 G, 就会一 G 到底, 直接回到命令行, 原因在于 DOS 9.00 还没出, 程序直接执行 DOS TERMINATE 功能了.

   如果你把 DOS 版本号先改成 9.0, 然后执行 G 4F9, WINICE 会跑到 2400:04F9 处然后停下; 但是如果你不改版本号也这么来一把, 就也会一 G 到底, 回到命令行了, 因为 2400:04F9 在这种情况下永远不会被执行到.

  (2) P: 单步执行程序; 只执行 P 时, 相当于按下 F10 键; 如果后面加入 P RET 参数, 会执行到最近的一条 RET/RETF 处, 注意, IRET 会被忽略, 所以要小心;

  (3) T: 相当于按下 F8;

  (4) BPINT: 设置中断断点; 格式为 "BPINT 中断号 [条件]"; 在 WINICE 2.0 里面, 条件只能是下面三种之一:

    bpint xx ah=ab
    bpint xx al=cd
    bpint xx ax=abcd


  而在 WINICE 3.0 里面, 条件的格式丰富得多:

    bpint xx if ah==ab
    bpint xx if al==cd
    bpint xx if ax==abcd
    bpint xx if dx->4==abcd (当 DS:DX+4 处的值为 0xabcd 的时候)


  还有一些, 不是很常用, 等到用的时候再说吧.       ;)

   (5) BPX: 设置执行地址断点; 格式为 "BPX 地址", 还以上面的例子来说, 假如我们执行 bpx 486, 然后来一把 G 4F9 的话就不会一 G 到底了, 因为我们在那个判断处设了断点, WINICE 会执行到 2400:0486 然后停下;

   BPX 的第二种用法是我们这个教程的关键, 格式为 "BPX 函数名". 这个函数名可以是任意一个 Windows API 函数, 虚拟机指令, DLL 的引出函数等等. 功能强劲. 比如说, 我们先启动 Notepad, 然后在里面随便敲些东东, 然后按 Ctrl+D, 执行:

  :bpx messageboxa     (不用区分大小写)
  :g


  然后关闭 Notepad, 这时 WINICE 会被激活, 原因是断点条件已经符合了. Notepad 此时将弹出一个消息框提醒你存盘, 这就进入了 MessageBox 函数, 后面加一个 A 是由于我们现在在 Windows 95 里面, 函数是区分字符集的. A 表示 ANSI, W 表示 Wide, 即 Unicode (Wide character-set).

  (6) BPM: 设置内存访问断点; 格式为 "BPM 地址"; 比如: BPM F000:E6F9 会把断点设在内存中存放 BIOS 更新版本号的内存位置上, 只要有程序访问这个地址, WINICE 就会激活. 另外, 还可以单独设定对地址的访问条件: 读(R)/写(W)/执行(X). 只需要在后面把对应的字母加上就可以了. 以那段 "一 G 到底" 为例:

  :bpm 2400:0486 x
  :g


  和 bpx 486 的效果完全一样.

  (7) BMSG: 跟踪 Windows 消息; 格式为 "BMSG 消息名"; 比如我们执行 Notepad, 然后 Ctrl+D 激活 WINICE, 输入:

  :bmsg wm_char
  :g


  然后回到 Notepad 中, 随便按一个键, WINICE 就激活了; 原因在于我们在按键消息上设置了断点.

  (8) BL: 列出所有的断点; 格式为 "BL"; 它会把所有断点按从 0 开始的编号列出;

  (9) BC: 清除断点; 格式为 "BC 断点编号", 这个编号就是 BL 列出的那个; 还有一种格式为 "BC *", 作用是清除所有的断点.

  (10) RFL: 改变标志字; 格式为 "RFL 标志位"; 比如当前 Z 标志位 (零位) 为置位状态, 执行 "rfl z" 之后会被清楚; 如果 C 标志位为清除状态, 那么 "rfl c" 将使之置位; 比如:

  :g 486
  :rfl z
  :g 4f9


  这次就真可以 G 到 4F9 处了.

  (11) A: 进入 WINICE 小汇编状态; 格式为 "A 地址"; 也可以不加地址值, 直接在当前 CS:IP 处汇编; 举例:

  :g 486
  :a 2400:0486
  2400:0486 jnz 4f9
  2400:0488 (按下 ENTER 键结束汇编)
  :g 4f9


  也可以 G 到 4F9 处.

  (12) E: 进入 WINICE 内存修改状态; 格式为 "E 地址"; 也可以不加地址值, 直接在 DS:DX 处修改; 比如:

  :e 2400:0485
  2400:0485 - 09 74 XX XX XX XX XX XX XX XX XX XX XX XX XX XX
  (光标停在 09 下面, 我们输入 09 EB, 然后按 ENTER 结束修改)
  2400:0485 - 09 EB XX XX XX XX XX XX XX XX XX XX XX XX XX XX


  然后 "g 4f9" 就可以到 4F9 啦; 我们输入的 EB 是 JMP 的机器码.

  (13) U: 反汇编; 格式为 "U 地址", 也可以不加地址, 直接在当前 CS:IP (EIP) 往后反汇编 12 行 (行数由 CODE 栏的宽度而定).

  (14) R: 更改寄存器的值; 格式为 "R 寄存器名=值", 也可以不加值, WINICE 会让你在最上面的寄存器区直接修改, 用光标键可以在各个位之间移动. 比如:

  :g 484
  :r ax=0009
  :g 4f9


  就到了 4F9 处.

  基本的指令就是这 14 条, 如果需要, 俺随时补充就是了.




--- 如何拆解 ACDSee '95
  ACDSee '95 是一个速度快, 功能强, 格式多, BUG 少的 ...... 图形浏览程序 (不要想歪了哦! ;) . 我们以 ACDSee '95 1.0 正式版为例来看看此类程序如何拆解.

  首先是得到该程序, 该程序可以在:

  http://www.acdsys.com/download.htm(l)

  下获得. 也可以在国内的许多 BBS 上得到. 注意: 我们需要它的正式版, 否则许多地址值是错误的, 但是原理是一样的.

   首先用用这个程序, 当你看了 30 张左右的图片之后, 该程序就开始频繁提醒你注册了. 另外, 在程序的 About 对话框里面也有 Register 按钮让你注册. 我们来试试注册会是什么样子. 在 Register 对话框里面随便输入一个名字, 比如 "eGIS - pCE '97" 吧, 然后按 Tab 键跑到 Code 栏里面输入个数字, 比如 12345, 然后按回车键.

  啊! ACDSee '95 弹出一只 Message Box, 告诉你输入的号是无效的.   :..(

  让我们来想想看这种判断应该是如何工作的, 这不难猜:

  (1) 取得用户输入的名字;
  (2) 取得用户输入的注册号;
  (3) 使用某种算法检验;
  (4) 判断是否合法;
  (5) 如果合法就显示注册消息;
  (6) 如果非法就弹出一只 Message Box.


   好. 知道了这个就好办. 我们不难看到, 拆解的关键在于上面的第四步, 如何令这个程序认为你输入的号是正确的. 从那个 "一 G 到底" 程序里面我们应该学到些简单的拆解经验, 在那个例子里, 我们就是改动了一个 Z 标志来达到可以 G 到 4F9 的目的的, 这个方法是 "普适" 的, 也适于这个比较复杂的例子.

  现在我们开始拆. 首先我们猜想 ACDSEE 使用了 GetWindowTextA 函数来取得用户输入的信息, 来试试:

  :bpx getwindowtexta
  :g

  然后再按 ENTER 键, 结果 WINICE 没有被激活. 为什么呢? 原因是很简单的啦, 就是 ACDSEE 没用这个函数. 那用的是什么呢? 不难想到 GetDlgItemTextA 函数, 再跟一次看看:

  :bc *
  :bpx getdlgitemtexta
  :g

   按下回车键. Bingo! WINICE 被激活了! 好, 说明我们跟的函数是对的. 但是不要着急, 我们输入了两条信息: 名字和注册号, 因此这个函数会被执行两次, 所以我们继续 G 一下. 果然, WINICE 又拦到一个 GetDlgItemTextA 函数:

  USER32! GetDlgItemTextA
  --------------------------------------------------------------
  0137:BFF61657    MOV   CL,96
  0137:BFF61659    PUSH  EBP
  0137:BFF6165A    MOV   EBP,ESP
  0137:BFF6165C    PUSH  ECX
  0137:BFF6165D    SUB   ESP,3C
  0137:BFF61660    PUSH  WORD PTR [EBP+08]


  好了好了, 就 A 到这儿吧. 反正 WINICE 拦到了这个函数, 并且我们不关心它是如何得到那些数据的, 我们直接走完这个函数. 按下 F11 键就回到了调用它的地方:

  0137:004016FB    CALL  EDI  ; 我们就是从这里回来的    ;)
  0137:004016FD    XOR   DI,DI ; 当前 EIP
  0137:00401700    LEA   EBX,[ESP+18]
  0137:00401704    CMP   BYTE [ESP+18],0
  0137:00401709    JZ   401723
  0137:0040170B    MOVSX  EAX,BYTE PTR [EBX]


  我们看到 EIP 下面两条指令都和 [ESP+18] 有关. 究竟 [ESP+18] 处是什么东东呢? 我们来 DUMP 一下它:

  :d esp+18

  哈哈! 原来 [ESP+18] 处存的是你输入的姓名. 那么下面一条 CMP 的作用很明显了, 它是判断你是不是什么都没有输入, 我们既然输入了姓名, 就可以狠光明正大地 G 到地址 40170B 去也.

  接下来的一段是:

  0137:0040170E   PUSH  EAX
  0137:0040170F   CALL  0045A230
  0137:00401714   ADD   ESP,04
  0137:00401717   TEST  EAX,EAX
  0137:00401719   JZ   0040171D
  0137:0040171B   INC   DI
  0137:0040171D   INC   EBX
  0137:0040171E   CMP   BYTE PTE [EBX],0
  0137:00401721   JNZ   0040170B
  0137:00401723   CMP   DI,05
  0137:00401727   JL   00401841
  0137:0040172D   LEA   EAX,[ESP+38]    ; 注册号
  0137:00401731   LEA   EAX,[ESP+18]    ; 名字
  0137:00401735   PUSH  EAX
  0137:00401736   PUSH  ECX
  0137:00401737   PUSH  0047A128
  0137:0040173C   CALL  00403560
  0137:00401741   ADD   ESP,0C
  0137:00401744   CMP   EAX,01
  0137:00401747   SBB   EAX,EAX
  0137:00401749   INC   EAX
  0137:0040174A   TEST  EAX,EAX
  0137:0040174C   JL   00401841
  0137:00401752   LEA   EAX,[ESP+14]
  0137:00401756   LEA   ECX,[ESP+0C]
  0137:0040175A   PUSH  EAX
  0137:0040175B   PUSH  ECX

  A 了这么长, 我们来看看这段代码的用处吧. 首先我们看到在 40172D 这个地方用到了 [ESP+38] 和 [ESP+18], 经过 DUMP 知道, [ESP+38] 存放着我们输入的序列号, 那么上面的一个循环就可以直接跳过来了. 输入:

  :g 40173c

  然后有一个 CALL, 它前面的几个压栈函数压进去的是名字和序号, 然后经过一个子程序, 然后下面又有判断, 那么这个 CALL 极有可能是判断名字和序号是否符合的子程序, 我们来看看是不是这么回事. 先 G 到下面的判断处:

  :g 401744

   我们看到这时候 EAX 是 0, 然后继续走到 40174C, 发现它是要跳到 401841 执行程序. 我们继续走, 发现那个破框弹了出来. 之前的唯一一个分支就是在 40174C 这个地方了, 因此我们重复上面的步骤跟到 401744, 将 EAX 改成 1, 到 40174C 的时候继续执行下一条指令. 我们来 G 一把.

  Bingo!!! ACDSee '95 告诉我们它已经被注册了. 但是别得意, 看看它的标题栏吧, 还是 [unregistered] 的呢.     :(

  这是怎么回事呢? 不难想到, 还有别的判断. 是不是还要跟呢? 不用啦. 既然我们找到了判断子程序, 就可以直接改它了. 根据我们上次的经验, EAX 是 1 的时候表示名字和注册号是相符的, 那么我们把程序的返回值 EAX 改成恒为 1 就可以了.

  重新跟踪, 到 40173C 这个地方按下 F8 开始. 然后进入程序, 我们用 Ctrl+光标下键移动代码, 到 4035FE 处:

  0137:004035FE    TEST  EAX,EAX
  0137:00403600    MOV   EAX,00000001
  0137:00403605    JZ   00403609
  0137:00403607    XOR   EAX,EAX
  0137:00403609    POP   EDI
  0137:0040360A    POP   ESI
  0137:0040360B    POP   EBX
  0137:0040360C    ADD   ESP,4
  0137:00403612    RET

  如果你有一些反汇编 C++ 编译出的 EXE 的经验, 就可以看到这实际是:

  return ( fValid ? 1 : 0 ) ;

  的编译结果. 可以看到, 403605 是一个关键的地方, 就是这条该死的指令把可爱的 EAX 弄成了 0. 知道这个就好办啦. 我们把它跳过去:

  :a 403605
  0137:00403605 jmp 403609
  0137:00403607 (按回车结束汇编)


  然后重新执行一次. HOHOHO! 这次标题栏也变成注册版的样子啦. 拆解工作完毕!

  回到 DOS 下, 把我们的成果写回 EXE 里面就可以了. 刚才我们改的是把 JZ 改成了 JMP, 也就是说, 把

  B8 01 00 00 00 74 02 33 C0 5F 5E 5B
  改成了:
  B8 01 00 00 00 EB 02 33 C0 5F 5E 5B
  (黑话叫做 "74 改 EB"   :)


  为什么要把要查找的串弄得这么长呢? 直接把 "74 02" 替换成 "EB 02" 不就得了吗? 不是这样的. 在 EXE 里面可能有许多个 "74 02", 因为 JMP $+2 是一条太普通不过的指令了, 而一个 EXE 里面有多个

    MOV   EAX,00000001
    JNZ   @HERE+2
    XOR   EAX,EAX
    POP   EDI
    POP   ESI
    POP   EBX


  的机会就少得多了, 一般情况下只有一处, 这就是我们要改的一处.

  把特征串取得太长了也没什么实际意义, 反而会浪费地球上有限的纸资源, 地球只有一个, 是我们的家, 我们要爱护它 ......

  好, 用一个你喜爱的二进制文件编辑器改掉它. 然后重新执行 ACDSee '95.

  大功告成啦。

  经验一: 你现在知道了注册码的判断步骤;
  经验二: 你知道了程序可以用 GetDlgItemText 和 GetWindowText 取得
      在 Edit Box 中用户输入的数据;
  经验三: 你知道了程序判断注册号是否合法的地方可能不止一处, 但是一
      般都用同一个子程序来完成检验功能;
  经验四: 你知道了取得替换码的一些原则: 即 --- 不要太长也不要太短.
 
  相关解决方案