保护程序的栈内存

protecting stack memory of program

让我们假设我用 .c 编写程序并且最终用户正在启动 .exe 文件。在程序执行期间,有一个名为 CHECK 的变量,它使用一些伪随机算法在程序执行过程中动态分配。一方面,如果变量符合某些条件(比如 CHECK == 1580 或某个静态预定义数字),程序会在输出上执行某些操作。我的问题是,控制系统 运行 这个程序的人可以修改内存,在设置 IF 条件之前修改变量 CHECK 的地址 space 并将其与数字“1580”匹配并触发 IF 函数,即使算法没有首先设置“1580”?

是的,使用调试器很容易,例如数据库。在if、运行程序之前设置断点,直到断点触发,将变量设置为任意值,移除断点,然后继续。您甚至可以让调试器完全跳过条件检查,直接跳转到 if 块。您还可以用 nop 替换二进制代码中的检查。这基本上就是盗版软件 "cracks" 所做的。

如果没有源代码和调试符号,这会变得有些困难,因为您必须找出地址,但它只会延迟不可避免的事情。通过对计算机的完全访问,您可以随心所欲地操纵任何程序。存在各种保护方案(主要是混淆),但它们只会让事情变得更难,而不是不可能。

为了进一步证明我的观点,这里有一个非常简单的例子: 给定以下 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    [=14=]x8,%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    [=14=]x62c,%eax
   0x000055555555463a:  jne    0x555555554630
   0x000055555555463c:  lea    0x17a(%rip),%rdi        # 0x5555555547bd
   0x0000555555554643:  callq  0x5555555545c0 <puts@plt>
   0x0000555555554648:  xor    %eax,%eax
   0x000055555555464a:  add    [=14=]x8,%rsp
   0x000055555555464e:  retq   
   ...

这就是整个程序。 cmp 指令是有趣的部分;在那里设置一个断点,让程序 运行:

(gdb) break *(0x0000555555554635)
Breakpoint 1 at 0x555555554635
(gdb) c
Continuing.

Breakpoint 1, 0x0000555555554635 in ?? ()

从上面的汇编输出可以看出 0x62c(即 1580)是幻数。写入寄存器,覆盖rand()s return的值,继续程序:

(gdb) set $eax = 1580
(gdb) c
Continuing.
You got me!
[Inferior 1 (process 12745) exited normally]
(gdb)

程序将立即打印消息并退出。如果我们使用某种密码输入功能而不是 rand(),我们可以做完全相同的事情来规避密码检查。除了在寄存器中设置值,我们还可以输入 jump *0x000055555555463c 来跳转到 if 块;这样,我们甚至不必找到 "magic" 号码。