Stack overflow攻击是一种很常见的代码攻击,armcc和gcc等编译器都实现了stack protector来避免stack overflow攻击。虽然armcc和gcc在汇编代码生成有些不同,但其原理是相同的。这篇文章以armcc为例,看一看编译器的stack protector。
armcc提供了三个编译选项来打开/关闭stack protector。
- –no_protect_stack 关闭stack protector
- –protect_stack 为armcc认为危险的函数打开stack protector
- –protect_stack_all 为所有的函数打开stack protector
armcc如何防止stack overflow攻击?
armcc在函数栈中的上下文和局部变量之间插入了一个数字来监控堆栈破坏,这个值一般被称作为canary word,在armcc中将这个值定义为__stack_chk_guard。当函数返回之前,函数会去检查canary word是否被修改,如果canary word被修改了,那么证明函数栈被破坏了,这个时候armcc就会去调用一个函数来处理这种栈破坏行为,armcc为我们提供了__stack_chk_fail这个回调函数来处理栈破坏。
因此,在armcc打开- –protect_stack之前需要在代码中设置__stack_chk_guard和__stack_chk_fail。我从ARM的官网上摘抄了一段它们的描述。
void *__stack_chk_guard
You must provide this variable with a suitable value, such as a random value. The value can change during the life of the program. For example, a suitable implementation might be to have the value constantly changed by another thread.
void __stack_chk_fail(void)
It is called by the checking code on detection of corruption of the guard. In general, such a function would exit, possibly after reporting a fault.
armcc stack protector产生了什么代码来防止stack overflow?
首先来看一下写的一个c代码片段, 代码很简单,__stack_chk_guard 设置为一个常数,当然这只是一个例子,最好的方法是设置这个值为随机数。然后重写了__stack_chk_fail这个回调接口。test_stack_overflow这个函数很简单,仅仅在函数栈上分配了i和c_arr这两个局部变量,并对部分成员赋值。
void __stack_chk_fail()
{
print_uart0("__stack_chk_fail()\n");
while(1);
}
void *__stack_chk_guard = (void *)0;
int test_stack_overflow(int a, int b, int c, int d, int e)
{
int i;
int c_arr[15];
int *p = c_arr;
i = 15;
c_arr[0] = 2;
c_arr[1] = 3;
return 0;
}
OK,首先看一下在–no_protect_stack情况下armcc产生的汇编代码,仅仅只是在栈上分配c_arr这个局部数组,而i这个变量则使用r1寄存器来保存。
60010044 <test_stack_overflow>:
60010044: e92d4070 push {r4, r5, r6, lr}
60010048: e24dd03c sub sp, sp, #60 ; 0x3c
6001004c: e1a04000 mov r4, r0
60010050: e1a05001 mov r5, r1
60010054: e1a06002 mov r6, r2
60010058: e59dc04c ldr ip, [sp, #76] ; 0x4c
6001005c: e1a0200d mov r2, sp
60010060: e3a0100f mov r1, #15
60010064: e3a00002 mov r0, #2
60010068: e58d0000 str r0, [sp]
6001006c: e3a00003 mov r0, #3
60010070: e58d0004 str r0, [sp, #4]
60010074: e3a00000 mov r0, #0
60010078: e28dd03c add sp, sp, #60 ; 0x3c
6001007c: e8bd8070 pop {r4, r5, r6, pc}
其栈上的内存map如下图所示
然后看一看使用–protect_stack_all选项编译后产生的汇编代码
600100a0 <test_stack_overflow>:
600100a0: e92d47f0 push {r4, r5, r6, r7, r8, r9, sl, lr}
600100a4: e24dd040 sub sp, sp, #64 ; 0x40
600100a8: e1a07000 mov r7, r0
600100ac: e1a08001 mov r8, r1
600100b0: e1a09002 mov r9, r2
600100b4: e1a0a003 mov sl, r3
600100b8: e59d6060 ldr r6, [sp, #96] ; 0x60
600100bc: e59f0094 ldr r0, [pc, #148] ; 60010158 <c_entry+0x58>
600100c0: e5904000 ldr r4, [r0]
600100c4: e58d403c str r4, [sp, #60] ; 0x3c
600100c8: e1a00000 nop ; (mov r0, r0)
600100cc: e1a00000 nop ; (mov r0, r0)
600100d0: e3a00002 mov r0, #2
600100d4: e58d0000 str r0, [sp]
600100d8: e3a00003 mov r0, #3
600100dc: e3a05000 mov r5, #0
600100e0: e58d0004 str r0, [sp, #4]
600100e4: e59d003c ldr r0, [sp, #60] ; 0x3c
600100e8: e1500004 cmp r0, r4
600100ec: 0a000000 beq 600100f4 <test_stack_overflow+0x54>
600100f0: ebffffc2 bl 60010000 <__stack_chk_fail>
600100f4: e1a00005 mov r0, r5
600100f8: e28dd040 add sp, sp, #64 ; 0x40
600100fc: e8bd87f0 pop {r4, r5, r6, r7, r8, r9, sl, pc}
两段代码主要的差异在于如下
600100bc: e59f0094 ldr r0, [pc, #148] ; 60010158 <c_entry+0x58>
600100c0: e5904000 ldr r4, [r0]
600100c4: e58d403c str r4, [sp, #60] ; 0x3c
这段代码很简单,就是从60010158 这个地址取出一个值,再将这个值作为地址取出他的值,将它保存到了sp, #60这个位置,这个位置就是位于上下文的下方和c_arr数组的上方。可以看一下此时的函数栈内存map是什么样子,如下图
还有一段差异代码如下,很简单就是在函数return之前拿出这个stack_chk_guard比较了一下,如果这个值被修改了就证明函数栈被破坏,如果没被修改就说明函数可以正常返回。
600100e4: e59d003c ldr r0, [sp, #60] ; 0x3c
600100e8: e1500004 cmp r0, r4
600100ec: 0a000000 beq 600100f4 <test_stack_overflow+0x54>
600100f0: ebffffc2 bl 60010000 <__stack_chk_fail>
armcc中stack protector的作用
这个段落中写了一段代码来模拟这个stack overflow攻击,大部分代码与之前没有什么差异,在test_stack_overflow中*(p + 15) = 1234这一句表示修改stack_chk_guard,从图2中可以看到c_arr是15个整形变量数组,那么p+15就正好位于c_arr上方,即stack_chk_guard。同样根据上图推算出p+23就是栈中保存的返回地址,在这里将返回地址修改为attack_attack这个函数地址,来模拟栈被攻击后跳转到黑客想去运行的地址。attack_attack只是打印了一句话而已。
int test_stack_overflow(int a, int b, int c, int d, int e)
{
int i;
int c_arr[15];
int *p = c_arr;
i = 15;
c_arr[0] = 2;
c_arr[1] = 3;
*(p + 15) = 1234; /* modify the guard word, see fig.2*/
*(p + 23) = (int)attack_attack; /* modify return address as the attack function, see fig.2*/
return 0;
}
int c_entry()
{
print_uart0("befroe test_stack_overflow\n");
test_stack_overflow(1, 2, 3, 4, 5);
print_uart0("after test_stack_overflow\n");
return 0;
}
void attack_attack()
{
print_uart0("attack attack!\n");
}
将代码编译后,运行于qemu-system-arm上,得到打印如下,正如我们所预期的一样,由于stack_chk_guard被修改了,证明函数栈已经被破坏,所以并没有运行到attack_attack函数,而是跳转到了__stack_chk_fail进行处理。
不过需要注意的是,如果攻击者能够绕过stack_chk_guard而去直接修改pc值,那么stack protector是没有效果的,任何编译器都是一样的。但其实由于stack overflow的特性,攻击者是很难绕过stack_chk_guard这个值而去直接修改pc。假设他能绕过stack_chk_guard,那么实际上他可以去任意修改栈中的数据,也就不需要使用stack overflow来进行攻击。还是以上面那段代码做一个实验,在test_stack_overflow函数中将 *(p + 15) = 1234注释到,那么最终还是能够运行到attack_attack函数。代码如下:
int test_stack_overflow(int a, int b, int c, int d, int e)
{
int i;
int c_arr[15];
int *p = c_arr;
i = 15;
c_arr[0] = 2;
c_arr[1] = 3;
//*(p + 15) = 1234; /*no modify the guard word, see fig.2*/
*(p + 23) = (int)attack_attack; /* modify return address, see fig.2*/
return 0;
}
int c_entry() {
print_uart0("befroe test_stack_overflow\n");
test_stack_overflow(1, 2, 3, 4, 5);
print_uart0("after test_stack_overflow\n");
return 0;
}
实验结果
小结
armcc中stack protector通过一些简单的设置就能实现,其他编译器的实现的原理也是大同小异,至少我看过gcc的stack protector,它的实现方式是跟armcc一样的。