代码黑客攻击与防御策略
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
-
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.
启用栈金丝雀
:在需要严格检测缓冲区溢出的场景中,启用栈金丝雀可以及时发现并阻止攻击。但要考虑其带来的性能开销,以及金丝雀值被发现的风险。
通过综合运用这些防御策略,并根据具体的应用场景进行权衡和选择,我们可以提高程序的安全性,减少被黑客攻击的风险。同时,不断关注安全领域的最新动态,学习和应用新的安全技术,也是保障程序安全的重要手段。
超级会员免费看
6万+

被折叠的 条评论
为什么被折叠?



