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一樣的。