SimpleVM

A tiny bytecode VM validating the key; solving it means understanding the instruction set and running it backwards.

2026.08.18 Reversing original post

파일을 보니 ElF 파일이다. 칼리로 가서 열어 주었는데 안된다. Strace 로 먼저 확인해보자. strace는 “프로그램이 커널에 무슨 부탁을 하는지”를 그대로 보여주는 X-ray 같은 도구라서, 동적 분석이 막히는 순간 가장 먼저 써보는 도구이다 (strace = system call trace). 어쨋든 strace 로 보면 access denied 가 뜨는걸 볼수있다. 그래서 걍 루트로 실행 해준다. sudo ./SimpleVM. 이 문제는 겉보기에는 간단한 VM(Simple Virtual Machine) 기반의 키 검증 프로그램처럼 보인다. 프로그램은 사용자로부터 입력을 받아 내부 VM을 실행하고, 올바른 입력일 경우에만 CORRECT 메시지와 함께 종료된 다.

Figure 1

한번더 실행한후 아무것도 입력하지 않고 2번째 터미널에서 sudo gcore (PID) 를 입력해 코어파일을 덤프했다. (pid 는 ps-al 로 알아내면도됨).

Figure 2

혹시라도 모를까봐 코어 파일을 덤프하는 이유에 대해서 설명하겠다 (내가 몰라서 삽질했으니까) . 코어 파일을 덤프하는 이유는 환경 삽질을 피하기 위해서다. input :이 단계에 도달하려면, 권한 아키텍처 터미널 실행 방식 전부 맞아야 한다. 하나라도 틀리면 아예 분석 시작 불가이다. 그래서 그냥 실행 중인 메모리를 잡아다 보는거다. gcore는 “실행 중 상태를 그 대로 보존”하기 때문이다 (지금 이 프로그램이 무슨 데이터를 가지고 어디까지 실행됐는지).

Figure 3

이제 아이다로 분석을 시작해보자.

Figure 4

아이다에서 문자열 찾기를 통해 유저 인풋을 얻는곳을 찾아낸다 (Fn+12). Xref 사용해 서 문자열이 참조된곳으로 간다. 디컴파일해서 (F5) 코드를 분석해보자. 코드를 보면 코드가 유저 인풋을 받은후:

  1. 각 바이트를 왼쪽으로 3비트 회전 (ROL 3)
  2. 그 결과에 XOR 0x20
  3. 이후 사용자 입력이 일부 영역에 덮어쓰기 됨
  4. 마지막으로 전체 버퍼에 XOR 0x10

이 시점에서 사용자 입력은 전체 데이터 중 일부만을 덮어쓰며, 이후 다시 전체 XOR 연산이 적용된다. 즉, 입력값이 플래 그 생성에 직접적으로 사용된다고 보기 어렵다.

Figure 5

쨋든 위 코드를 보면 코드가 0x804B0A0 영역부터 XOR 하는걸 알수있다. 헥스 덤프로 가져오자.

ar = [4, 4, 4, 4, 4, 4, 4, 4, 4, 69, 69, 196, 196, 4, 4, 101, 68, 228, 8, 196, 4, 228, 68, 228, 37, 228, 4, 228,
      37, 68, 68, 228, 200, 196, 36, 228, 68, 228, 68, 228, 36, 228, 37, 68, 68, 228, 166, 196, 68, 228, 68, 228, 192,
      228, 68, 228, 37, 68, 68, 228, 228, 196, 100, 228, 68, 228, 161, 228, 100, 228, 37, 68, 68, 228, 141, 196, 132,
      228, 68, 228, 64, 228, 132, 228, 37, 68, 68, 228, 104, 196, 164, 228, 68, 228, 228, 228, 164, 228, 37, 68, 68,
      228, 11, 196, 196, 228, 68, 228, 6, 228, 196, 228, 37, 68, 68, 4, 36, 101, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
      4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]

이다.

Figure 6

그래서 코드는 이 헥스 값들을 1차적으로 복호화된 코드의 2차 복호화를 한 후, 이제서야 키 값 체크 루틴으로 들어간다. 이후 코드의 흐름을 따라가면, 일반적인 분기 구조가 아닌 switch-case 기반의 VM 인터프리터 형태임을 확인할 수 있다. 루

  • Case 2: 플래그 문자 생성
  • Case 6: 메모리 XOR
  • Case 7: 비교
  • Case 9: 조건 분기
  • Case 10: 점프
  • Case 11: 종료

VM 코드 전체를 복호화하여 확인해보면, 조건 분기와 관계없이 Case 2(opcode 2)가 선형적으로 반복 등장한다. 즉, 이 VM은 사용자 입력을 실제로 검증하기 위한 구조가 아니라, 플래그를 VM 코드 내부에 숨기기 위한 장치에 가깝다. 따라 서 VM을 실제로 에뮬레이션할 필요 없이, 복호화된 VM 코드에서 Case 2에 사용되는 즉시값(immediate value)들만 순 서대로 추출하면 플래그를 복원할 수 있다. 여기 위에 로직을 포함한 파이썬 코드다.

byte = [4, 4, 4, 4, 4, 4, 4, 4, 4, 69, 69, 196, 196, 4, 4, 101, 68, 228, 8, 196, 4, 228, 68, 228, 37, 228, 4, 228,
        37, 68, 68, 228, 200, 196, 36, 228, 68, 228, 68, 228, 36, 228, 37, 68, 68, 228, 166, 196, 68, 228, 68, 228, 192,
        228, 68, 228, 37, 68, 68, 228, 228, 196, 100, 228, 68, 228, 161, 228, 100, 228, 37, 68, 68, 228, 141, 196, 132,
        228, 68, 228, 64, 228, 132, 228, 37, 68, 68, 228, 104, 196, 164, 228, 68, 228, 228, 228, 164, 228, 37, 68, 68,
        228, 11, 196, 196, 228, 68, 228, 6, 228, 196, 228, 37, 68, 68, 4, 36, 101, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
        4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
        4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]


def rol3(value):
    return ((value << 3) | (value >> (8 - 3))) & 0xff


input = b'123456789'

# Stage 1: 1차 디코딩 (ROL + XOR)
for i in range(200):
    byte[i] = rol3(byte[i]) ^ 0x20

# Stage 2: 사용자 입력 덮어쓰기 (미끼 단계)
for i in range(len(input)):
    byte[i] = input[i]

# Stage 3: 2차 디코딩 (VM opcode 복원)
for i in range(200):
    byte[i] ^= 0x10

# Stage 4: VM 실행을 위한 패딩 영역 확보
byte.extend([0x00] * 50)

# 실제 VM 인터프리터가 해석하는 바이트코드는 byte[10:]부터 시작한다.
# 즉 버퍼의 앞부분에만 덮어써지며 유저 인풋은 사용되지 않는다는 뜻이다.
code = byte[10:]

i = 0
d = []
flag = ""

while i < len(code):

    opcode = code[i] ^ 0x10

    if opcode == 2:
        decoded = code[i + 2] ^ 0x10
        d.append(decoded)
        i += 3

    elif opcode in (6, 7):
        i += 3

    elif opcode in (9, 10):
        i += 2

    elif opcode == 11:
        i += 1

    else:
        break

print("Flag Is: ", end="")

for i in range(0, len(d) - 1, 2):
    ch = d[i] ^ d[i + 1]
    print(chr(ch), end="")

이 문제는 겉보기에는 사용자 입력을 검증하는 VM 문제처럼 보이지만, 실제로는 VM 내부의 즉시값을 조합하여 플래그 를 생성하는 구조였다 따라서 입력값 분석이나 조건 분기 추적보다, VM 바이트코드 자체를 해석하는 것이 핵심이었다. 끄 읕!

Figure 7