Study

개발 언어의 동작 원리


Compiler나 Interpreter에 의해 하이레벨에서 로우레벨로 변환이 되어 CPU에게 명령을 내림

어셈블리어 문법 구조


[opcode1] [operand2] 형식으로 이루어져 있음

주요 명령 코드

수행 역할 명령 코드
데이터 이동(Data Transfer) mov, lea
산술 연산(Arithmetic) inc, dex, add, sub
논리 연산(Logical) and, or, xor, not
비교(Comparison) cmp, test
분기(Branch) jmp, je, jg
스택(Stack) push, pop
프로시저(Procedure) call, ret, leave
시스템 콜(System call) syscall

피연산자의 종류 총 3가지 종류가 존재

  • 상수(Immediate Value)
  • 레지스터(Register)
  • 메모리(Memory)
    ※ 메모리 피연산자는 []으로 둘러싸인 것으로 표현, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있음

데이터 이동

명령 설명
mov rdi. rsi rsi의 값을 rdi에 대입
mov QWORD PRT[rdi], rsi rsi의 값을 rdi가 가리키는 주소에 대입
lea rsi, [rbx+8 * rcx] rbx+8 * rcxrsi에 대입

mov : 레지스터나 메모리의 값을 다른 레지스터로 로드
lea : 메모리 주소 자체를 레지스터로 로드

산술 연산

명령 설명
add eax, 3 eax 레지스터의 값에 3을 더하기
sub eax, 3 eax 레지스터의 현재 값에서 3을 빼기
mul eax, 3 eax 레지스터의 값을 3을 곱하기
div ebx 부호 없는 나눗셈
edx:eaxebx로 나누고 몫은 eax, 나머지는 edx에 저장
idiv ebx 부호 있는 나눗셈
edx:eaxebx로 나누고 몫은 eax, 나머지는 edx에 저장
inc eax eax 레지스터의 값을 1 증가
dec eax eax 레지스터의 값을 1 감소

div와 idiv의 차이점
div는 부호가 없기 때문에 양수의 몫과 나머지를 반환함. 즉, 음수의 몫과 나머지가 나오면 반환을 못함.
idiv는 부호가 있기 때문에 결과가 양수 또는 음수가 될 수 있음.

문제

ASMLevel1에 오신 것을 환영합니다.
==================================================

어떤 레벨과 상호작용하려면 이 프로그램에 stdin을 통해 원시 바이트를 보내야 합니다.
이 문제를 효과적으로 해결하려면 먼저 필요한 것이 무엇인지 확인하기 위해 한 번 실행해보십시오.
그런 다음, 조립하고 바이트를 만들어 이 프로그램에 파이프로 전송하세요.

예를 들어, asm.S 파일에 어셈블리 코드를 작성한다면 다음과 같이 오브젝트 파일로 조립할 수 있습니다:
as -o asm.o asm.S

그 다음, .text 섹션 (코드)을 asm.bin 파일로 복사할 수 있습니다:
objcopy -O binary --only-section=.text asm.o asm.bin

마지막으로, 그것을 도전과제에 전송할 수 있습니다:
cat ./asm.bin | /challenge/run

모든 것을 한 번의 명령으로 실행할 수도 있습니다:
as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run

이 레벨에서는 레지스터와 함께 작업하게 됩니다. 레지스터 수정이나 읽기가 요청될 것입니다.

이 레벨에서는 registers_use와 함께 작업하게 됩니다! 다음을 설정해 주십시오:
* rdi = 0x1337

어셈블리를 바이트로 제공해 주십시오 (최대 0x1000 바이트까지):

/challenge/run에서 나온 방식대로 어셈블리 코드를 작성하지 않고, nasm으로 제출하도록 하겠음.

문제 풀이

[bits 64]
mov rdi, 0x1337

코드 분석

해당 pwn.college의 어셈블리 문제를 풀면서 초심자에게 해당 방법대로 학습하면 훨씬 좋을 거 같아서, 문제를 분석해보기로 했음.

/challenge/run를 cat으로 한번 확인해보면, 엄청난 파이썬 코드를 볼 수 있음.
즉, 파이썬으로 구현된 것을 알 수 있었음. 하지만 level1 문제 말고도 다른 문제의 코드도 있었기 때문에 기본적인 동작 원리 및 level1 코드만 확인해봤음.

pwnlib.context.context.update(arch="amd64")
builtin_print = print
print = lambda text: builtin_print(re.sub("\n{2,}", "\n\n", textwrap.dedent(str(text))))

config = (pathlib.Path(__file__).parent / ".config").read_text()
level = int(config)

해당 코드는 pwnlib을 64비트 모드로 설정하고 출력을 깔끔하게 보여주기 위해 print 함수를 변경함.
그리고 현재 디렉토리의 .config 파일에서 문제 레벨 정보를 불러옴.

#!/opt/pwn.college/python

import sys
import re
import collections
import string
import random
import textwrap
import pathlib
from collections import defaultdict

import pwnlib
import pwnlib.asm
from unicorn import *
from unicorn.x86_const import *
from capstone import *


pwnlib.context.context.update(arch="amd64")
builtin_print = print
print = lambda text: builtin_print(re.sub("\n{2,}", "\n\n", textwrap.dedent(str(text))))

config = (pathlib.Path(__file__).parent / ".config").read_text()
level = int(config)


class ASMBase:
    """
    ASM:
    A set of levels to teach people the basics of x86 assembly:
    - registers_use
    - stack
    - functions
    - control statements
    Level Layout:
    === Reg ===
    1. Reg write
    2. Reg modify
    3. Reg complex use
    4. Integer Division
    5. Modulo
    6. Smaller register access
    === Bits in Registers ===
    7. Shifting bits
    8. Logic gates as a mov (bit logic)
    9. Hard bit logic challenge
    === Mem Access ===
    10. Read & Write from static memory location
    11. Sized read & write from static memory
    12. R/W to dynamic memory (stored in registers)
    13. Access adjacent memory given at runtime
    === Stack ===
    14. Pop from stack, modify, push back
    15. Stack operations as a swap
    16. r/w from stack without pop (rsp operations)
    === Control Statements ===
    17. Unconditional jumps (jump trampoline, relative and absolute)
    18. If statement jumps (computing value based on a header in mem)
    19. Switch Statements
    20. For-Loop (summing n numbers in memory)
    21. While-Loop (implementing strlen, stop on null)
    === Functions ===
    22. Making your own function, calling ours
    23. Making your own function with stack vars (the stack frame)
    """

    BASE_ADDR = 0x400000
    CODE_ADDR = BASE_ADDR
    LIB_ADDR = BASE_ADDR + 0x3000
    DATA_ADDR = BASE_ADDR + 0x4000
    BASE_STACK = 0x7FFFFF000000
    RSP_INIT = BASE_STACK + 0x200000
    REG_MAP = {
        "rax": UC_X86_REG_RAX,
        "rbx": UC_X86_REG_RBX,
        "rcx": UC_X86_REG_RCX,
        "rdx": UC_X86_REG_RDX,
        "rsi": UC_X86_REG_RSI,
        "rdi": UC_X86_REG_RDI,
        "rbp": UC_X86_REG_RBP,
        "rsp": UC_X86_REG_RSP,
        "r8": UC_X86_REG_R8,
        "r9": UC_X86_REG_R9,
        "r10": UC_X86_REG_R10,
        "r11": UC_X86_REG_R11,
        "r12": UC_X86_REG_R12,
        "r13": UC_X86_REG_R13,
        "r14": UC_X86_REG_R14,
        "r15": UC_X86_REG_R15,
        "rip": UC_X86_REG_RIP,
        "efl": UC_X86_REG_EFLAGS,
        "cs": UC_X86_REG_CS,
        "ds": UC_X86_REG_DS,
        "es": UC_X86_REG_ES,
        "fs": UC_X86_REG_FS,
        "gs": UC_X86_REG_GS,
        "ss": UC_X86_REG_SS,
    }

    init_memory = {}
    secret_key = random.randint(0, 0xFFFFFFFFFFFFFFFF)

    registers_use = False
    dynamic_values = False
    memory_use = False
    stack_use = False
    bit_logic = False
    ip_control = False
    multi_test = False
    functions = False

    whitelist = None
    blacklist = None

    interrupt_stack_read_length = 4
    interrupt_memory_read_length = 4
    interrupt_memory_read_base = DATA_ADDR

    def __init__(self, asm=None):
        self.asm = asm

        self.emu = None
        self.bb_trace = []

        self.init()

    @property
    def description(self):
        raise NotImplementedError

    @property
    def init_register_values(self):
        return {
            attr: getattr(self, attr)
            for attr in dir(self)
            if attr.startswith("init_") and attr[5:] in self.REG_MAP
        }

    def trace(self):
        raise NotImplementedError

    def init(self, *args, **kwargs):
        pass

    def create(self, *args, **kwargs):
        self.init(*args, **kwargs)

        self.emu = Uc(UC_ARCH_X86, UC_MODE_64)
        self.emu.mem_map(self.BASE_ADDR, 2 * 1024 * 1024)
        self.emu.mem_write(self.CODE_ADDR, self.asm)
        self.emu.mem_map(self.BASE_STACK, 2 * 1024 * 1024)
        self.rsp = self.RSP_INIT

        for register, value in self.init_register_values.items():
            setattr(self, register[5:], value)

        for address, value in self.init_memory.items():
            self[address] = value

        self.emu.hook_add(UC_HOOK_BLOCK, self.block_hook, begin=self.CODE_ADDR)
        self.emu.hook_add(
            UC_HOOK_INSN, self.syscall_hook, None, 1, 0, UC_X86_INS_SYSCALL
        )
        self.emu.hook_add(UC_HOOK_CODE, self.code_hook)
        self.emu.hook_add(UC_HOOK_INTR, self.intr_hook)

        if self.whitelist is not None:
            self.emu.hook_add(UC_HOOK_CODE, self.whitelist_hook)
        if self.blacklist is not None:
            self.emu.hook_add(UC_HOOK_CODE, self.blacklist_hook)

    def start(self, begin_until=None):
        if begin_until is None:
            begin_until = (self.CODE_ADDR, self.CODE_ADDR + len(self.asm))
        begin, until = begin_until
        self.emu.emu_start(begin, until)

    def run(self):
        hints = ""

        if self.registers_use:
            hints += """
            In this level you will be working with registers. You will be asked to modify
            or read from registers_use.
            """

        if self.dynamic_values:
            hints += """
            We will now set some values in memory dynamically before each run. On each run
            the values will change. This means you will need to do some type of formulaic
            operation with registers_use. We will tell you which registers_use are set beforehand
            and where you should put the result. In most cases, its rax.
            """

        if self.memory_use:
            hints += """
            In this level you will be working with memory. This will require you to read or write
            to things stored linearly in memory. If you are confused, go look at the linear
            addressing module in 'ike. You may also be asked to dereference things, possibly multiple
            times, to things we dynamically put in memory for your use.
            """

        if self.bit_logic:
            hints += """
            In this level you will be working with bit logic and operations. This will involve heavy use of
            directly interacting with bits stored in a register or memory location. You will also likely
            need to make use of the logic instructions in x86: and, or, not, xor.
            """

        if self.stack_use:
            hints += """
            In this level you will be working with the Stack, the memory region that dynamically expands
            and shrinks. You will be required to read and write to the Stack, which may require you to use
            the pop & push instructions. You may also need to utilize rsp to know where the stack is pointing.
            """

        if self.ip_control:
            hints += """
            In this level you will be working with control flow manipulation. This involves using instructions
            to both indirectly and directly control the special register `rip`, the instruction pointer.
            You will use instructions like: jmp, call, cmp, and the like to implement requests behavior.
            """

        if self.multi_test:
            hints += """
            We will be testing your code multiple times in this level with dynamic values! This means we will
            be running your code in a variety of random ways to verify that the logic is robust enough to
            survive normal use. You can consider this as normal dynamic value se
            """

        if self.functions:
            hints += """
            In this level you will be working with functions! This will involve manipulating both ip control
            as well as doing harder tasks than normal. You may be asked to utilize the stack to save things
            and call other functions that we provide you.
            """

        print(
            f"""
            Welcome to {self.__class__.__name__}
            ==================================================

            To interact with any level you will send raw bytes over stdin to this program.
            To efficiently solve these problems, first run it once to see what you need
            then craft, assemble, and pipe your bytes to this program.

            For instance, if you write your assembly code in the file asm.S, you can assemble that to an object file with:
            as -o asm.o asm.S

            Then, you can copy the .text section (your code) to the file asm.bin
            objcopy -O binary --only-section=.text asm.o asm.bin

            And finally, send that to the challenge:
            cat ./asm.bin | /challenge/run

            You can even run all that together as one command:
            as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run

            {hints}
            """
        )

        print(self.description)

        if not self.asm:
            print("Please give me your assembly in bytes (up to 0x1000 bytes): ")
            self.asm = sys.stdin.buffer.read1(0x1000)

            # assuming no hex-only assembly challenges
            if all((c in (string.hexdigits+string.whitespace).encode()) for c in self.asm):
                print("")
                print("ERROR: It looks like your input is hex-ecoded! Please provide")
                print("the actual unencoded bytes!")
                sys.exit(1)

            if all((c in string.printable.encode()) for c in self.asm):
                print("")
                print("WARNING: It looks like your input might not be assembled binary")
                print("code, but actual assembly source. This challenge needs the")
                print("raw binary assembled code as input.")
                print("")

        self.create()

        print("Executing your code...")
        print("---------------- CODE ----------------")
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        for i in md.disasm(self.asm, self.CODE_ADDR):
            print("0x%x:\t%-6s\t%s" % (i.address, i.mnemonic, i.op_str))
        print("--------------------------------------")

        try:
            won = True
            for condition, error in self.trace():
                if not condition:
                    print(f"Failed in the following way: {error}")
                    won = False
                    break
        except Exception as e:
            print(f"ERROR: {e}")
            won = False

        if won:
            print(open("/flag").read())
        else:
            print("Sorry, no flag :(.")
        return won

    def __getattr__(self, name):
        if name in self.REG_MAP:
            return self.emu.reg_read(self.REG_MAP[name])
        if name in self.init_register_values:
            return self.init_register_values[name]
        raise AttributeError

    def __setattr__(self, name, value):
        if name in self.REG_MAP:
            return self.emu.reg_write(self.REG_MAP[name], value)
        return super().__setattr__(name, value)

    def __getitem__(self, key):
        return self.emu.mem_read(key.start, key.stop - key.start)

    def __setitem__(self, key, value):
        self.emu.mem_write(key, value)

    def dump_state(self, uc):
        print(
            f"+--------------------------------------------------------------------------------+"
        )
        print(f"| {'Registers':78} |")
        print(
            f"+-------+----------------------+-------+----------------------+------------------+"
        )

        lines = []
        lc = False
        line = ""
        for reg, const in self.REG_MAP.items():
            if not lc:
                line = "| "
            # skip flag registers
            if not reg.startswith("r"):
                continue

            line += f" {reg.lower():3}  |  0x{getattr(self, reg):016x}  |"

            if not lc:
                line += " "
                lc = True
            else:
                print(f"{line:80} |")
                line = ""
                lc = False

        if line:
            print(f"{line:38} | {' ':20} | {' ':16} |")

        stack_read_amount = self.interrupt_stack_read_length
        memory_read_amount = self.interrupt_memory_read_length

        memory_read_base = self.interrupt_memory_read_base
        multiple_memory_read = False

        if isinstance(memory_read_base, list):
            multiple_memory_read = True

        read_size = 8
        # stack
        print(
            f"+---------------------------------+-------------------------+--------------------+"
        )
        print(f"| {'Stack location':31} | {'Data (bytes)':23} | {'Data (LE int)':18} |")
        print(
            f"+---------------------------------+-------------------------+--------------------+"
        )
        c = 0
        while True:
            read_addr = self.rsp + c * read_size
            if c > stack_read_amount + 10:
                break
            try:
                if (
                    f"{self[read_addr:read_addr + read_size].hex()[::-1]:0>16}"
                    == "0000000000000000"
                    and c > stack_read_amount
                ):
                    break
                print(
                    f"| 0x{read_addr:016x} (rsp+0x{(c * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
                )
            except:
                break

            c += 1

        print(
            f"+---------------------------------+-------------------------+--------------------+"
        )
        print(
            f"| {'Memory location':31} | {'Data (bytes)':23} | {'Data (LE int)':18} |"
        )
        print(
            f"+---------------------------------+-------------------------+--------------------+"
        )
        if multiple_memory_read:
            for baseaddr in memory_read_base:
                for i in range(memory_read_amount):
                    read_addr = baseaddr + i * read_size
                    print(
                        f"| {' ':2} 0x{read_addr:016x} (+0x{(i * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
                    )
                if baseaddr != memory_read_base[-1]:
                    print(
                        "|    -------------------------    |    -----------------    |    ------------    |"
                    )
        else:
            for i in range(memory_read_amount):
                read_addr = memory_read_base + i * read_size
                print(
                    f"| {' ':2} 0x{read_addr:016x} (+0x{(i * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
                )

        print(
            f"+---------------------------------+-------------------------+--------------------+"
        )

    def block_hook(self, uc, address, size, user_data):
        self.bb_trace.append(address)

    def intr_hook(self, uc, intr_num, user_data):
        if intr_num == 3:
            self.dump_state(uc)

    def syscall_hook(self, uc, user_data):
        if self.rax == 0x3C:
            uc.emu_stop()
        else:
            uc.emu_stop()
            raise Exception(f"syscall {self.rax} not supported")

    def code_hook(self, uc, address, size, user_data):
        pass

    def blacklist_hook(self, uc, address, size, user_data):
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        i = next(md.disasm(uc.mem_read(address, size), address))

        if i.mnemonic in self.blacklist:
            uc.emu_stop()
            raise Exception(f"fail: this instruction is not allowed: {i.mnemonic}")

    def whitelist_hook(self, uc, address, size, user_data):
        whitelist = self.whitelist + ["int3"]
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        i = next(md.disasm(uc.mem_read(address, size), address))

        if i.mnemonic not in whitelist:
            uc.emu_stop()
            raise Exception(f"fail: this instruction is not allowed: {i.mnemonic}")

    def get_size_of_insn_at(self, idx):
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        for i, insn in enumerate(md.disasm(self.asm, self.CODE_ADDR)):
            if i == idx:
                return insn.size

ASMBase 클래스는 모든 어셈블리 문제에 공통적으로 필요한 기능과 구조를 제공하며, 각각의 문제 레벨에서는 이 클래스를 상속받아 특정 레벨에 맞는 추가적인 로직이나 변경사항을 구현하게 됨.

ASMBase 클래스 분석

클래스 변수

BASE_ADDR = 0x400000
CODE_ADDR = BASE_ADDR
LIB_ADDR = BASE_ADDR + 0x3000
DATA_ADDR = BASE_ADDR + 0x4000
BASE_STACK = 0x7FFFFF000000
RSP_INIT = BASE_STACK + 0x200000
REG_MAP = {
    "rax": UC_X86_REG_RAX,
    "rbx": UC_X86_REG_RBX,
    "rcx": UC_X86_REG_RCX,
    "rdx": UC_X86_REG_RDX,
    "rsi": UC_X86_REG_RSI,
    "rdi": UC_X86_REG_RDI,
    "rbp": UC_X86_REG_RBP,
    "rsp": UC_X86_REG_RSP,
    "r8": UC_X86_REG_R8,
    "r9": UC_X86_REG_R9,
    "r10": UC_X86_REG_R10,
    "r11": UC_X86_REG_R11,
    "r12": UC_X86_REG_R12,
    "r13": UC_X86_REG_R13,
    "r14": UC_X86_REG_R14,
    "r15": UC_X86_REG_R15,
    "rip": UC_X86_REG_RIP,
    "efl": UC_X86_REG_EFLAGS,
    "cs": UC_X86_REG_CS,
    "ds": UC_X86_REG_DS,
    "es": UC_X86_REG_ES,
    "fs": UC_X86_REG_FS,
    "gs": UC_X86_REG_GS,
    "ss": UC_X86_REG_SS,
}

init_memory = {}
secret_key = random.randint(0, 0xFFFFFFFFFFFFFFFF)

registers_use = False
dynamic_values = False
memory_use = False
stack_use = False
bit_logic = False
ip_control = False
multi_test = False
functions = False

whitelist = None
blacklist = None

interrupt_stack_read_length = 4
interrupt_memory_read_length = 4
interrupt_memory_read_base = DATA_ADDR

주소 관련 상수

  • BASE_ADDR : 기본 주소를 나타냄.코드의 시작 주소나 데이터의 기본 위치를 나타내는 데 사용됨.
  • CODE_ADDR : 코드가 위치할 주소를 나타냄. 해당 주소에서 어셈블리 코드가 시작됨.
  • LIB_ADDR : 라이브러리가 위치할 주소를 나타냄. 추가적인 기능이나 지원 코드가 해당 주소에 위치할 수 있음.
  • DATA_ADDR : 데이터가 저장될 주소를 나타냄. 변수나 버퍼 등의 데이터가 해당 주소에서 시작됨.

스택 관련 상수

  • BASE_STACK : 스택의 기본 위치를 나타냄.
  • RSP_INIT : 초기 스택 포인터(RSP)의 값

    RSP 레지스터는 스택의 현재 위치를 가리키므로, 이 값은 스택의 초기 위치를 나타냄.

레지스터 매핑 변수

  • REG_MAP : 이 딕셔너리는 x86_64 아키텍처의 주요 레지스터와 해당 레지스터를 제어하는 데 필요한 Unicorn 엔진의 상수를 매핑함. 이를 통해 프로그램은 레지스터의 이름을 사용하여 해당 레지스터에 접근하거나 값을 변경할 수 있음.

    Unicorn은 CPU 에뮬레이션 프레임워크로, 다양한 아키텍처의 코드를 에뮬레이션하게 해줌.

메모리 관련 변수

  • init_memory : 프로그램이 시작될 때의 메모리 상태를 나타내는 딕셔너리 타입의 변수임. 주소를 key, 주소의 초기값을 value로 설정됨. 어셈블리 코드의 실행 전후로 메모리의 상태를 비교하는데, 사용될 수 있음.

암호화 및 검증 관련 변수

  • secret_key : 0부터 2^64−1 사이의 랜덤한 정수임. 해당 변수는 특정 암호화 작업이나 사용자의 코드와의 상호작용에서 검증 목적으로 사용될 수 있음.

기능 및 조건 플래그 변수

  • registers_use, dynamic_values, memory_use, stack_use, bit_logic, ip_control, multi_test, functions : Boolean 타입의 변수로 문제 레벨의 특성을 나타냄.

명령어 관련 리스트 변수

  • whitelist : 사용자가 해당 문제에서 사용할 수 있는 어셈블리 명령어나 기능의 목록임. 명시된 명령어나 기능만 사용하도록 제한할 때 사용됨.
  • blacklist : 사용자가 해당 문제에서 사용하면 안 되는 어셈블리 명령어나 기능의 목록임.명시된 명령어나 기능을 사용할 경우, 에러나 경고를 반환할 수 있음.

인터럽트 관련 설정 변수

  • interrupt_stack_read_length : 인터럽트 발생 시 스택에서 읽어야 할 데이터의 길이임. 인터럽트 처리 로직에서 스택의 특정 부분을 읽어 처리할 때 이 길이만큼 읽게 됨.
  • interrupt_memory_read_length : 인터럽트 발생 시 메모리에서 읽어야 할 데이터의 길이임. 특정 메모리 영역에서 이 길이만큼의 데이터를 읽어 인터럽트를 처리함.
  • interrupt_memory_read_base : 인터럽트 처리 시 시작되는 메모리 주소임. 이 주소부터 interrupt_memory_read_length 만큼의 데이터를 읽어 인터럽트를 처리하게 됨.

__init__ 메소드

def __init__(self, asm=None):
    self.asm = asm

    self.emu = None
    self.bb_trace = []

    self.init()

def init(self, *args, **kwargs):
    pass

파라미터

  • def __init__(self, asm=None): : 해당 메소드는 클래스의 인스턴스를 생성 시 호출됨. 선택적으로 asm 매개변수를 받음. 해당 매개변수가 제공되지 않으면 기본값으로 None을 가짐.

변수

  • self.asm : 주어진 asm 매개변수의 값을 가짐. 해당 변수는 문제에 대한 어셈블리 코드를 저장할 수 있음.
  • self.emu : 에뮬레이션을 수행하는 데 사용되는 객체나 도구를 참조되는 변수임. 초기에는 None으로 설정되어 있음.
  • self.bb_trace : 기본 블록의 추적 정보를 저장하는 리스트임. 어셈블리 코드의 실행 중에 방문한 기본 블록의 목록이나 순서를 저장하는 데 사용될 수 있음.
  • self.init() : init 메소드를 호출하는 코드임. 해당 메소드는 추가적인 초기화 작업이 필요 시 오버라이드를 통해 추가할 수 있음.

Property 데코레이터

@property는 메서드를 속성처럼 호출 할 수 있게 하는 데코레이터임.
즉, 데코레이터가 사용된 메서드는 괄호 없이 호출하여 결과를 얻을 수 있음. (ex. 쉽게 생각하면 메서드를 변수처럼 사용)

@property
def description(self):
    raise NotImplementedError

@property
def init_register_values(self):
    return {
        attr: getattr(self, attr)
        for attr in dir(self)
        if attr.startswith("init_") and attr[5:] in self.REG_MAP
    }

description 메서드
description 메서드는 각 문제 레벨의 설명을 제공하기 위해 존재함.
호출 시 NotImplementedError를 발생시킴. 이는 서브클래스에서 반드시 오버라이드를 해야 함을 나타냄.
오버라이드를 통해 문제 설명을 다양화 시킬 수 있음.

init_register_values 메서드
init_register_values 메서드는 각 레지스터의 초기 값을 제공하기 위해 존재함.
현재 객체의 모든 속성을 검사하여 init_로 시작하는 속성 이름을 찾음. 해당 속성 이름의 init_이후 부분이 REG_MAP에 포함되어 있다면, 해당 속성의 이름과 값으로 구성된 딕셔너리를 반환함.

trace 메서드

def trace(self):
    raise NotImplementedError

trace 메서드는 에뮬레이션 중 특정 조건을 검사하고, 그 결과와 관련된 정보를 반환하는데 사용됨.
호출 시 NotImplementedError를 발생시킴. 이는 서브클래스에서 반드시 오버라이드를 해야 함을 나타냄.

init 메서드

def init(self, *args, **kwargs):
    pass

init 메서드는 ASMBase 클래스와 그 서브클래스에서 문제의 초기 상태를 설정하는 데 사용됨. 이 메서드는 각 문제 레벨의 특성과 요구 사항에 따라 다르게 구성될 수 있음.

  1. 확장성
    서브 클래스는 init 메서드를 통해 특정 문제 레벨의 초기 상태를 구성함. 이를 통해 다양한 문제 레벨에 대한 유연한 초기화가 가능해짐.
  2. 재사용성
    필요한 부분만 서브 클래스에서 추가하거나 수정할 수 있음. 이는 코드 중복을 최소화하고 유지 관리를 효율적으로 할 수 있게 함.

기본적으로 init 메서드는 오버라이드를 강제하지 않음. 따라서, 서브 클래스에서 특별한 초기화 작업이 필요하지 않은 경우, 메서드를 그대로 두어 pass로 넘어갈 수 있음.

create 메서드

def create(self, *args, **kwargs):
    self.init(*args, **kwargs)

    self.emu = Uc(UC_ARCH_X86, UC_MODE_64)
    self.emu.mem_map(self.BASE_ADDR, 2 * 1024 * 1024)
    self.emu.mem_write(self.CODE_ADDR, self.asm)
    self.emu.mem_map(self.BASE_STACK, 2 * 1024 * 1024)
    self.rsp = self.RSP_INIT

    for register, value in self.init_register_values.items():
        setattr(self, register[5:], value)

    for address, value in self.init_memory.items():
        self[address] = value

    self.emu.hook_add(UC_HOOK_BLOCK, self.block_hook, begin=self.CODE_ADDR)
    self.emu.hook_add(
        UC_HOOK_INSN, self.syscall_hook, None, 1, 0, UC_X86_INS_SYSCALL
    )
    self.emu.hook_add(UC_HOOK_CODE, self.code_hook)
    self.emu.hook_add(UC_HOOK_INTR, self.intr_hook)

    if self.whitelist is not None:
        self.emu.hook_add(UC_HOOK_CODE, self.whitelist_hook)
    if self.blacklist is not None:
        self.emu.hook_add(UC_HOOK_CODE, self.blacklist_hook)

create 메서드는 에뮬레이션 환경을 초기 설정과 환경 구성을 기능을 담당하는 메서드임.

  1. 초기화(init 메서드 호출)
    init 메서드를 호출하여 문제를 설정하는 데 필요한 초기 상태를 구성함.
  2. Unicorn 엔진 설정
    • self.emu = Uc(UC_ARCH_X86, UC_MODE_64)를 통해 64 bit x86 아키텍처용 Unicorn 엔진 인스턴스를 생성함.
    • 메모리 맵핑 및 초기 어셈블리 코드를 메모리에 쓰기를 수행함.
  3. 스택 설정
    스택의 메모리 위치를 설정하고, 초기 스택 포인터 위치를 설정함.
  4. 레지스터 초기값 설정
    문제의 시작 시 메모리에 필요한 초기 데이터를 설정함.
  5. 메모리 초기값 설정
    문제의 시작 시 메모리에 필요한 초기 데이터를 설정함.
  6. 훅 설정
    • 각종 이벤트(블록 실행, 시스템 호출, 코드 실행, 인터럽트 등)의 처리를 위한 훅을 설정함.
    • whitelistblacklist에 따라, 특정 명령어 또는 기능의 사용을 허용하거나 금지하는 추가 훅을 설정함.

start 메서드

def start(self, begin_until=None):
    if begin_until is None:
        begin_until = (self.CODE_ADDR, self.CODE_ADDR + len(self.asm))
    begin, until = begin_until
    self.emu.emu_start(begin, until)

주어진 주소 범위에서 에뮬레이션을 시작하는 기능을 수행함.
만약 주소 범위(begin_until)가 제공되지 않으면 전체 코드 세그먼트를 대상으로 에뮬레이션을 시작함.

run 메서드

def run(self):
        hints = ""

        if self.registers_use:
            hints += """
            In this level you will be working with registers. You will be asked to modify
            or read from registers_use.
            """

        if self.dynamic_values:
            hints += """
            We will now set some values in memory dynamically before each run. On each run
            the values will change. This means you will need to do some type of formulaic
            operation with registers_use. We will tell you which registers_use are set beforehand
            and where you should put the result. In most cases, its rax.
            """

        if self.memory_use:
            hints += """
            In this level you will be working with memory. This will require you to read or write
            to things stored linearly in memory. If you are confused, go look at the linear
            addressing module in 'ike. You may also be asked to dereference things, possibly multiple
            times, to things we dynamically put in memory for your use.
            """

        if self.bit_logic:
            hints += """
            In this level you will be working with bit logic and operations. This will involve heavy use of
            directly interacting with bits stored in a register or memory location. You will also likely
            need to make use of the logic instructions in x86: and, or, not, xor.
            """

        if self.stack_use:
            hints += """
            In this level you will be working with the Stack, the memory region that dynamically expands
            and shrinks. You will be required to read and write to the Stack, which may require you to use
            the pop & push instructions. You may also need to utilize rsp to know where the stack is pointing.
            """

        if self.ip_control:
            hints += """
            In this level you will be working with control flow manipulation. This involves using instructions
            to both indirectly and directly control the special register `rip`, the instruction pointer.
            You will use instructions like: jmp, call, cmp, and the like to implement requests behavior.
            """

        if self.multi_test:
            hints += """
            We will be testing your code multiple times in this level with dynamic values! This means we will
            be running your code in a variety of random ways to verify that the logic is robust enough to
            survive normal use. You can consider this as normal dynamic value se
            """

        if self.functions:
            hints += """
            In this level you will be working with functions! This will involve manipulating both ip control
            as well as doing harder tasks than normal. You may be asked to utilize the stack to save things
            and call other functions that we provide you.
            """

        print(
            f"""
            Welcome to {self.__class__.__name__}
            ==================================================

            To interact with any level you will send raw bytes over stdin to this program.
            To efficiently solve these problems, first run it once to see what you need
            then craft, assemble, and pipe your bytes to this program.

            For instance, if you write your assembly code in the file asm.S, you can assemble that to an object file with:
            as -o asm.o asm.S

            Then, you can copy the .text section (your code) to the file asm.bin
            objcopy -O binary --only-section=.text asm.o asm.bin

            And finally, send that to the challenge:
            cat ./asm.bin | /challenge/run

            You can even run all that together as one command:
            as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run

            {hints}
            """
        )

        print(self.description)

        if not self.asm:
            print("Please give me your assembly in bytes (up to 0x1000 bytes): ")
            self.asm = sys.stdin.buffer.read1(0x1000)

            # assuming no hex-only assembly challenges
            if all((c in (string.hexdigits+string.whitespace).encode()) for c in self.asm):
                print("")
                print("ERROR: It looks like your input is hex-ecoded! Please provide")
                print("the actual unencoded bytes!")
                sys.exit(1)

            if all((c in string.printable.encode()) for c in self.asm):
                print("")
                print("WARNING: It looks like your input might not be assembled binary")
                print("code, but actual assembly source. This challenge needs the")
                print("raw binary assembled code as input.")
                print("")

        self.create()

        print("Executing your code...")
        print("---------------- CODE ----------------")
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        for i in md.disasm(self.asm, self.CODE_ADDR):
            print("0x%x:\t%-6s\t%s" % (i.address, i.mnemonic, i.op_str))
        print("--------------------------------------")

        try:
            won = True
            for condition, error in self.trace():
                if not condition:
                    print(f"Failed in the following way: {error}")
                    won = False
                    break
        except Exception as e:
            print(f"ERROR: {e}")
            won = False

        if won:
            print(open("/flag").read())
        else:
            print("Sorry, no flag :(.")
        return won

주어진 에뮬레이션 환경에서의 결과를 검사하고, 성공 여부를 반환됨. 또한 사용자들을 위한 힌트와 가이드를 제공함.

  1. 다양한 조건에 따라 hints 변수에 힌트 메시지를 삽입함.

    • self.registers_use : 레지스터 사용에 관한 힌트
    • self.dynamic_values : 동적 값(랜덤 값) 사용에 관한 힌트
    • self.memory_use : 메모리 사용에 관한 힌트
    • self.bit_logic : 비트 논리 연산에 관한 힌트
    • stack_use: 스택 사용에 관한 힌트
    • ip_control: 제어 흐름 조작에 관한 힌트
    • multi_test: 다양한 동적 값으로의 테스트에 관한 힌트
    • functions: 함수 사용에 관한 힌트
  2. 제출된 어셈블리 바이트 코드 검증 로직

    if not self.asm:
     print("Please give me your assembly in bytes (up to 0x1000 bytes): ")
     self.asm = sys.stdin.buffer.read1(0x1000)
    
     # assuming no hex-only assembly challenges
     if all((c in (string.hexdigits+string.whitespace).encode()) for c in self.asm):
         print("")
         print("ERROR: It looks like your input is hex-ecoded! Please provide")
         print("the actual unencoded bytes!")
         sys.exit(1)
    
     if all((c in string.printable.encode()) for c in self.asm):
         print("")
         print("WARNING: It looks like your input might not be assembled binary")
         print("code, but actual assembly source. This challenge needs the")
         print("raw binary assembled code as input.")
         print("")
  3. 제출한 어셈블리 바이트 코드를 디스어셈블하여 출력

    print("Executing your code...")
    print("---------------- CODE ----------------")
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    for i in md.disasm(self.asm, self.CODE_ADDR):
     print("0x%x:\t%-6s\t%s" % (i.address, i.mnemonic, i.op_str))
    print("--------------------------------------")
  4. 결과 검증 로직

    try:
     won = True
     for condition, error in self.trace():
         if not condition:
             print(f"Failed in the following way: {error}")
             won = False
             break
    except Exception as e:
     print(f"ERROR: {e}")
     won = False
    if won:
     print(open("/flag").read())
    else:
     print("Sorry, no flag :(.")
    return won

    trace 메서드를 사용하여 어셈블리 코드의 실행을 검증하고, 실패 조건 또는 예외가 발생한 경우 해당 오류 메시지를 출력하여 won 값을 False로 설정함.

__getattr__ 메서드

def __getattr__(self, name):
    if name in self.REG_MAP:
        return self.emu.reg_read(self.REG_MAP[name])
    if name in self.init_register_values:
        return self.init_register_values[name]
    raise AttributeError

__getattr__ 메서드는 Python의 매직 메서드로 객체의 속성에 접근하려 할 때 존재하지 않은 경우 호출되는 메소드임.
해당 클래스에선 레지스터 이름인 속성에 접근 시 레지스터의 값을 반환하거나 초기 레지스터 값들 중에서 값을 반환하도록 도와줌.

만약 두가지 경우에도 존재하지 않으면, AttributeError 속성 에러가 발생됨.

__setattr__ 메서드

 def __setattr__(self, name, value):
    if name in self.REG_MAP:
        return self.emu.reg_write(self.REG_MAP[name], value)
    return super().__setattr__(name, value)

__setattr__ 메서드는 Python의 매직 메서드로 객체의 속성에 값을 할당하려 할 때 호출되는 메소드임.
해당 클래스에선 레지스터 이름으로 직접 속성에 값을 할당할 수 있게 해줌.

super().__setattr__(name, value)를 호출하는 이유
__setattr__ 메서드를 오버라이드를 하면, 해당 메서드 내에서 직접 속성에 값을 할당하려고 하면 __setattr__ 메서드가 재귀적으로 호출됨. 해당 문제가 무한 반복되어 동작을 제대로 수행 못 함.
이를 방지하기 위해 부모 클래스의 __setattr__ 동작을 수행하여 속성에 안전하게 값을 할당함.

__getitem__ 메서드

def __getitem__(self, key):
    return self.emu.mem_read(key.start, key.stop - key.start)

__getitem__ 메서드는 Python의 매직 메서드로 객체의 항목에 대한 접근을 정의하는 메소드임.
주로 시퀀스나 딕셔너리와 같은 컬렉션 타입에서 항목을 가져오기 위해 사용됨.

해당 클래스에선 주어진 메모리 주소 범위에서 데이터를 읽어오는 기능을 수행함.
ex). obj[0x1000:0x1010]의 경우, 메모리 주소 0x1000 ~ 0x1010까지의 데이터를 가져옴.

__setitem__ 메서드

def __setitem__(self, key, value):
    self.emu.mem_write(key, value)

__setitem__ 메서드는 Python의 매직 메서드로 객체의 항목에 값을 할당할 때 사용되는 메소드임.
주로 리스트나 딕셔너리와 같은 컬렉션 타입에서 특정 항목에 값을 설정하기 위해 사용됨.

해당 클래스에선 주어진 메모리 주소에 데이터를 기록하는 기능을 수행함.

dump_state 메소드

def dump_state(self, uc):
    print(
        f"+--------------------------------------------------------------------------------+"
    )
    print(f"| {'Registers':78} |")
    print(
        f"+-------+----------------------+-------+----------------------+------------------+"
    )

    lines = []
    lc = False
    line = ""
    for reg, const in self.REG_MAP.items():
        if not lc:
            line = "| "
        # skip flag registers
        if not reg.startswith("r"):
            continue

        line += f" {reg.lower():3}  |  0x{getattr(self, reg):016x}  |"

        if not lc:
            line += " "
            lc = True
        else:
            print(f"{line:80} |")
            line = ""
            lc = False

    if line:
        print(f"{line:38} | {' ':20} | {' ':16} |")

    stack_read_amount = self.interrupt_stack_read_length
    memory_read_amount = self.interrupt_memory_read_length

    memory_read_base = self.interrupt_memory_read_base
    multiple_memory_read = False

    if isinstance(memory_read_base, list):
        multiple_memory_read = True

    read_size = 8
    # stack
    print(
        f"+---------------------------------+-------------------------+--------------------+"
    )
    print(f"| {'Stack location':31} | {'Data (bytes)':23} | {'Data (LE int)':18} |")
    print(
        f"+---------------------------------+-------------------------+--------------------+"
    )
    c = 0
    while True:
        read_addr = self.rsp + c * read_size
        if c > stack_read_amount + 10:
            break
        try:
            if (
                f"{self[read_addr:read_addr + read_size].hex()[::-1]:0>16}"
                == "0000000000000000"
                and c > stack_read_amount
            ):
                break
            print(
                f"| 0x{read_addr:016x} (rsp+0x{(c * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
            )
        except:
            break

        c += 1

    print(
        f"+---------------------------------+-------------------------+--------------------+"
    )
    print(
        f"| {'Memory location':31} | {'Data (bytes)':23} | {'Data (LE int)':18} |"
    )
    print(
        f"+---------------------------------+-------------------------+--------------------+"
    )
    if multiple_memory_read:
        for baseaddr in memory_read_base:
            for i in range(memory_read_amount):
                read_addr = baseaddr + i * read_size
                print(
                    f"| {' ':2} 0x{read_addr:016x} (+0x{(i * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
                )
            if baseaddr != memory_read_base[-1]:
                print(
                    "|    -------------------------    |    -----------------    |    ------------    |"
                )
    else:
        for i in range(memory_read_amount):
            read_addr = memory_read_base + i * read_size
            print(
                f"| {' ':2} 0x{read_addr:016x} (+0x{(i * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
            )

    print(
        f"+---------------------------------+-------------------------+--------------------+"
    )

ASMBase 클래스의 현재 상태(레지스터 값, 메모리 내용 등)를 출력하는 메서드임.
주로 디버깅이나 상태 추적을 위해 사용됨.

  1. 레지스터 상태 출력
  • 모든 레지스터에 대해 그 이름과 해당 레지스터의 현재 값을 출력함.

    for reg, const in self.REG_MAP.items():
      # skip flag registers
      if not reg.startswith("r"):
          continue
      line += f" {reg.lower():3}  |  0x{getattr(self, reg):016x}  |"
  • self.REG_MAP을 사용하여 레지스터 이름과 상수 값 획득

    for reg, const in self.REG_MAP.items():
  1. 스택 내용 출력
  • 스택의 위치, 해당 위치에서 읽은 바이트 데이터, 그리고 Little-Endian으로 해석된 정수값을 출력

    print(
      f"| 0x{read_addr:016x} (rsp+0x{(c * read_size):04x}) | {self[read_addr:read_addr+1].hex()} {self[read_addr+1:read_addr+2].hex()} {self[read_addr+2:read_addr+3].hex()} {self[read_addr+3:read_addr+4].hex()} {self[read_addr+4:read_addr+5].hex()} {self[read_addr+5:read_addr+6].hex()} {self[read_addr+6:read_addr+7].hex()} {self[read_addr+7:read_addr+8].hex()} | 0x{self[read_addr:read_addr + read_size][::-1].hex():0>16} |"
    )
  • 출력은 테이블 형식으로, 스택의 주소, 해당 주소에서의 바이트 값, 그리고 바이트 값을 정수로 해석한 값으로 구성

    print(
      f"+---------------------------------+-------------------------+--------------------+"
    )
    print(f"| {'Stack location':31} | {'Data (bytes)':23} | {'Data (LE int)':18} |")
    print(
      f"+---------------------------------+-------------------------+--------------------+"
    )
  1. 메모리 내용 출력
  • self.interrupt_memory_read_base가 리스트 형식인지 확인하여 다수의 메모리 위치에서 데이터를 읽기
    if isinstance(memory_read_base, list):

Hook 메서드

  1. block_hook 메서드
    블록이 실행될 때마다 해당 블록의 주소를 bb_trace 리스트에 추가함.

  2. intr_hook 메서드
    인터럽트가 발생할 때 실행됨. 만약 인터럽트 번호가 3일 경우, dump_state 메서드를 호출하여 현재 상태를 출력함.

  3. syscall_hook 메서드
    시스템 콜이 발생될 때 실행됨. rax 레지스터의 값이 0x3C인 경우 에뮬레이션을 중단함.
    만약 0x3C이 아닌 경우, 시스템 콜에 대한 예외가 발생됨.

  4. code_hook 메서드
    코드를 실행될 때마가 호출되는 메서드임. 서브 클래스에서 오버라이드를 하여 사용할 수 있음.

  5. blacklist_hook 메서드
    실행된 명령어가 블랙리스트에 있는지 확인함. 블랙리스트에 있으면 에뮬레이션을 중단하고 예외를 발생시킴.

  6. whitelist_hook 메서드
    실행된 명령어가 화이트리스트에 있는지 확인함. 화이트리스트에 없으면 에뮬레이션을 중단하고 예외를 발생시킴.

해당 메서드들은 주로 에뮬레이션 중에 특정 조건에 따라 에뮬레이션의 흐름을 제어하거나 정보를 수집하는데 사용됨.

get_size_of_insn_at 메서드

for i, insn in enumerate(md.disasm(self.asm, self.CODE_ADDR)):
    if i == idx:
        return insn.size

주어진 idx에 해당하는 어셈블리 명령어의 바이트 크기를 반환함.
만약 해당 인텍스의 명령어를 찾지 못하면 아무것도 반환하지 않음(None를 반환)

ASMLevel1 클래스

class ASMLevel1(ASMBase):
    """
    Set register
    """

    registers_use = True

    @property
    def description(self):
        return f"""
        In this level you will work with registers_use! Please set the following:
        * rdi = 0x1337
        """

    def trace(self):
        self.start()
        yield self.rdi == 0x1337, f"rdi was expected to be 0x1337, but was {hex(self.rdi)} instead"

레지스터 사용 힌트

registers_use = True

해당 클래스 변수를 설정함으로 레지스터 사용하였다는 힌트를 사용자에게 출력하였음.

def run(self):
    hints = ""

    if self.registers_use:
        hints += """
        In this level you will be working with registers. You will be asked to modify
        or read from registers_use.
        """

해당 출력은 부모 클래스인 ASMBase 클래스의 run 메소드에서 선언된 것을 알 수 있음.

정답 검증 로직

def trace(self):
    self.start()
    yield self.rdi == 0x1337, f"rdi was expected to be 0x1337, but was {hex(self.rdi)} instead"

self.start 메소드를 호출하여 제출한 어셈블리 바이너리를 실행함.
이후, rdi 레지스터 값이 0x1337인지 확인하여 (True, rdi was expected to be 0x1337, but was [현재 rdi의 값] instead)를 출력하게 됨.

try:
    won = True
    for condition, error in self.trace():
        if not condition:
            print(f"Failed in the following way: {error}")
            won = False
            break
except Exception as e:
    print(f"ERROR: {e}")
    won = False

부모 클래스인 ASMBase 클래스에선 run 메소드에서 trace 메소드를 사용하여 결과 검증 로직을 구현되어 있음.

ASMLevel1 클래스의 trace 메소드가 yield 키워드로 구현한 이유는 정답의 조건이 다른 레지스터들도 존재하는 경우, run 메소드에서 for 문을 통하여 next 메소드를 통해 결과를 호출하여 모든 조건의 결과를 받아올 수 있기 때문임.

복사했습니다!