1. 개요
Obfuscation / 難讀化프로그램의 수행과 결과를 변경하지 않으면서, 사람이 의미를 파악하기 어렵고 역설계에 오랜 시간이 소모되도록 하는 행위, 또는 그러한 기술을 말한다.
주로 디컴파일, 리버스 엔지니어링, 코드 유출 및 취약점 분석 방지 등을 목적으로 행해진다.
obfuscation의 반댓말은 deobfuscation이라고 불린다.[1]
uglification으로도 불린다.
2. 종류
2.1. 소스 코드 난독화
프로그래밍 언어로 된 원본 소스 코드 자체를 사람이 읽기 어렵게 만드는 행위. 프로그래밍 언어는 기본적으로 사람이 코드를 쉽게 이해하도록 돕는 다양한 장치를 마련하고 있으나, 소스 코드 난독화를 통해 이러한 장치를 대부분 제거할 수 있다. 런타임에 로딩되는 실제 프로그램이 소스 코드 그 자체여야 하기에 주로 인터프리터 언어의 배포에 사용된다.리네임을 사용하지 않는 경우 주석, 화이트스페이스 등 불필요한 lexical elements를 제거하고 선형으로 압축하는 format obfuscation이 가장 기본적이다. 특히 주석 등은 리버싱 시 가장 유용한 정보 중 하나이기에 주석조차 제거하지 않는 건 가장 치명적인 수준이다. 이렇게 해도 코드 포매터 등을 통해 읽는 데 거의 문제가 없는 수준까지 되돌릴 수 있어 단독으로는 잘 쓰이지 않고, 후술할 여러 방법과 같이 사용된다.
대부분의 경우 식별자를 lexical scope에 따라 겹치지 않게
a, b, DJd3PO9d 등 무작위한 이름으로 rename한다. rename refactoring과 동일한 메커니즘이지만, 단회 시행되는 것이 아니라 노출되지 않은 내부 식별자까지 AST 순회를 하며 전부 수정하게 된다. minification을 겸하는 경우 식별자명을 최대한 짧은 길이로 수정하는 편.이외에도 바이너리 난독화와 마찬가지로 임베딩된 상수, 정적 문자열 등의 data obfuscation을 추가로 진행하기도 한다. 단순히 식별자 이름이랑 코드 흐름만 난독화해 봤자 코드 중간중간에
gCcYW941Q("특수문자를 하나 이상 포함해야 합니다")
같은 게 그대로 보인다면 해당 부근이 비밀번호 폼 검증 코드라는 걸 누구나 알 수 있어 말짱 도루묵이기 때문. 예를 들면 문자열로 "토큰 생성일"을#!syntax javascript
(function(){var z=Array.prototype.slice.call(arguments),F=z.shift();return z.reverse().map(function(u,I){return String.fromCharCode(u-F-4-I)}).join('')})(11,53679)+(function(){var g=Array.prototype.slice.call(arguments),A=g.shift();return g.reverse().map(function(G,N){return String.fromCharCode(G-A-48-N)}).join('')})(41,49464,122,53449)+(function(){var z=Array.prototype.slice.call(arguments),e=z.shift();return z.reverse().map(function(t,m){return String.fromCharCode(t-e-60-m)}).join('')})(28,51157,49545)
처럼 나타내는 식이다. 다만 이런 식의 data obfuscation은 결국 static한 값으로 평가되는 것이 목적이므로 boundary만 잘 찾을 수 있다면 원본에 가까운 deobfuscation이 가능할 때도 있다. 가령 위 코드를 그대로 복사해 브라우저 콘솔창에 그대로 붙혀넣기하면
"토큰 생성일"이라는 원본 문자열이 정확하게 평가된다.필요에 따라 난독화를 더욱 어렵게 만들기 위해 dead code 삽입, function inlining, code duplication 등을 수행하기도 한다. 이 경우 트리가 아니라 flow 단위의 조작이 필요한데, 정적 분석만으로는 모든 flow를 예측하기 힘들어 간단한 깊이만 구현되기도 한다.
2.2. 바이너리 난독화
고수준의 프로그래밍 언어를 바이트코드, 또는 기계어로 컴파일하며 디버깅 심볼 정보 등을 삭제하는 방식이 자주 쓰인다. 이 경우 컴파일러가 같이 생성하는 맵핑 파일만으로 원본에 가까운 복원이 가능하다.다만 이 경우 맵핑 정보가 없어도 디컴파일 자체는 일정 수준까지 가능하기 때문에, 본격적인 난독화를 수행할 경우 어셈블리 수준의 instruction fuzzing을 수행하는 것이 일반적이다. 특히 전체 소스를 CFG로 만든 다음 불규칙적인 점프 명령어를 삽입하여 실행 흐름을 수정하는 방법이 있다. 어셈블리 수준 난독화의 경우 소스보다 본격적인 인라이닝이나 unwinding이 들어가는 편.
또한 바이너리 난독화는 런타임 성능 저하의 원인이 될 수 있다. 주로 사용되는 기법인 Deadcode, MBA, Flattning 과 같은 기법들은 정적 분석기의 분석을 어렵게 만들기도 하지만, 그 인스트럭션을 실행하는 CPU 입장에서는 인스트럭션 캐시의 효율 저하 또는, 분기 예측기의 성능을 저하시키는 원인으로 작용한다.
3. 자동화
사람이 일일이 코드를 난독화하는 것은 시간 소모가 크고 개발에 좋지 않기 때문에 대부분 CI 파이프라인 또는 배포시에 자동으로 수행된다.4. 기타
암호화와는 다르다. 암호화는 원본과 동일하게 복호화가 가능한 대칭 연산이지만 난독화는 일반적으로 심볼 맵 등이 없다면 난독화된 소스 자체만으로는 원본으로의 복호화가 불가능하다. 즉, 비대칭 연산이다.DRM과도 차이가 있다. DRM은 동적 콘텐츠 자체의 복제를 방지하기 위한 기술이지 정적 데이터인 코드를 조작하는 개념이 아니다. 대부분 난독화는 CI 등 단계에서 배포 이전에 이루어진다.
minification과 매우 비슷하지만 목적이 다르다. minification은 주로 웹에서, 스크립트의 배포 용량을 줄이기 위한 과정으로 소스 코드를 최대한 압축하는 것이 유일한 목적으로, 소스를 읽을 수 없게만 하면 되는 난독화와는 다르다. 예를 들어 경우에 따라 난독화 과정에서 혼란을 주기 위한 dead code, stub code 등이 삽입되기도 하는데, minification은 반대로 사용하지 않는 모든 코드를 반드시 걸러낸다(dead code elimination). 다만 현실적으로 두 결과가 거의 비슷하기 때문에 혼용되어 쓰이기도 하는 편.
[1] 난독화를 해제한다는 의미다.