保护程序的堆栈内存

Posted

技术标签:

【中文标题】保护程序的堆栈内存【英文标题】:protecting stack memory of program 【发布时间】:2020-08-17 17:54:26 【问题描述】:

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

【问题讨论】:

如果问题是关于 C 的,为什么要使用 java-security 标签? 【参考方案1】:

是的,使用调试器很容易,例如数据库。在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()s返回值,继续程序:

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

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

【讨论】:

这是否意味着如果我想真正“隐藏”攻击者进入此 IF 语句的可能性,Web 应用程序会更好地解决这个问题?另外,这对我来说实际上没有意义。根据您的说法,如果桌面应用程序具有登录表单,那么任何攻击者都能够绕过 if 语句检查密码是否正确,直接进入用户仪表板?我知道不存在完美的安全性,但是这个带有调试器的东西似乎很容易@Erlkoenig 是的,一个纯粹的本地登录表单很容易被绕过。毕竟,这就是旧游戏的教练或破解者所做的;绕过许可证密钥输入以进行复制保护或在游戏中执行所需的操作以获得积分或物品。基于 Web 的方法更加安全,因为攻击者无法控制服务器。如果服务器仅在输入正确的登录数据时才显示信息,则攻击者必须找到安全漏洞来绕过登录或例如使用社会工程学获取登录数据。 我添加了一个示例来展示如何使用gdb 来操纵程序以执行您想要的操作。

以上是关于保护程序的堆栈内存的主要内容,如果未能解决你的问题,请参考以下文章

堆栈与ESP(栈指针寄存器)

iOS堆栈内存区别

堆栈和队列

Java 堆栈内存分配

Java 堆栈内存分配

初识JVM堆栈