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

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

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

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

아이다에서 문자열 찾기를 통해 유저 인풋을 얻는곳을 찾아낸다 (Fn+12). Xref 사용해 서 문자열이 참조된곳으로 간다. 디컴파일해서 (F5) 코드를 분석해보자. 코드를 보면 코드가 유저 인풋을 받은후:
- 각 바이트를 왼쪽으로 3비트 회전 (ROL 3)
- 그 결과에 XOR 0x20
- 이후 사용자 입력이 일부 영역에 덮어쓰기 됨
- 마지막으로 전체 버퍼에 XOR 0x10
이 시점에서 사용자 입력은 전체 데이터 중 일부만을 덮어쓰며, 이후 다시 전체 XOR 연산이 적용된다. 즉, 입력값이 플래 그 생성에 직접적으로 사용된다고 보기 어렵다.

쨋든 위 코드를 보면 코드가 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]
이다.

그래서 코드는 이 헥스 값들을 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 바이트코드 자체를 해석하는 것이 핵심이었다. 끄 읕!
