21、代码黑客攻击与防御策略

代码黑客攻击与防御策略

1. 黑客攻击概述

在编程领域,黑客攻击指的是通过各种巧妙手段非法访问计算机或网络。其中一种攻击方式是向程序提供恶意数据来进行攻击,而另一种社会工程学攻击,即通过电话、社交媒体或电子邮件诱使人们泄露密码或其他个人数据,不在本文讨论范围内。每个程序员都应该了解黑客攻击,因为如果不了解黑客如何利用程序代码中的安全漏洞,就可能在不知不觉中为他们创造机会。

2. 缓冲区溢出攻击
2.1 缓冲区溢出的原因

以一个将文本转换为大写的例程为例,该例程会持续将文本转换为大写,直到遇到空字符(0)。如果提供的文本长度超过调用者提供的输出缓冲区大小,该例程就会覆盖后续内存中的内容。当缓冲区位于栈上时,由于嵌套函数调用时函数返回地址会存储在栈中,若精心安排代码,就可以覆盖函数返回地址,使函数返回到我们指定的位置。

如果向文本字段输入过多数据,程序通常会崩溃,因为重要的程序数据被覆盖,指针被破坏。虽然黑客无法通过这种方式获取专有数据,但这是进行拒绝服务(DoS)攻击的良好基础。例如,对于一个 Web 服务器,若使其崩溃,就需要重新启动和初始化,这通常需要几秒钟时间,因此可以每隔几秒向服务器发送消息,使其持续离线。

2.2 窃取信用卡号示例

假设一家信用卡公司的 Web 服务器运行着一个使用上述大写转换程序的 Web 应用程序,该应用程序需要快速将姓名转换为大写以提高网页响应速度。网站上有一个页面允许用户输入姓名,Web 应用程序会将其转换为大写,但该页面未对输入数据的长度进行错误检查,直接将数据传递给大写转换例程。此外,为了方便,该 Web 应用程序提供了一些管理工具,如下载所有信用卡数据进行备份的功能,这些工具仅对具有特殊权限的管理员用户可用,且需要数字证书才能访问。黑客的目标是欺骗面向客户的网站部分,在无需额外身份验证的情况下访问管理部分。

以下是信用卡公司的主要 Web 应用程序代码示例:

//
// Assembler program to demonstrate a buffer
// overrun hacking attack.
//
// X0-X2 - parameters to Linux function services
// X1 - address of output string
// X0 - address of input string
// X8 - Linux function number
//
.global _start            // Provide program starting address
DownloadCreditCardNumbers:
// Setup the parameters to print hello world
// and then call Linux to do it.
      MOV    X0, #1     // 1 = StdOut
      LDR    X1, =getcreditcards // string to print
      MOV    X2, #30             // length of our string
      MOV    X8, #64             // Linux write system call
      SVC    0         // Call linux to output the string
      RET
calltoupper:
       STR   LR, [SP, #-16]!    // Put LR on the stack
       SUB   SP, SP, #16         // 16 bytes for outstr
       LDR   X0, =instr         // start of input string
       MOV   X1, SP             // address of output string
       BL    toupper
aftertoupper:       // convenient label to use as a breakpoint
       ADD   SP, SP, #16   // Free outstr
       LDR   LR, [SP], #16
       RET
_start:
       BL   calltoupper
// Setup the parameters to exit the program
// and then call Linux to do it.
      MOV     X0, #0     // Use 0 return code
      MOV     X8, #93     // Service command code 93 terminates
      SVC     0           // Call Linux to terminate the 
program
.data
instr:  .ascii  "This is our Test"     // Correct length string
        .dword 0x00000000004000b0      // overwrite for LR
getcreditcards:       .asciz  "Downloading Credit Card Data!\n"
        .align 4

当编译并运行该程序时,会不断输出“Downloading Credit Card Data!”,直到按下 Ctrl + C。这是因为程序进入了无限循环,原因是没有使用 BL 指令调用 DownloadCreditCardNumbers 例程,而是使用了 RET 指令,导致 LR 寄存器没有更新为新值, DownloadCreditCardNumbers 例程结束时的 RET 指令会再次跳转到相同地址。

2.3 栈操作步骤

以下是函数运行过程中栈的操作步骤:
1. 在 _start 中使用 BL 指令调用 calltoupper 例程,将下一条指令的地址存入 LR 并跳转到 calltoupper ,此时 LR 的值为 0x4000ec。
2. 进入 calltoupper 时,SP 为 0x7ffffff210,执行 STR LR, [sp, #-16]! 指令,将 SP 减 16 并将 LR 复制到该内存位置,此时 SP 为 0x7ffffff200,该位置的 16 字节内容为 0x004000ec 0x00000000 0x00000000 0x00000000 ,表明 LR 已压入栈中。
3. 执行 SUB SP, SP, #16 指令,为输出缓冲区分配 16 字节,SP 减为 0x7ffffff1f0,栈的内容如下:

0x7ffffff1f0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7ffffff200: 0x004000ec 0x00000000 0x00000000 0x00000000
  1. toupper 函数将字符串转换为大写,对于字符串 “This is our Test”(16 字节)能正确转换。由于没有空字符(0)终止符,会继续处理下一个非小写字节 0xb0 并原样复制,遇到空字符(0)时停止。此操作不影响 SP,但从 toupper 返回后,栈的内容变为:
0x7ffffff1f0: 0x53494854 0x20534920 0x2052554f 0x54534554
0x7ffffff200: 0x004000b0 0x00000000 0x00000000 0x00000000

可以看到,0x7ffffff200 处的返回地址从 0x004000ec 变为 0x004000b0,即 DownloadCreditCardNumbers 例程的地址。
5. calltoupper 清理栈并返回,执行 ADD SP, SP, #16 释放输出缓冲区, LDR LR, [SP], #16 DownloadCreditCardNumbers 的地址加载到 LR,RET 指令跳转到该例程,导致重大数据泄露。

在这次攻击中,有两个幸运因素:一是只需复制一个字节就能将地址更改为所需值,因为地址的下一个字节为空字符(0);二是需要复制的字节不是小写字母,所以 toupper 例程不会对其进行修改。

3. 减轻缓冲区溢出漏洞的方法
3.1 避免使用 strcpy

C 运行时的 strcpy 例程原型如下:

char * strcpy ( char * destination, const char * source );

该例程会将字符从源复制到目标,直到遇到空字符(0),这会导致缓冲区溢出漏洞。最初建议用 strncpy 替换 strcpy

char * strncpy ( char * destination, const char * source, size_t num );

num 中指定目标缓冲区的大小,复制到该位置时停止。但这样目标字符串不会以空字符(0)结尾,可能导致后续代码出现缓冲区溢出。一种建议是在复制后手动添加空字符:

strncpy( dest, source, num );
dest[num-1] = '\0';

但这要求程序员始终记住这样做,在时间紧迫的情况下可能会被遗忘。

BSD C 运行时引入了 strlcpy 函数,该函数始终以空字符(0)终止目标字符串:

size_t strlcpy(char *destination, const char *source, size_t size);

该函数解决了目标字符串未以空字符结尾的问题,但它是非标准函数,不属于 GNU C 库。

strncpy strlcpy 类型的函数存在一个问题,即它们会消除嵌套函数以快速构建更复杂字符串的能力,因为在拼接字符串时不容易知道剩余的缓冲区长度。另一个建议的解决方案是 strecpy 函数:

char * strecpy ( char * destination, const char * source, char * end );

该函数传入目标缓冲区末尾的指针,在嵌套调用时很方便,因为 end 保持不变,而剩余长度会随着字符串的构建而缩小。但它也是非标准函数,不属于 C 运行时。

这些函数都能防止覆盖目标缓冲区并避免数据损坏,但都可能导致敏感数据泄露。若源字符串未以空字符(0)结尾且源缓冲区小于目标缓冲区,函数会复制数据直到目标缓冲区填满,可能会将源缓冲区末尾之后的敏感数据复制到目标缓冲区。为解决这个问题,引入了 strncpy_s 函数:

errno_t strncpy_s(char * destination, size_t destmax, const char * source, size_t srcmax);

strncpy_s 中,需要提供两个缓冲区的大小,函数会返回错误代码告知操作结果。

3.2 使用位置无关可执行文件(PIE)

之前的攻击依赖于知道 DownloadCreditCardNumbers 例程的地址,而现代虚拟内存系统引入了位置无关可执行文件(PIE)特性。自 2005 年左右引入 Linux 后,每次运行可执行文件时,它会以不同的基地址加载,这是地址空间布局随机化(ASLR)的一种特殊情况。

要启用 PIE,需要在 ld 命令行中添加 -pie 选项。例如:

as   main.s -o main.o
as   upper.s -o upper.o
ld -pie -o upperpie main.o upper.o

启用 PIE 后,程序运行时地址会改变。但在调试时,通常会关闭 PIE 以方便解码程序运行情况。

Apple 的 iOS 操作系统默认启用 PIE,若程序无法处理,需要手动关闭。虽然启用 PIE 后信用卡号未被盗取,但程序仍可能崩溃,这可能被黑客利用进行 DoS 攻击。

程序不可重定位的主要原因是在数据段中硬编码了链接器不知道的内存地址。例如,使用 LDR 指令时会在内存中创建一个地址,同时也会创建一个重定位记录,以便加载器修复地址。Apple 强制使用 ADR 代替 LDR 以减少需要处理的重定位记录数量。若使用 MOV 和三个 MOVK 指令加载内存地址,程序将不可重定位,因为加载器无法理解操作并修复地址。

启用 PIE 是编写 C 或汇编语言程序的良好实践,虽然它不是完美的,黑客可能找到绕过方法,但它增加了攻击的难度,通常需要除缓冲区溢出之外的第二个漏洞才能成功攻击程序。

3.3 使用栈金丝雀

GNU C 编译器具有检测缓冲区溢出的功能,即栈金丝雀。在包含位于栈上的字符串缓冲区的例程中,会添加额外代码,在存储的函数返回地址旁边放置一个秘密随机值。函数返回前会检查该值,若被破坏,则表明发生了缓冲区溢出,程序将终止。

要启用栈金丝雀功能,可使用 gcc 命令行选项 -fstack-protector-all ,这是该功能最激进的形式。例如:

gcc -o uppercanary -fstack-protector-all -O3 upper.c

启用栈金丝雀后,程序能防止缓冲区溢出,但会在每个函数中添加一些指令,增加了开销。以下是生成的代码示例:

00000000000008e8 <routine>:
 8e8:   a9be7bfd    stp   x29, x30, [sp, #-32]!
 8ec:   90000080    adrp  x0, 10000 <__FRAME_END__+0xf3c0>
 8f0:   910003fd    mov   x29, sp
 8f4:   f947e400    ldr   x0, [x0, #4040]
 8f8:   f9400001    ldr   x1, [x0]
 8fc:   f9000fe1    str   x1, [sp, #24]
 900:   d2800001    mov   x1, #0x0                  // #0
// body of routine ...
 904:   f9400fe1    ldr   x1, [sp, #24]
 908:   f9400000    ldr   x0, [x0]
 90c:   ca000020    eor   x0, x1, x0
 910:   b5000080    cbnz  x0, 920 <routine+0x38>
 918:   a8c27bfd    ldp   x29, x30, [sp], #32
 91c:   d65f03c0    ret
 920:   97ffff74    bl    6f0 <__stack_chk_fail@plt>

函数序言部分的指令操作如下:
1. STP:将 LR 和 FP 存储到栈中,将栈指针减 32 以留出栈金丝雀的空间。
2. ADRP:加载指向包含数据段页面的指针,这里主要关注栈金丝雀值,大多数例程也会用于其他目的。
3. MOV:将 SP 移动到 FP,设置 C 栈帧。
4. LDR:形成栈金丝雀的地址,偏移量 4040 是栈金丝雀存储的位置,这是 C 运行时初始化代码生成的随机值。
5. LDR:将栈金丝雀的值加载到寄存器 X1。
6. STR:将栈金丝雀存储到栈上的正确位置,以保护函数返回指针(压入的 LR)。
7. MOV:将栈金丝雀覆盖为零,防止数据泄露。

函数尾声部分的指令操作如下:
1. LDR:将栈金丝雀从栈加载到寄存器 X1。
2. LDR:从 C 运行时的数据段加载原始栈金丝雀值,由于 X0 仍包含指针,无需重新构建。
3. EOR:比较两个值,异或操作的结果为零表示两个值相同。
4. CBNZ:若值不相等(Z 标志未设置),则跳转到 RET 指令后的 BL 指令。
5. LDP:从栈中加载 LR 和 FP,若能执行到这一步,说明栈金丝雀未被破坏,LR 大概率未被覆盖。
6. RET:正常子程序返回。
7. BL:调用错误报告例程,该例程会终止程序。

栈金丝雀相当有效,但如果黑客发现了运行过程中使用的值,就可以构造缓冲区溢出攻击。此外,程序以这种方式终止也不是理想情况。

综上所述,为了提高程序的安全性,应避免使用易导致缓冲区溢出的函数,合理使用 PIE 和栈金丝雀等技术,但同时也要考虑这些技术带来的性能开销和潜在问题。在实际编程中,需要根据具体情况权衡各种方法的利弊,采取综合措施来保护程序免受黑客攻击。

代码黑客攻击与防御策略(续)

4. 防御策略总结与对比

为了更清晰地了解各种防御缓冲区溢出漏洞方法的特点,下面通过表格进行总结对比:
| 防御方法 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 避免使用 strcpy 及相关替代函数 | 防止覆盖目标缓冲区,避免数据损坏 | 可能导致敏感数据泄露,部分函数非标准,嵌套功能受限 | 对数据复制有基本安全要求的场景 |
| 使用位置无关可执行文件(PIE) | 增加攻击难度,每次运行地址随机化 | 程序可能崩溃,易被用于 DoS 攻击,需处理程序可重定位问题 | 对地址安全性要求较高的场景 |
| 使用栈金丝雀 | 能有效检测缓冲区溢出 | 增加程序开销,若金丝雀值被发现易被攻击,程序终止影响使用 | 对缓冲区溢出检测有严格要求的场景 |

5. 防御策略实施流程图
graph TD;
    A[编写程序] --> B{是否使用易导致溢出函数};
    B -- 是 --> C[考虑替代函数];
    C --> D{是否需要地址随机化};
    D -- 是 --> E[启用 PIE];
    D -- 否 --> F{是否需要检测缓冲区溢出};
    E --> F;
    F -- 是 --> G[启用栈金丝雀];
    F -- 否 --> H[完成编程];
    B -- 否 --> F;
    G --> H;
6. 实际应用案例分析

假设我们正在开发一个简单的命令行工具,用于接收用户输入并进行处理。以下是一个可能存在缓冲区溢出风险的代码示例:

#include <stdio.h>
#include <string.h>

void processInput(char *input) {
    char buffer[10];
    strcpy(buffer, input);
    // 处理 buffer 中的数据
    printf("Processed input: %s\n", buffer);
}

int main() {
    char input[100];
    printf("Please enter some input: ");
    fgets(input, sizeof(input), stdin);
    processInput(input);
    return 0;
}

在这个示例中, strcpy 函数可能会导致缓冲区溢出。为了修复这个问题,我们可以采用前面提到的防御策略。

6.1 使用 strncpy 替代 strcpy
#include <stdio.h>
#include <string.h>

void processInput(char *input) {
    char buffer[10];
    strncpy(buffer, input, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';
    // 处理 buffer 中的数据
    printf("Processed input: %s\n", buffer);
}

int main() {
    char input[100];
    printf("Please enter some input: ");
    fgets(input, sizeof(input), stdin);
    processInput(input);
    return 0;
}

通过使用 strncpy 并手动添加空字符,我们可以避免缓冲区溢出。

6.2 启用栈金丝雀

使用 gcc 编译时添加 -fstack-protector-all 选项:

gcc -o tool -fstack-protector-all -O3 tool.c

这样,当发生缓冲区溢出时,栈金丝雀会检测到并终止程序。

6.3 启用 PIE

使用 ld 命令添加 -pie 选项:

gcc -c tool.c -o tool.o
ld -pie -o tool tool.o

启用 PIE 后,程序每次运行的地址会随机化,增加了攻击的难度。

7. 总结与建议

在编程过程中,缓冲区溢出是一个常见且危险的安全漏洞。为了保护程序免受黑客攻击,我们可以采取以下措施:
1. 谨慎使用字符串复制函数 :避免使用 strcpy ,优先选择更安全的替代函数,如 strncpy_s ,并注意处理可能的敏感数据泄露问题。
2. 合理使用 PIE :对于对地址安全性要求较高的程序,启用 PIE 可以增加攻击的难度。但要注意处理程序的可重定位问题,避免因硬编码内存地址导致程序无法正常运行。
3. 启用栈金丝雀 :在需要严格检测缓冲区溢出的场景中,启用栈金丝雀可以及时发现并阻止攻击。但要考虑其带来的性能开销,以及金丝雀值被发现的风险。

通过综合运用这些防御策略,并根据具体的应用场景进行权衡和选择,我们可以提高程序的安全性,减少被黑客攻击的风险。同时,不断关注安全领域的最新动态,学习和应用新的安全技术,也是保障程序安全的重要手段。

【电能质量扰动】基于ML和DWT的电能质量扰动分类方法研究(Matlab实现)内容概要:本文研究了一种基于机器学习(ML)和离散小波变换(DWT)的电能质量扰动分类方法,并提供了Matlab实现方案。首先利用DWT对电能质量信号进行多尺度分解,提取信号的时频域特征,有效捕捉电压暂降、暂升、中断、谐波、闪变等常见扰动的关键信息;随后结合机器学习分类器(如SVM、BP神经网络等)对提取的特征进行训练分类,实现对不同类型扰动的自动识别准确区分。该方法充分发挥DWT在信号去噪特征提取方面的优势,结合ML强大的模式识别能力,提升了分类精度鲁棒性,具有较强的实用价值。; 适合人群:电气工程、自动化、电力系统及其自动化等相关专业的研究生、科研人员及从事电能质量监测分析的工程技术人员;具备一定的信号处理基础和Matlab编程能力者更佳。; 使用场景及目标:①应用于智能电网中的电能质量在线监测系统,实现扰动类型的自动识别;②作为高校或科研机构在信号处理、模式识别、电力系统分析等课程的教学案例或科研实验平台;③目标是提高电能质量扰动分类的准确性效率,为后续的电能治理设备保护提供决策依据。; 阅读建议:建议读者结合Matlab代码深入理解DWT的实现过程特征提取步骤,重点关注小波基选择、分解层数设定及特征向量构造对分类性能的影响,并尝试对比不同机器学习模型的分类效果,以全面掌握该方法的核心技术要点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值