假设我用.c语言写了一个程序,终端用户启动的是.exe文件。在程序的执行过程中,有一个叫做CHECK的变量,它在程序执行的中间使用一些伪随机算法被动态分配。在某一点上,如果这个变量符合一些标准(比如说CHECK == 1580或者一些静态的预定义数字),程序就会在输出上做一些事情。我的问题是,一个对运行这个程序的系统有控制权的人,能不能在IF条件被设置之前,以这样的方式修改变量CHECK的地址空间,并将其匹配到数字'1580',并触发IF函数,即使该算法一开始就没有设置'1580'?
是的,用调试器很简单,比如gdb。在程序执行过程中,设置一个断点,就在 if
,运行程序直到触发断点,将变量设置为任何需要的值,删除断点,然后继续。你甚至可以让调试器完全跳过条件检查,直接跳入if块。你也可以在二进制代码中用一个 nop
. 盗版软件的 "裂缝 "基本上就是这样做的。
如果没有源代码和调试符号,这就变得有些困难,因为你必须弄清地址,但这只是拖延了不可避免的时间。只要能完全进入计算机,你就能以任何方式操纵任何程序。各种各样的保护方案都存在(主要是混淆),但它们只是增加了难度,而不是不可能。
为了进一步证明我的观点,这里有一个非常快速的例子:给定以下C代码。
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
int main () {
srand (time (NULL));
while (1) {
if (rand () == 1580) {
puts ("You got me!");
break;
}
}
}
编译时要进行优化,不使用符号,使其更难,假设是在x86_64的linux系统上。
gcc -O3 -flto -ffunction-sections -fdata-sections -Wl,--gc-sections -s test.c -o test
通常情况下,这个程序会运行几秒钟后退出。我们要让它立即退出。通过以下方法启动它 gdb
调试器。
$ gdb ./test
(gdb) starti
Starting program: /tmp/test
Program stopped.
0x00007ffff7dd6090 in _start () from /lib64/ld-linux-x86-64.so.2
获取内存范围的信息 我们感兴趣的是 .text
节。
(gdb) info files
Symbols from "/tmp/test".
Native process:
Using the running image of child process 12745.
While running this, GDB does not access memory from...
Local exec file:
`/tmp/test', file type elf64-x86-64.
Entry point: 0x555555554650
...
0x0000555555554610 - 0x00005555555547b2 is .text
...
所以实际的代码是从 0x0000555555554610
在记忆中。让我们来拆解一些吧。
(gdb) disas 0x0000555555554610,0x0000555555554700
Dump of assembler code from 0x555555554610 to 0x555555554700:
0x0000555555554610: xor %edi,%edi
0x0000555555554612: sub $0x8,%rsp
0x0000555555554616: callq 0x5555555545e0 <time@plt>
0x000055555555461b: mov %eax,%edi
0x000055555555461d: callq 0x5555555545d0 <srand@plt>
0x0000555555554622: nopl 0x0(%rax)
0x0000555555554626: nopw %cs:0x0(%rax,%rax,1)
0x0000555555554630: callq 0x5555555545f0 <rand@plt>
0x0000555555554635: cmp $0x62c,%eax
0x000055555555463a: jne 0x555555554630
0x000055555555463c: lea 0x17a(%rip),%rdi # 0x5555555547bd
0x0000555555554643: callq 0x5555555545c0 <puts@plt>
0x0000555555554648: xor %eax,%eax
0x000055555555464a: add $0x8,%rsp
0x000055555555464e: retq
...
这就是整个程序 这就是整个程序。cmp
指令是有趣的部分;在那里设置一个断点,让程序运行。
(gdb) break *(0x0000555555554635)
Breakpoint 1 at 0x555555554635
(gdb) c
Continuing.
Breakpoint 1, 0x0000555555554635 in ?? ()
从上面的汇编输出可以看出 0x62c
(即1580)是神奇的数字。将其写入寄存器中,覆盖 rand()
的返回值,然后继续程序。
(gdb) set $eax = 1580
(gdb) c
Continuing.
You got me!
[Inferior 1 (process 12745) exited normally]
(gdb)
程序会立即打印信息并退出。如果我们使用某种密码输入函数,而不是仅仅使用 rand()
我们可以做同样的事情来规避密码检查。我们也可以不在寄存器中设置值,而是键入 jump *0x000055555555463c
来跳入if块;这样一来,我们甚至不需要找到 "神奇 "的数字。