保护程序的栈内存
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" 号码。
让我们假设我用 .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" 号码。