Because of time and ability, i just finished one problem in this contest. after the game, i first write the answer of my finished problem, later i’ll resubmit the other problems’writeup.


this problem is interesting. i think it’s the easiest problem in the whole contest.

it seems there is no vuln in the program. but when dig into it, a race condition is in there.

the reason that the vuln formed is that it use a thread to execute the encrypt or decrypt. what’s more, there is a sleep(2) function in the thread.

a simple example of the vuln is as shown as below:


when i use go to execute the encrypt of chunk 0 in thread 1, but it will sleep 2 seconds. In the 2 seconds, the main function will delete the chunk 0, so it will free into bins which will can used to leak address.

Essentially, its may called a uaf vuln because of the race condition.

we can use it leak heap address first, and then with the heap address we can leak libc address.

we can leak any address we want, but how to write a address? we can use the same vuln to overwrite the first big chunk which size is 0x1010, we can build a fake decrypt which size is bigger than 0x1010, and which will then overwrite into next freed chunk. because the features of tcache, we can malloc out __malloc_hook, and write one gadget into it.

finally get the shell.


this is a problem that we need patience to find how to exploit. the most important thing is we need to learn how AddressSanitizer works to protect memory.

there are there vulns in the program:

  1. in add_note function, the ID is located next to content with no null fence which forms a bigger content than expected. and in update_note function, we can use this characteristic to overwrite to next chunk.
  2. in secret function, we can write a null byte to any address bigger than 0x700000000000
  3. there is also a uaf vuln, the note pointer was not set to null, after delete

with upper 3 vulns, how to exploit it?

first we need to know how AddressSanitizer works to protect the memory. it will set a shadow for memory, every 8 bytes correspond to one shadow byte.

Shadow = (Mem >> 3) + 0x7fff8000;

when access to a memory, it will check the shadow whether its shadow is 0 or not. if it is 0, it will permit read or write, or it will report error.

a example is shown as below:

if ( *(_BYTE *)((v1 >> 3) + 0x7FFF8000) )

so we can use write the secret func to write a null byte to shadow, which will allow us to handle any 8 bytes we handle.

second, which 8 bytes should we write? a chunk header is shown as below:

struct ChunkHeader {
  // 1-st 8 bytes.
  u32 chunk_state       : 8;  // Must be first.
  u32 alloc_tid         : 24;

  u32 free_tid          : 24;
  u32 from_memalign     : 1;
  u32 alloc_type        : 2;
  u32 rz_log            : 3;
  u32 lsan_tag          : 2;
  // 2-nd 8 bytes
  // This field is used for small sizes. For large sizes it is equal to
  // SizeClassMap::kMaxSize and the actual size is stored in the
  // SecondaryAllocator's metadata.
  u32 user_requested_size : 29;
  // align < 8 -> 0
  // else      -> log2(min(align, 512)) - 2
  u32 user_requested_alignment_log : 3;
  u32 alloc_context_id;

Before we continue, we have to go over a very important concept of ASAN’s allocator. The allocator is not designed to be hyper efficient, but to find bugs. Thus, it will try to avoid allocating valid memory over freed memory at all costs so it can catch bugs like UAF. It does this by putting free chunks into quarantine. Quarantined chunks are only reused after a certain amount of memory has been freed. The standard setting is quarantine_size_mb=256M which can be seen if you set the environment variable export ASAN_OPTIONS=verbosity=1. Thus, we need to free a HUGE amount of memory before it gets reused.

what we want is overwrite the size of the victim chunk with a HUGE number. From our experience, 0xffffffff should do it

so our target is the addr of a header of a chunk, because of no aslr in this heap algorithm, so we can make sure the address with no leak.

ok, right now, we got everything we need to get the exploit. the whole exploit contains:

  1. add a note with size of 0x10.
  2. overwrite the shadow memory with null byte of the second chunk header.
  3. use the overwrite vuln in update function to change the chunk size to 0xffffffff
  4. free the note.
  5. allocate again with 0x10 size, the two chunk’s address will turn around.
  6. leak heap and libc address.
  7. overwrite _ZN11__sanitizerL15UserDieCallbackE in bss addr with one gadget. which will execute when memory goes error:
    if ( __sanitizer::UserDieCallback )
      v2 = &__sanitizer::InternalDieCallbacks;
  8. get the shell.


I think it’s a relative simple for the game. because its libc is libc-2.28 and i think it may be a new pwn skill, so i didn’t see the program in the game time.(also for the reason of time).

There is only a off-by-null vuln in update function. because we can only malloc chunk which size is smaller and equal than 0x58, we can’t simply build overlap chunk by off-by-null.

Based on past experience, we can use this off-by-null vuln on heap to build overlap chunk to leak and get shell. but the limitation is that need to write a null to a chunk which size is bigger than 0x100.

here we can only malloc chunks which size is smaller than 0x58. how to get chunk bigger than 0x100. in past game, we can malloc some fastbin chunks and free them first and then use scanf alloc a big chunk to trigger malloc_consolidate which can consolidate fastbins to unsorted bin. so we can get chunk size which is bigger than 0x100.

how about this game? as we can see, there is a big malloc in init function which malloc out a chunk which size is 0x1F000 and leave a small top chunk in memory. so the idea is that we can malloc fastbins and then use off-by-null to decrease top chunk faster, which will finally trigger malloc_consolidate for top chunk is too small. the relative source code is shown as below:

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))

      /* When we are using atomic ops to free fast chunks we can get
         here for all block sizes.  */
      else if (atomic_load_relaxed (&av->have_fastchunks))
          malloc_consolidate (av);

so we can get chunk bigger than 0x100 by upper ways. it seems that we can hopefully get the shell.

there is a little things that i need to point out. the program uses calloc to malloc memory, which will finally call __int_malloc to malloc memory. as we know, there is tcache in libc 2.28, the magic trick is that tcache malloc is only in __libc_malloc(which is malloc). there is no tcache malloc in __int_malloc.

which means if we malloc chunks out with calloc then free into tcache list. and then calloc again, it will not malloc out again. so it we put enough free chunk to tcache chunk. from then on, there is no tcache any more. (we can use this trick to build exp more quickly)

we can use overlap chunk to leak libc and heap address. and then use overlap chunk to write address we want to top chunk, and then malloc that area out.

one area that we normally use is the __malloc_hook, write one gadget to __malloc_hook and get the shell. but in this program we can’t get shell, for the limitation is not satisfied. A trick is that we can not overwrite __malloc_hook to one gadget which limitation is [rsp+0x30] == NULL. but in this program the [rsp+0x28] is NULL . so we can write a call realloc address to __malloc_hook and then write one gadget to __realloc_hook, and then get the shell.

another area is that overwrite __free_hook minus around 0xb00 address to top chunk, and malloc the chunk out. finally get the __free_hook, write system to the hook and then get the shell.

in my [exp]((, i use the first solution.


Trying to do the similar game for the first time, interesting.

It’s a custom vim program with a perm.diff which gives out the differences between original vim and the custom vim program.

it adds a new encrypt method in the vim binary, you can use set cm=perm to trigger the encryption.

- p1 = getcmdline_prompt(NUL, round == 0
-   ? (char_u *)_("Enter encryption key: ")
-   : (char_u *)_("Enter same key again: "), 0, EXPAND_NOTHING,
-   NULL);
+ // to avoid interactive step, without loss of generality
+ p1 = alloc(8);
+ p1[0] = 'a';
+ p1[1] = NUL;

As show in above, the key has been set to a by hardcoding. and the perm.diff also show the source code of the new algorithm.

The vuln is a Integer overflow. The relative struct is shown as below:

+typedef struct {
+    int key;
+    int shift;
+    int step;
+    int orig_size;
+    int size;
+    int cur_idx;
+    char_u *buffer;
+} perm_state_T;

In crypt_perm_decode function, we can control the step to 0xffffffff which is -1(the source code isps->step = ps->key ^ iv;), and it will also can bypass the check.

+    i = 4;
+    while (i < len)
+    {
+        if (ps->cur_idx < ps->orig_size)
+        {
+            to[ps->cur_idx+4] = from[i];  //overwrite here
+            i++;
+        }
+        ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;
+    }

as show in the line with comment,once we set ps->step to -1, we can underflow the to array step by step. so we need to what to overwrite located before the to array.

During the debugging, we can know the perm_state_T struct is located before the to array. What’s more important, there is a char_u *buffer; pointer in perm_state_T struct. We can overwrite the pointer to a address and then write value into it by the crypt_perm_decode function which formed a write-to-where vuln:

/* Step 2: Inverse of Addition */
+    for (i = 0; i < ps->shift; ++i)
+        ps->buffer[i] = to[i+4];

With the vuln, we can do almost everything. How to get the flag?

The solution is we can overwrite the ps->buffer to free got and revise the ps-cur_idx, and then overwrite the free got to 0x4C915d which is a execl("/bin/sh","sh","-c",$rax:

.text:00000000004C915D                 mov     r8d, 0
.text:00000000004C9163                 mov     rcx, rax
.text:00000000004C9166                 lea     rdx, aC_2       ; "-c"
.text:00000000004C916D                 lea     rsi, arg        ; "sh"
.text:00000000004C9174                 lea     rdi, path       ; "/bin/sh"
.text:00000000004C917B                 mov     eax, 0
.text:00000000004C9180                 call    _execl

and in later call vim_free(ps->buffer);, we also can control the $rax, we can set it to cat flag to get the flag.



That’s all for the contest. there are still applepie, plang, scanner, babysandbox, but i don’t want to go on for the type is not so typical.

the upper 4 exps is in my github, and i write some comments in it, hope it can help you.

  1. AddressSanitizer算法及源码解析
  4. 0CTF 2019 babyaegis writeup
  5. “Baby” Heap 2019 - 0ctf Quals 2019
  6. TCTF 2019 Babyheap
  7. 0CTF/TCTF 2019 Quals If on a winters night a traveler writeup
  8. If on a winters night a traveler write-up (0CTF/TCTF Quals 2019)
  9. If on a winters night a traveler