보통 프로그래밍 코드는 레시피 (소스 코드) 가 있고, 그것을 요리한 결과물 (컴파일된 실행 파일) 이 있습니다. 해커들은 이 요리된 음식을 다시 레시피로 되돌려서 (역컴파일) 원본을 알아내려 합니다.
이 논문은 에를랑의 실행 파일 (BEAM 바이트코드) 이 가진 독특한 성질을 이용해, 요리된 음식은 맛 (기능) 은 그대로인데, 레시피를 다시 만들어내는 과정을 극도로 어렵게 만드는 방법들을 소개합니다.
2. 주요 기법 4 가지 (비유로 설명)
① 레고 블록의 숨겨진 연결고리 (Opcode 의존성)
비유: 레고 블록을 조립할 때, 보통은 'A 블록을 B 블록에 붙이면 C 블록이 나옵니다'라고 정해져 있습니다. 하지만 에를랑의 실행 파일에서는 A 블록을 붙일 때, B 블록의 '색깔'이나 '무게' 같은 눈에 보이지 않는 속성을 이용해 C 블록이 어떻게 변할지 결정합니다.
현실: 분석가는 "이 명령어 다음에 뭐가 올까?"라고 추측하지만, 실제로는 컴파일러가 미리 계산해 둔 '숨겨진 규칙'에 따라 달라집니다. 해커는 이 숨겨진 규칙을 이용해 분석가가 코드를 따라가는 길을 막아섭니다.
② 메신저로 만든 미로 (수신 루프, Receive-based Loops)
비유: 보통 프로그램은 "1 번을 하고, 2 번을 하고, 3 번을 한다"처럼 직선으로 흐릅니다. 하지만 에를랑은 **메시지 (편지)**를 주고받는 방식입니다. 이 논문은 "A 편지를 받으면 B 를 하고, B 를 하면 다시 A 편지를 보내고, C 편지를 받으면 D 를 한다"는 식으로 편지함에 메시지를 쌓아놓고 그걸로 반복 작업을 시키는 미로를 만듭니다.
현실: 분석가는 "여기서 반복문 (Loop) 이 있겠지?"라고 생각하지만, 실제로는 편지함의 상태에 따라 코드가 뱅글뱅글 돌고 있습니다. 마치 미로에서 출구를 찾으려는데 벽이 계속 움직이는 것처럼 분석을 어렵게 만듭니다.
③ 변하는 요리 재료 (가변성, Mutability)
비유: 에를랑은 원래 "요리된 요리는 절대 변하지 않는다 (불변성)"는 철학이 있습니다. 하지만 이 논문은 요리된 접시 (튜플) 를 직접 칼로 찢어서 재료를 바꿔 넣는 방법을 보여줍니다.
현실: 이 방법은 매우 빠릅니다 (C 언어처럼). 하지만 분석가가 이 코드를 다시 레시피 (소스 코드) 로 되돌리려 하면, "어? 이 요리는 원래 변하지 않아야 하는데 왜 변했지?"라고 혼란을 겪습니다. 분석가가 다시 코드를 만들면, 원래의 빠른 속도가 사라지고 매우 느린 버전이 되어버립니다. 즉, 코드를 해독하는 순간 성능이 폭락하게 만들어 재사용을 막는 것입니다.
④ 스스로를 고치는 로봇 (자기 수정 코드)
비유: 이 로봇은 스스로의 설계도 (소스 코드) 를 읽어서, "내 다리를 좀 더 길게 고쳐야겠다"라고 생각하면 실시간으로 설계도를 고치고, 그 새 설계도로 자신을 다시 조립합니다.
현실: 분석가가 코드를 찍어내려 (디컴파일) 하면, 그 순간의 모습만 찍히게 됩니다. 하지만 코드가 실행되는 순간, 그 모습은 사라지고 새로운 모습으로 변해버립니다. 마치 카메라로 찍으려 하면 사진이 계속 변하는 유령과 같습니다.
3. 왜 이것이 중요한가요? (결론)
이 논문의 핵심 메시지는 다음과 같습니다.
"에를랑의 실행 파일은 단순히 코드가 나열된 것이 아니라, 에러를 체크하는 검사관 (Validator) 과 가상 머신 (VM) 이 서로 다른 규칙으로 움직이는 복잡한 세계입니다. 해커는 이 두 세계 사이의 '간극'을 이용해, 기능은 정상적으로 작동하지만 분석가는 미친 듯이 헤매게 만드는 기법을 개발할 수 있습니다."
한 줄 요약: 에를랑 코드를 해독하려는 사람은 마치 맛은 그대로인데, 레시피를 다시 쓰려면 미로 속을 헤매고, 요리 도구를 갈아치우고, 심지어 요리사가 스스로 변신하는 주방을 분석해야 하는 상황에 처하게 됩니다.
이 연구는 소프트웨어 보호 기술을 강화하고, 반대로 해커들이 어떻게 코드를 숨기는지 이해하여 더 강력한 보안 도구를 만드는 데 기여합니다.
제시된 논문 "Erlang Binary and Source Code Obfuscation"은 Erlang 프로그래밍 언어와 BEAM 가상 머신 (VM) 환경에서의 코드 난독화 (Obfuscation) 기법을 심층적으로 분석한 연구입니다. 저자 Gregory Morse 와 Tamás Kozsik 은 Erlang 의 고수준 의미론 (Semantics) 과 하위 수준 실행 모델 간의 간극을 활용하여 역공학, 디컴파일, 재컴파일을 어렵게 만드는 다양한 기법을 제시합니다.
다음은 이 논문의 문제 정의, 방법론, 주요 기여, 결과 및 의의에 대한 상세한 기술적 요약입니다.
1. 문제 정의 (Problem)
역공학의 취약점: Erlang 프로그램은 소스 코드, 추상 구문 트리 (AST), BEAM 어셈블리, BEAM 바이트코드를 거치며 변환됩니다. 일반적인 디컴파일러는 이러한 변환 과정에서 생성된 구조적 가정 (예: 제어 흐름의 단순성, 불변성 등) 에 의존하여 코드를 복원하려 합니다.
검증기 (Validator) 와 실행 환경의 괴리: BEAM 바이트코드는 컴파일러의 엄격한 검증 과정을 거치지만, BEAM 로더와 VM 은 특정 검증 조건을 우회하거나 구현 의존적인 (implementation-dependent) 동작을 허용할 수 있습니다.
난독화의 한계: 기존의 난독화 기법들은 주로 무작위 손상 (arbitrary corruption) 에 의존하는 경향이 있으나, 이는 VM 이 실행을 중단하거나 예외를 발생시킬 수 있습니다. 본 논문은 의미론적 무결성 (Semantics-preserving) 을 유지하면서, 도구 체인과 런타임 간의 표현적 간극을 악용하여 역공학을 방해하는 기법을 탐구합니다.
2. 방법론 (Methodology)
저자들은 Erlang 도구 체인의 각 변환 단계 (Source → AST → Assembly → Bytecode) 와 BEAM VM 의 실행 메커니즘을 분석하여 다음과 같은 기법들을 도출했습니다.
A. BEAM 바이트코드 및 어셈블리 수준 기법
Opcode 의존성 조작 (Opcode Sequence Dependencies):
바이너리나 튜플 생성 시, 크기를 결정하는 연산자와 데이터 로드 연산자 사이의 의존 관계를 흐리게 만듭니다.
디컴파일러가 바이너리 데이터 로드 명령어의 수를 예측하기 어렵게 하거나, 제어 흐름을 섞어 정적 분석을 방해합니다.
수신 (Receive) 기반 루프 인코딩:
Erlang 의 receive 문과 메시지 큐 (Mailbox) 를 활용하여 재귀나 반복 구조를 시뮬레이션합니다.
loop_rec, wait, remove_message 등의 명령어를 사용하여 단일 진입/탈출점이 아닌 다중 진입/다중 탈출 (Multi-entry/Multi-exit) 구조를 생성합니다. 이는 일반적인 제어 흐름 그래프 (CFG) 복원 알고리즘을 혼란스럽게 만듭니다.
비정형 제어 흐름 (Irregular Control-Flow):
try-catch 및 receive 블록에서 컴파일러가 생성하지 않는 비정상적인 점프 구조를 도입합니다.
예외 처리 및 종료 명령어 간의 순서 제약을 위반하지 않으면서도, 복잡한 제어 흐름을 만들어 디컴파일러가 코드 중복을 제거하거나 구조를 파악하는 것을 어렵게 합니다.
변수 및 레지스터 상태 조작:
BEAM 검증기는 초기화되지 않은 변수를 허용하지 않지만, VM 실행 시 특정 레지스터 (예: x0 대신 x1 사용) 나 스택 변수의 상태를 조작하여 검증기를 우회하고 실행 시에는 정상 작동하도록 하는 패치 기법을 제시합니다.
검증기는 통과하지만 VM 로더가 최적화 과정에서 실수로 제거하는 '죽은 코드'를 악용하는 사례도 분석했습니다.
B. 성능 및 가변성 기반 기법 (Efficiency via Mutability)
불변성 위반을 통한 성능 변조: Erlang 은 불변 (Immutable) 데이터 구조를 지향하지만, BEAM 어셈블리 수준에서 set_tuple_element 와 같은 명령어를 직접 사용하여 튜플을 가변적으로 수정할 수 있습니다.
난독화 전략: 디컴파일러가 생성한 코드는 setelement 를 사용하여 데이터를 복사하므로 O(n) 의 복잡도를 가지지만, 원본 BEAM 코드는 O(1) 의 성능을 냅니다. 이는 재컴파일 시 성능이 급격히 저하되도록 하여 코드의 실용성을 떨어뜨리는 난독화 효과를 냅니다.
C. 자기 수정 코드 (Self-Modifying Code)
Erlang 의 핫 스왑 (Hot-swapping) 기능을 활용하여 런타임 중 모듈을 동적으로 재로드하고, 함수 단위의 소스 코드를 변경하여 다시 실행하는 기법을 제시합니다.
이는 정적 분석 도구에게 프로그램의 정체성이 고정되어 있다는 가정을 무너뜨려, 실행 중인 코드가 디스크에 있는 코드와 다르게 변형될 수 있음을 보여줍니다.
3. 주요 기여 (Key Contributions)
Erlang 난독화 공간의 체계적 분류: 소스, AST, 어셈블리, 바이트코드 수준에서의 난독화 기법을 분류하고, 각 단계에서의 변환 경계 (Transformation Boundaries) 가 어떻게 악용될 수 있는지를 매핑했습니다.
검증기 vs 실행기 간극의 실증: BEAM 검증기가 허용하지 않지만 VM 이 실행할 수 있는 (또는 로더가 잘못 최적화하는) 코드 패턴을 구체적으로 증명했습니다. 이는 "정의되지 않은 행동 (Undefined Behavior)"이 아닌 "구현 의존적 가정"을 이용한 난독화의 가능성을 보여줍니다.
제어 흐름 복원 (Control-Flow Recovery) 에 대한 도전:receive 기반 루프와 비정형 제어 흐름을 통해 기존 디컴파일러가 가정하는 단순한 CFG 구조를 무너뜨리는 사례를 제시했습니다.
성능 기반 난독화: 가변성 (Mutability) 을 악용하여 디컴파일된 코드의 성능을 의도적으로 저하시키는 새로운 난독화 패러다임을 제안했습니다.
4. 결과 및 발견 (Results)
디컴파일러의 실패 시나리오: BEAM 어셈블리에서 생성된 비정형 제어 흐름 (다중 진입/탈출 루프) 은 기존 디컴파일러가 고수준 Erlang 소스로 복원하는 것을 실패하게 하거나, 과도한 코드 중복을 초래하게 만듭니다.
성능 격차: 튜플을 가변적으로 수정하는 BEAM 어셈블리 코드는 O(1) 성능을 내지만, 이를 디컴파일하여 재컴파일하면 O(n) 성능으로 저하되어 실제 사용이 불가능해집니다.
동적 코드 변경의 가능성: 런타임 중 모듈을 재로드하고 소스 코드를 변형하는 '자기 수정' 코드가 가능하며, 이는 정적 분석을 무력화합니다.
검증 우회: BEAM 파일의 바이너리 패치를 통해 검증기가 통과하지 못하는 레지스터 사용 패턴을 VM 에서는 정상적으로 실행되도록 만들 수 있음을 확인했습니다.
5. 의의 및 결론 (Significance)
역공학 관점: Erlang 의 BEAM 생태계는 소스 코드 수준 이상의 적대적 표면 (Adversarial Surface) 을 가지고 있습니다. 신뢰할 수 있는 Erlang 디컴파일러를 개발하려면 단순한 구문 복원을 넘어, BEAM 검증기, 로더, 메일박스 모델, 런타임 시스템의 의미론과 제약 조건을 이해해야 합니다.
난독화 관점: 가장 효과적인 난독화는 무작위 손상이 아니라, 고수준 도구의 가정과 BEAM 실행 모델 간의 허용 가능한 간극을 정교하게 활용하는 데서 나옵니다.
보안 시사점: Erlang 의 동적 로딩, 핫 스왑, 메시지 전달 기반의 비구조적 제어 흐름은 전통적인 정적 분석 도구들에게 큰 도전 과제를 제기하며, 이를 고려한 새로운 보안 분석 도구의 필요성을 강조합니다.
요약하자면, 이 논문은 Erlang 의 강력한 동적 기능과 BEAM VM 의 내부 동작 원리를 역이용하여, 역공학자가 코드를 이해하거나 재사용하는 비용을 극대화하면서도 프로그램의 실행은 정상적으로 유지하는 정교한 난독화 기법들을 체계적으로 제시했습니다.