Obfuscator-LLVM Software Protection for the Masses

SeungHeon Kim·2022년 5월 2일
0

개요

리버스 엔지니어링과 관련된 소프트웨어 보안은 몇 년에 걸쳐서 지금까지도 활발히 연구되고 있는 분야이다. 그리고 이 연구는 굉장히 실용적이다. 실제로 악의적인 수정, 변조 또는 리버스 엔지니어링으로부터 소프트웨어를 보호하는 것은 매우 어럽다. 이 논문에서는 LLVM을 기반으로 하는 소프트웨어 난독화 프로토타입 도구를 제시하고 그에 대해 논의할 것이다. 우리의 툴은 LLVM IR 코드에서 작동하고, 그 중 일부는 오픈소스이며 무료로 사용할 수 있다. 이 접근 방식은 언어에 구애받지 않고 하드웨어 아키텍처와 독립적이라는 이점이 있다. 우리의 현재 프로토 타입은 기본 명령어 대체, 코드 난독화, 제어 흐름 병합, 절차 병합뿐만 아니라 제어 흐름 병합 메코니즘에 대한 코드 변조 방지 알고리즘을 지원한다.

I. INTRODUCTION

소프트웨어를 보호하는 것이 어려운 작업인 주된 이유는 공격자가 에뮬레이션, 디버거에서 실행, 디스어셈블, 디컴파일 등을 수행할 수 있는 소프트웨어에 대한 무제한 접근과 같은 비정상적인 권한을 가지고 있기 때문입니다. 따라서 공격자는 중요한 데이터를 포함해, 메모리의 데이터를 읽을 수 있고, 암호화 키 같은 소프트웨어 실행 중에 값을 수정해서 소프트웨어를 불법적으로 복사하는 등의 목적으로 분석한다.

컴퓨터의 형태가 모바일 및 클라우드로 발전함에 따라서 이 문제를 해결하는 것은 개인 정보 보호, 보안 측면에서 실제로 엄청난 영향을 미칠 것이다. 지금까지 우리는 실행 중인 코드의 무결성과 기밀성을 보호하는 문제가 암호화 및 소프트웨어 난독화에서 공격을 받았다는 것을 관찬했습니다. 그래서 고정키 구현과 같은 알고리즘을 인스턴스화 할 수 있는 소위 화이트 박스 암호화 기술이 제안되기도 했다. 하지만 이런 제안은 대부분 무산되었다. 이 화이트 박스 암호화의 문제는 공격자가 응용 프로그램에서 알고리즘을 들어올리는 이른바 코드 리프팅이 문제인다. 이 경우 공격자 알고리즘을 해제해서 자신의 제어 하에 원래 키를 알 필요도 없고, 알고리즘을 이해할 필요도 없다.

소프트웨어 난독화는 보다 실용적일 수 있다. 암호화 난독화로 얻을 수 있는 것 정도의 공격 저항 수준을 기대하는 것이 아니라, 적의 리버스 엔지니어링 비용을 증가시켜서, 공격자를 포기하기를 기대한다.

일반적인 보안 소프트웨어 접근 방식은 USB 토큰, HSM(하드웨어 보안 모듈), 스마트 카드, 원격 인증과 같은 하드웨어에 의존한다. 하지만 이런 솔루션들은 무시할 수 없는 비용이 들고, 모바일 또는 클라우트 컴퓨팅과 같은 환경이나 연결되지 않은 상태에서 구현하기가 항상 쉽지만은 않다. 그래서 휴대전화나 태블릿에서 실행되는 소프트웨어를 보호하기 위해서는 데이터나 알고리즘을 숨길 수 있는 소프트웨어 전용 보호 메커니즘에 의존해야 한다. 그러므로 리버스 엔지니어링에 대한 소프트웨어 저항을 증가시켜야 한다.

학계와 산업계 모두 소프트웨어 보안 향상을 목표로 하는 기술을 제안했다. 이러한 기술에는데이터 인코딩, 변수 분할 및 병합, 배열 접기 밎 병합, 정적 값을 런타임 생성 값으로 변환, 혼합 부울 산술 변환이 포함된다. 이 기술은 리버스 엔지니어링 비용을 증가시켰지만, 불가능하도록 만들지는 못한다.

변조 방지 소프트웨어는 일반적으로 실행 중인 코드를 보호하거나 프로그램을 통한 제어 흐름이 예상된 코드인지 확인함으로써 코드 조작을 감지하는 내장 무결성 검사를 사용한다. 하지만 이런 기법을 독립 실행형 응용 프로그램을 보호하는데 사용하는 경우, 기대 체크섬 값과 반응 레커니즘이 응용 프로그램 자체에서 하드 코딩되어 분석 및 변경될 수 있기 때문에 보안은 다소 제한된다. 자체 검사 소프트웨어는 디버거를 연결시켜 자동으로 공격당할 수 있다.

이것은 완전히 리버스 엔지니어들을 막을 수 없더라도, 이것들이 모두 리버스 엔지니어링의 비용을 크게 증가시킨다는 것을 다시 한번 강조한다.

A. Our Contributions

이 논문에서, 우리는 LLVM 컴파일 중간 단계에 구현된 난독화 코드 변환 세트, Obfuscator-LLVM(ollvm)을 제시한다. 구현된 보호 기술의 대부분은 무료로 사용이 가능한 오픈 소스 형태로 제공된다. ollvm은 현재 거의 완전히 언어 및 플랫폼에 독립적이다. 명령어 대체 , 가짜 제어 흐름 삽입, 기본 블록 분할, 제어 흐름 병합, 코드 변조 방지 메커니즘의 절차 병합 및 삽입을 구현할 수 있다. 하지만 마지막 두 기술은 현재 ollvm에서 무료로 사용할 수 없다.

시중에서 구할 수 있는 상용 도구 외에도 C/C++와 같은 전통적인 프로그래밍 언어로 작성된 프로그램의 난독화를 지원할 수 있는 무료 도구는 많지 않다. 최근 눈에 띄는 예로는 Tigress, LOCO, Sandmark가 있다.

II. CODE TRANSFORMATIONS

다음에서는 LLVM 컴파일 제품군을 빠르게 검토하고 코드 다양화에 대해 논의하며 지금까지 ollvm에서 구현한 모든 난독화 기술에 대해 설명합니다.

A. The LLVM Compilation Suit

컴파일 제품군의 목적은 대상에 대해 올바르게 실행될 수 있는 코드를 생성하는 것이다. CPU 컴파일러는 고수준 언어로 쓰인 상위 코드를 저수준 언어인 어셈블러 코드로 변환하고, 어셈블러가 이를 번역해 오브젝트 코드로 변환한다. 컴파일러는 상위 코드가 올바를 구문을 가지고 있는지 확인하고 가능한 한 효율적으로 어셈블리 코드를 정확하게 생성해야 한다. 또한 컴파일러는 해당 아키텍저의 규칙을 준수하는 코드를 생성해야 한다.

LLVM은 크리스 래트너가 처음 작성한 오픈 소스 컴파일 제품군으로 임의의 프로그래밍 언어의 정적 컴파일 및 동적 컴파일을 모두 지원할 수 있는 현대의 정적-단일 할당(SSA) 기반 컴파일 전략을 제공하는 것을 목표로 한다. LLVM은 현재 대규모 개발자 커뮤니티에 의해 지원되고 있고, 오늘날 GNU컴파일러 컬렉션의 주요 오픈 소스 경쟁자로 간주되고 있다.

LLVM은 프론트 엔드, 옵티마이저, 백 엔드로 구성된다. 프론트엔드는 소스 코드를 분석하고 정확성을 확인하고, 옵티마이저라고도 하는 미들엔드에 전달될 이 코드의 중간 표현(IR)을 구축하는 일을 담당한다. LLVM에서 지원하는 두 가지 주요 프론트 엔드는 C/C++ 및 Objective-C를 컴파일 할 수 있는 clang과 GNU Compiler Collection 파서를 기반으로 하는 프론트엔드이다. 그 결과로, 중간표현은 LLVM 생태계에서 IR 코드라는 이름의 범용 의사 어셈블리 언어이며, 최적화 도구 또는 백엔드에 제공될 수 있다. 어떤 IR 코드가 주어지면, 옵티마지어는 중복 코드, 인라인 함수, 데드 루프 삭제, 제어 흐름 그래프 단순화 등을 담당한다. 이렇게 옵티마이저를 거친 최적화된 IR코드는 백엔드로 전달되고, 백엔드는 해당 아키텍처에 대한 효율적인 어셈블러 코드를 생성한다.

우리가 지금까지 구현한 난독화 및 변조 방지 메커니즘의 대부분은 중간 단계에서 발생한다. 이 접근 방식은 언어에 구애받지 않고 해당 아키텍처와 독립적이라는 사실과 함께 몇 가지 장점을 가지고 있다. 이러한 방법은 가능한 모든 보호를 구현하지 못하지만, 소스 및 타겟 하드웨어에 구애받지 않는 장점이 단점을 압도한다. 아키텍처별 보호 기능이 각 백엔드에서 구현될 수 있음은 말할 필요도 없다.

B. Bringing Code Diversification

일반적인 컴파일 과정은 결정론적이다. 즉, 동일한 소스코드를 두번 컴파일 하면 동일한 머신 코드가 생성된다. ollvm이 오픈 소스이기 때문에, 적이 추적할 수 없는 새로운 실행 파일을 만들기 쉬워지지만, 이것은 약간의 작업 없이는 불가능하다.

우리가 구현한 모든 코드 변환은 어떤 방식으로든 무작위화된다.

C. Instructions Substitution

명령어 대체 방식은 가장 간단한 난독화 기법일 수 있다. 산술 연산자나 부울 연산자와 같은 표준 이진 연산자를 기능적으론 같지만 더 복잡한 명령어 시퀸스로 대체한다. 명령어 대체는 ollvm의 -mllvm -sub 옵션으로 이용할 수 있다. 현재, 덧셈과 뺄셈, 부울 연산 AND(&), OR(|), XOR(^)를 지원한다.

이런 종류의 난독화는 오히려 간단하고 생성된 코드를 다시 최적화함으로써 쉽게 파훼될 수 있기 때문에 리버스 엔지니어링의 cost를 많이 올리지 못한다. 그래서 우리의 명령어 대체가 모든 LLVM 최적화가 이루어진 후에 실행되는 이유다. 그럼에도 불구하고, 주어진 연산자에 대해 여러 등가식들을 생성할 수 있다는 사실을 감안할 때, 무작위로 하나를 선택하는 것은 코드 다양화를 가져오는 쉬운 방법이다. 그리고 XOR과 같은 대칭 암호에서 특정 기계 명령 패턴을 자동으로 검색하는 작업을 더 어렵게 만들 수 있다.

다음은 ollvm에서 명령어 치환 표이다. 여기에서 r 은 무작위 pseudo-value이다.

D.Bogus Control Flow Insertion

가짜 제어 흐름 삽입은 원래 기본 블록을 가리키거나 조건부 점프 블록으로 다시 루프하는 가짜 기본 블록을 가리키는 조건부 점프구문을 추가하여 함수의 제어 흐름 그래프를 수정하는 것으로 구성됩니다. 불명확한 술어, 즉 항상 같은 값으로 평가되지만 정적으로 리버스 엔지니어링하기 어려운 표현식은 런타임에 원래 런타임에 원래 기본블록만 실행회도록 해야 한다. 불투명 술어는 또한 최적화 프로그램이 데드 코드를 식별하여 결과 호출 그래프를 단순화할 수 없도록 해야한다. 이 변환은 -mllvm -bcf 컴파일 플래그를 통해 사용할 수 있고, 삽입 밀도, 반복 횟수 등을 포함하여 다양한 방식으로 매개변수화될 수 있다. 가짜 제어 흐름 삽입의 효과는 Fig 1의 C 소스코드를 가지고 생각할 때 가장 잘 알 수 있을 것이다. Fig 2에는 수정 전후의 함수 f()의 제어 흐름 그래프가 그려져 있다. 추가된 코드의 대부분이 죽어서 실행되지 않을지라도 제어흐름 그래프는 상당히 복잡해진 것을 알 수 있다.

E.Control Flow Flattening and Basic-Block Splitting

코드 평면화의 이면에 있는 아이디어는 모든 조건부 및 루프 구조를 제고해서 함수의 제어흐름 그래프를 깨는 것이다.

0개의 댓글