[PAPER] (State of) The Art of War:Offensive Techniques in Binary Analysis
https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=7546500
이 논문에서는 여러 분석 기술들을 구현한 바이너리 분석 프레임워크를 제안한다. 제안한 바이너리 분석 프레임워크가 효율적인 바이너리 취약점 분석 기술임을 평가하기 위해 DARPA에서 생성한 데이터셋을 통해 평가한다. 이 프레임워크는 오픈소스이다.
바이너리 분석의 중요성
저수준 언어에서의 취약점
offensive analysis 기술에서 직면한 문제
다른 기술과 비교했을 때 바이너리 분석 기술의 적용 가능성은 불분명하게 된다.
현대 운영체제의 복잡성과 정확하고 즉각적으로 애플리케이션의 환경과 상호작용하는 것을 모델링하는 어려움으로 비교를 위한 요인을 만들기 어렵다. 비교할 수 있어도 시스템을 서로 다른 구현 요소와 평가 데이터셋으로 진행하는 경향이 있다.
offensive 분석 기술에서 직면한 문제 해결을 위한 angr 제작
논문 목적
논문 내용
기여
연구자들은 자동화된 바이너리 분석 기술을 몇 년 동안 연구해왔고 이 필드에 많은 발전이 있었지만, 자동화된 바이너리 분석 시스템은 실제로 개발하고 배포하는데 어려움이 있어왔다. 실제 소프트웨어에는 자동화된 분석을 만드는데 기술적 한계가 있기 때문이다.
이 섹션에서는, 자동화된 분석의 어려움과 어떻게 DARPA의 CGC 대회가 서로 다른 분석 접근 방법들을 비교하는데 의미있는걸 제공하는지 설명한다.
버그는 항상 동일하게 만들어지지 않고, 분석으로 발견한 버그는 재연이 불가능할 수 있다. 애플리케이션 실행 처음부터 어떤게 취약점을 트리거하기 위해 필요한 것인지 찾아봐야 하는 분석이 필요할 때도 있다. 분석 시 버그가 특정 모듈에서 찾을 수 있다는걸 알아도, 해당 모듈을 실행할 방법을 알 수 없다면 크래시를 재연할 수 없게 된다.
재연 가능한 입력을 만드는 분석 기술은 분석하고자 하는 코드에 도달할 방법을 이해해야 한다. 버그에 대한 입력을 트리거할 수 없으면, 이는 false positive가 된다. 즉, 탐지 했던 흐름은 실제 취약점을 나타내지 않는 것이다.
어떤 분석은 의미상 의미있는 방법을 프로그램에서 찾을 능력이 부족할 수 있다.
동적 분석 시 코드를 추적해야 할 수 있는데, 왜 코드가 실행 됐는지나 어떤 입력이 애플리케이션을 그렇게 동작하게 했는지 이해하지 못할 수도 있다. 프로그램의 동작에 대한 입력의 특정 바이트를 알 수 있는 심볼릭 분석이 더 의미있는 이해가 될 수 있다.
분석하는 프로그램에 대한 의미론적인 통찰력을 위해서는 많은 데이터가 필요하다. 의미론적으로 통찰력 있는 동적 분석은 프로그램의 특정 분기를 취하기 위해 유지해야 하는 조건이 있을 수 있다.
재연성과 높은 의미론적 이해를 가지려는 분석은 확장성에 문제가 있을 수 있다. 애플리케이션의 모든 의미있는 정보를 보유하는 것은 모든 가능한 조건 아래에서 프로그램을 실행하는 자원을 처리할 능력이 필요하다. 이러한 분석은 확장성이 없고 다른 모든 잠재적으로 발견 가능한 취약점을 찾을 수 있는 가능성과 그런 정보를 포기해야 한다.
environment model
분석에서 높은 의미론적 이해를 위해서는 애플리케이션의 환경과 상호작용할 수 있어야 하는데, 현대 운영체제는 복잡하다. Linux에는 삼백 개가 넘는 시스템 호출이 있어서 시스템을 완전히 분석하려면 모든 시스템 호출의 영향을 고려해서 설계해야 한다.
int main(void){
char buf[32];
char *data = read_string();
unsigned int magic = read_number();
// difficult check for fuzzing
if(magic == 0x31337987){
// buffer overflow
10 memcpy(buf, data, 100);
}
if(magic < 100 && magic % 15 == 2 && magic % 11 == 6){
// Only solution if 17; safe
16 memcpy(buf, data, magic);
}
// symbolic execution will suffer from path explosion
int count = 0;
for(int i=0; i < 100; i++){
if(data[i] == 'Z')
count++;
}
if(count >= 8 && count <= 16){
// buffer overflow
30 memcpy(buf, data, count*20);
}
return 0;
}
바이너리 분석의 여러 문제를 보여주기 위해, Listing 1에서 여러 취약점이 존재하는 프로그램 예시를 보여준다.
정적 분석
memcpy() 함수가 세 번 호출되는데, 데이터 양에 따라 정적 분석으로는 모든 세 개의 memcpy() 호출에서 잠재적인 버그라고 할 수도 있는데, 16 라인에서 buffer overflow가 불가능한 정보가 없을 수 있기 때문이다. 또한 정적 분석 시 각 버그 위치에서 버그를 트리거할 입력을 제공할 수 없을 수 있다.
동적 분석
fuzzing과 같은 동적 분석 기술로는 찾은 버그를 트리거할 입력을 만들 수 있는 장점이 있다. 하지만 간단한 fuzzing 기술로는 얕은 버그만 찾거나 필요한 입력을 코드에 주지 못할 수도 있다. 동적 분석으로 라인 10의 버그를 찾기 힘든데, 특정 조건을 만족하는 입력이 필요하기 때문이다. 라인 30의 경우 fuzzing 기술은 랜덤 입력으로 버그를 트리거할 입력을 찾을 수 있다.
Dynamic Symbolic Execution (DSE)
라인 10의 버그를 찾을 수 있는, 많은 가능한 입력을 한 번에 찾을 수 있는 추상 데이터 모델이다.
하지만 동적 심볼릭 기술은 강한 반면 “경로 폭발 문제”(path explosion)가 있다. 경로 폭발 문제는 경로 수가 각 분기마다 기하급수적으로 증가해서 다루기 힘들게 되는 것이다.
심볼릭 실행으로 라인 10과 16의 취약점을 찾을 수 있는데, 라인 30의 취약점은 찾을 수 없을 수도 있다. 라인 30에는 버그를 트리거하지 않는 너무 많은 잠재적인 경로가 존재하기 때문이다.
DARPA Cyber Grand Challenge
2013년 10월, DARPA는 DARPA Cyber Grand Challenge를 발표했다. CGC는 모든 참가자들이 자율적인 프로그램이 되는 경기에서 전세계 팀들이 서로 맞붙는다.
CGC 목적
모든 시스템은 제공한 소프트웨어에서 취약점을 식별하고 익스플로잇하고 패치 해야 한다.
DARPA CGC 샘플 사용 목적
DARPA CGC에서 제공하는 바이너리는 복잡성 범위가 다양한데, 4KB부터 10MB 크기까지 다양하고 구현된 기능이 간단한 에코 서버, 웹 서버부터 이미지 처리라이브러리까지 다양하다. DARPA는 대회에서 사용한 모든 바이너리를 오픈소스로 공개하고 취약점에 대한 PoC 익스플로잇과 write-up도 공개했다.
DARPA CGC 샘플은 간단한 환경 모델이 바이너리 분석 기술을 정확히 구현하고 평가할 수 있어서 이 논문에서 평가 시 데이터셋으로 사용했다.
처리하는 애플리케이션에 따라 서로 다른 오펜시브 바이너리 분석 기술을 사용한다. 예를 들면, 서로 다른 도메인의 데이터를 분석할 수 있고, 애플리케이션의 서로 다른 레벨을 활용할 수 있다.
다음 두 섹션에서는 현재 분석 기술들을 조사했고, 이후 논문 내용으로는 몇 가지 분석을 골라서 깊이있게 평가한다.
정적 분석 기술
정적 기술은 프로그램을 실행하지 않고 분석하는 것이다.
정적 분석을 프로그램 속성을 그래프로 모델링하는 두 가지 패러다임으로 분류했다.
정적 취약점 식별 기술의 두 가지 단점
prerequisite
control flow 복원 방법
CFG 복원 시 문제점: indirect jump
간접 점프는 바이너리의 제어 흐름이 레지스터나 메모리 장소로 이동할 때 발생한다.
간접 점프의 타겟은 여러 요인에 의해 다를 수 있다. 간접 점프는 다음의 여러 카테고리로 나눌 수 있다.
특정 코드에서의 계산으로 애플리케이션에 의해 점프의 타겟이 결정된다. 계산은 레지스터나 메모리에 있는 값에 의존될 수 있다. 이러한 예로는 점프 테이블이 있는데, 애플리케이션은 레지스터나 메모리의 값을 메모리에 저장된 점프 테이블의 인덱스를 알기 위해 사용한다. 인덱스로부터 타겟 주소를 읽고 그 주소로 점프한다.
간접 점프는 애플리케이션의 문맥에 따라 이루어질 수 있다. 표준 C 라이브러리의 qsort()를 예로 들 수 있다. qsort() 함수는 전달된 값을 비교하기 위해 callback을 사용하고, qsort() 내 basic block의 점프 타겟은 callback 함수를 제공하는 caller에 따라 결정된다.
객체 지향 언어에서, 객체의 다형성은 virtual tables의 함수 포인터로 구현된 가상 함수 사용이 필요하다. 이 함수 포인터는 런타임에 참조되어 점프 타겟을 결정한다. 즉, 함수 caller에 의해 전달된 객체의 타입에 따라 점프 타겟이 결정된다.
CFG 복원의 목적
CFG 생성 위해 가능한 많은 간접 점프의 타겟을 해결하는 것
CFG 복원 분석 두 가지 속성
얼마나 점프 타겟이 잘 해결되었는지에 따라 두 가지 속성이 있다.
CFG 복원 기술은 모든 잠재적인 제어 흐름 이동이 생성된 그래프에서 나타날 때 sound하다고 한다. 잠재적인 basic block의 타겟이 빠지게 되면, 타겟으로 하는 block은 CFG 복원 알고리즘에서 볼 수 없고 직접이나 간접 점프에서도 빠지게 된다.
soundness는 바이너리에서 간접 점프 타겟 식별의 true positive 비율로 간주할 수 있다.
완전한 CFG 복원은 가능한 제어 흐름 이동을 보여주는 모든 edge를 가지는 CFG를 만든다.
CFG 분석이 completeness 면에서 실수를 하면, 실제로 존재하지 않는 edge를 가질 수 있다. completeness는 간접 점프 타겟 식별의 false positive 비율의 역으로 생각할 수 있다.
어떤 취약점은 프로그램의 그래프 분석 시 발견할 수도 있다.
프로그램 속성 그래프(control-flow graph, data-flow graph, control-dependence graph)는 소프트웨어에서 취약점을 식별하는데 사용할 수 있다. 기술은 control-flow나 data-dependency graph 내 노드들에서 보이는 버그의 모델을 만들고 애플리케이션에서 이 모델을 식별하는데 의존한다. 이 기술은 취약한 소스 코드의 복사본을 찾는데 맞춰져 있어서 이미 존재하는 취약점에 대한 지식을 활용할 수 있다. 하지만, 이 논문에서는 완전히 새로운 취약점을 찾는데 집중했다.
정적 분석 접근 중 하나로 Value-Set 분석(VSA)이 있다. VSA는 프로그램의 특정 지점에서 프로그램 상태(메모리나 레지스터의 값)의 초과 근사치를 식별한다. 이는 간접 점프나 가능한 메모리 write 연산의 타겟에 대한 가능한 타겟을 이해하는데 사용할 수 있다.
읽고 쓰는 메모리 접근 패턴을 분석해서 변수와 버퍼 장소를 바이너리에서 식별할 수 있다. 이후 복원된 변수와 버퍼 장소는 overlapping 버퍼를 찾는데 분석될 수 있고, overlapping 버퍼는 buffer overflow 취약점의 원인이 될 수 있다.
동적 분석
특정 입력에서 동작에 대한 프로그램 실행을 검사하는 분석이다.
동적 기술은 concrete execution과 symbolic execution으로 분류할 수 있다. 이 기술들은 입력이 높은 재연 가능성이 있지만 의미론적 통찰력 측면에서는 다양하다.
동적 concrete execution은 최소화된 instrument된 환경에서 프로그램을 실행하는 개념이다.
이 분석은 하나의 경로 레벨에 대해 특정 입력을 주었을 때 프로그램 내 어떤 경로가 되는지에 대해 알아보는 것이다. 따라서 동적 concrete execution에는 사용자에 의한 테스트 케이스가 필요하다.
fuzzing은 취약점을 찾기 위한 dynamic concrete execution 방법 중 하나로, 크래시를 트리거하기 위해 여러 형태의 입력을 주는 동적 기술이다.
input
입력은 하드코딩된 규칙으로 생성되며, 특정 입력으로 애플리케이션에 크래시가 발생하면 해당 입력은 버그를 트리거한 것으로 간주된다. 다른 방법으로는 입력을 랜덤하게 변형하며 생성할 수 있다.
test case
fuzzer는 교묘하게 조작된 테스트 케이스가 없으면, fuzzer는 프로그램의 깊이 없는 기능을 제외하고는 아무것도 훈련하지 못하게 된다.
Coverage-based fuzzing
code-coverage-based fuzzer는 코드가 많이 실행될수록, 취약한 코드를 실행할 기회가 더 높다는 통찰력을 기반으로 하고 있다. 따라서 실행되는 코드 양을 최대화하는 입력을 만드는 것이 목표이다.
American Fuzzy Lop(AFL)는 code coverage 매트릭을 원칙으로 하고 있고, 이 fuzzer로 많은 취약점들을 발견해왔다.
coverage-based fuzzing은 타겟 애플리케이션에 대한 의미론적인 통찰력이 부족한 단점이 있는데, 이는 아직 실행하지 않은 코드를 탐지할수는 있어도 어떤 입력이 해당 코드를 실행했는지에 대한 이해는 없음을 의미한다.
Taint-based fuzzing
taint-based fuzzer는 장래의 실행에서 입력의 어떤 부분을 변형해야 하는지 이해하기 위해 애플리케이션이 어떻게 입력을 처리하는지 분석하는 것이다. 정적 기술에 데이터 기반 복원과 같은 taint 추적을 결합하기도 한다.
taint-based fuzzer는 프로그램 내 주어진 경로에서 입력의 어떤 부분이 변형되어야 하는지에 대한 이해는 있지만, 입력을 어떻게 변형해야 하는지는 알지 못한다.
symbolic 기술은 정적 분석과 동적 분석 사이 갭을 연결하고 fuzzing에서의 제한된 의미론적 통찰력을 해결하는 방법을 제공한다.
dynamic symbolic execution은 에뮬레이트된 환경에서 프로그램을 실행하는데 사용하는 동적 기술이다. 이러한 시스템은 애플리케이션을 에뮬레이트할 때, 프로그램 실행 동안의 레지스터와 메모리 상태와 변수의 제약을 추적한다. 조건 분기에 도달하면, 실행은 나뉘어 두 경로를 따라가면서 분기가 사용한 경로의 제약조건을 분기조건으로 저장하고 분기가 사용하지 않은 경로의 제약조건을 분기 조건의 역으로 저장한다.
high semantic insight
높은 의미론적 통찰력을 갖는 기술은 실행 중인 경로에서 조건이 발생했을 때 애플리케이션에 적절한 입력을 만들기 위한 경로 제약을 사용함으로써 어떻게 특정 프로그램의 상태를 트리거할 수 있는지 알 수 있다.
Classical dynamic symbolic execution
dynamic symbolic execution은 소프트웨어의 취약점을 찾는데 바로 사용할 수 있다.
초기에는 소스 코드를 테스트하는데 사용되었다. 이후 바이너리 코드에 사용되었는데, Mayhem과 S2E를 사용해서 취약한 상태(ex. 공격자 입력에 의한 instruction pointer overwrite)가 식별될 때까지 경로 탐색을 수행한다.
trade-off: path explosion 문제
path explosion 문제 때문에 제한된 확장성을 갖는다. 새로운 경로는 매 분기마다 생성되므로 프로그램 내 경로 수가 기하 급수적으로 증가하게 된다.
prioritizing promising path
유망한 경로에 우선순위를 매겨서 path explosion 문제를 해결할 수도 있는데, 이 path explosion 문제를 완전히 극복하지 못했고 이러한 방식을 사용하는 시스템에서 버그가 적게 발견됐다.
Symbolic-assisted fuzzing
path explosion 문제를 해결하기 제안된 방법으로 symbolic execution에 fuzzing을 결합한 symbolically-guided fuzzer를 사용한다. 이 fuzzer는 dynamic symbolic execution 엔진에서 입력을 처리하며 fuzzing 구성요소에서 식별된 입력을 변경해간다. dynamic symbolic execution은 이전에 탐색하지 않은 코드를 트리거하고 fuzzing 요소가 계속 code coverage를 할 수 있도록 하는 추가적인 테스트 케이스를 제공하면서 적절하게 입력을 변형하기 위해 프로그램에 대한 심층적인 이해가 있다.
Under-constrained symbolic execution
애플리케이션의 일부분만 실행해서 dynamic symbolic execution의 용이성을 높일 수 있다. 잠재적인 버그를 찾는데는 효과적이나, 두 가지 단점이 있다. 애플리케이션에서 실행하는 부분에 대한 문맥 적절성을 보장할 수 없고, 확장성 면에서 탐지한 버그를 재연 가능성을 포기해야 한다.
취약점 발견 분석은 실질적으로 크래시를 발생시키는 입력을 찾는 것이다.
이 섹션에서는 식별한 크래시를 재연하는 과정과, 크래시 영향을 입증하기 위해 자동으로 익스플로잇을 생성하는 방법, 그리고 현대 예방 기술에서 익스플로잇을 굳건하게 하는 방법에 대해 다룬다.
대부분 fuzzer에서 실행을 de-randomization하는 이유
대부분의 fuzzing 접근법에는 두 개의 애플리케이션 인스턴스에 제공된 동일한 입력은 동일한 결과를 만들 것이라는 추정이 있다. 하지만 분석 환경을 벗어났을 때는 발견된 크래시가 재연 불가능할 수 있다. 랜던화되지 않는 분석 환경에서는 크래시를 발생시킨 입력은 항상 동일한 경로에 들어가서 크래시를 발생시킬 것인데, 분석 환경을 벗어나면 항상 어떤 토큰 값은 달라질 것이고 크래시를 발생시켰던 입력은 크래시가 발생하지 않는 경로에 들어갈 수 있다.
Missing Data
취약점 탐지 기술에서는 애플리케이션에서 처음 받는 값이 아니면 올바른 응답값을 추정하도록 되어있다. 따라서 랜덤화되지 않는 환경에서는 fuzzer와 같은 분석 엔진은 프로그램에서 검색하기 전에 토큰과 같은 값들을 짐작하는데, 분석 환경 밖에서는 토큰 값이 매칭되지 않게 되고 이에 따라 크래시가 발생하지 않는다.
분석 환경 밖에서는 크래시를 발생시킨 입력이 크래시를 재연할 수 없게 되고, 새로운 크래시를 발생시키는 입력을 찾을 수도 있다. 관련하여 data leak 식별에 대한 분석이 존재하는데, angr에는 구현하지 않았다.
Missing relationships
낮은 의미론적 통찰력을 가지는 fuzzing과 같은 기술은 프로그램에서 검색한 데이터와 이후의 데이터 사이의 관계를 복원할 수 없다.
예를 들면, 사용자가 제공한 토큰 값이 이후에 크래시를 발생시키는데 사용되더라도, 사용자가 제공한 토큰값과 사용자가 크래시를 발생시키기 위해 제공해야 하는 토큰값 사이에 관계를 알지 못한다.
input specification은 애플리케이션에 제공받은 데이터와 나중에 제공된 데이터 사이 관계를 정의할 수 있는데, 이러한 접근법은 Replayer이다. Replayer는 분석 환경에서의 프로그램 경로를 리얼월드에서는 어떤 프로그램 경로로 줘야 하는지 계산하는 것이다.
모든 크래시가 익스플로잇 가능한 것은 아니다.
익스플로잇 가능한 크래시를 이해하는건 버그 분류에 도움이 된다.
실행 불가능한 스택 영역과 Address Space Layout Randomization(ASLR)과 같은 보호기법으로 전통적인 익스플로잇이 불가능하게 되었다.
현재 자동화된 익스플로잇 기술은 현대 방어 기술이 적용되기 전에 설계되었기 때문에 현대 소프트웨어에는 그러한 익스플로잇 기술이 실용적이지 않게 되었다. 따라서 이러한 방어 기법을 우회하는 익스플로잇들이 만들어졌는데, 예를 들면 전통적인 shellcode 기반 익스플로잇을 Return-Oriented Programming(ROP) 익스플로잇으로 우회할 수 있다.
임베디드 기기의 증가로, ARM, MIPS 등 여러 하드웨어 아키텍처와, 32bit뿐만 64bit 아키텍처 바이너리를 지원해야 한다.
현대 분석 시스템은 서로 다른 운영체제를 사용하는 소프트웨어를 지원해야 한다. 따라서 운영체제는 추상화되어 서로 다른 실행 포맷을 로딩하도록 지원해야 한다.
유용한 분석 엔진은 넓은 범위의 분석을 지원해야 한다. 이를 위해서는 엔진을 추상화해서 서로 다른 메모리 모델뿐만 아니라 서로 다른 데이터도 제공해야 한다.
angr의 목적은 바이너리 분석 기술을 유용하게 재현하고, 향상시키고, 그리고 만드는 도구를 제공하는 것이다. 따라서 angr의 learning curve를 낮게, 사용성을 높게 유지하려고 노력했다.
angr는 Python으로만 구현했는데, Python은 상수 시간의 낮은 성능을 보인다. 언어 오버헤드가 중요해지면 angr는 Python JIT engine인 PyPy를 돌려서 속도를 높일 수 있다.
angr 목표
바이너리 분석 기술의 재현
여러 아키텍처를 지원하기 위해, 아키텍처 기반 네이티브 바이너리 코드를 intermediate representation(IR, 컴파일러나 가상머신에서 소스코드를 표현하는데 사용하는 데이터 구조 또는 코드)로 바꾼다. IR lifter를 직접 만들지 않고, libVEX를 차용했다. libVEX는 VEX라는 IR를 만드는데, Firmalice를 위해 만들어진 PyVEX를 사용했다. PyVEX는 VEX IR를 Python에 binding시킬 수 있다. VEX를 사용해서 32bit, 64bit 버전의 ARM, MIPS, PPC, x86 프로세서를 지원할 수 있다.
Registers
Symbolic memory
Abstract memory
메모리를 모델링하기 위한 정적 분석 시 추상화된 메모리 상태 플러그인이 사용된다.
POSIX
Log
Inspection
Solver
Architecture
프로그램 상태 변경
시스템 호출은 Python 함수 형태로 구현되어, Python 함수를 사용하여 프로그램 상태를 변경하는 등을 할 수 있다.
Python 함수인 SimRun은 현재 실행 상태를 나타낸다.
expression tree
Claripy backend
Claripy frontend
미래의 바이너리 분석의 근간을 제공하기 위해 angr을 오픈소스로 배포했다.
제공 기능
이 섹션에서는 angr에서 CFG 복원 방법을 설명한다.
CFG 복원
angr는 프로그램의 entry point부터 최적화 부분까지 CFG 복원을 수행한다.
forced execution, backward slicing, symbolic execution을 활용하여 모든 간접 점프의 점프 타겟까지 CFG을 복원한다.
CFG 복원 알고리즘 단점
CFG 복원 알고리즘 보완: 2차 알고리즘 사용
이 섹션에서는 발전된 CFG 복원 알고리즘인 CFGAccurate를 소개하고, 더 빠른 알고리즘인 CFGFast를 설명한다.
CFGAccurate에서 알고리즘 실행 시간 최적화 위한 가정들
난독화되었거나 일반적이지 않은 바이너리의 경우 이러한 가정을 제거할 수는 있지만 CFG 복원 실행 시간을 늘릴 수 있다.
CFG 복원하기 위한 근본적인 부분
빠른 CFG 복원 알고리즘의 목표
이 알고리즘은 control flow가 부족한 그래프라 완전하지 않지만, manual과 자동화된 바이너리 분석에 유용하다.
angr의 CFG recovery 알고리즘
별도의 strided-interval 집합 생성
VSA 데이터 타입인 strided interval은 concrete 값 집합을 근사하는데 적합하지만, 이러한 값은 프로그램 내 점프 타겟으로 사용되어 strided-interval의 과대 근사치는 복원된 CFG에서 soundness 하지 않을 수 있다. 이를 해결하기 위해 새로운 데이터 타입인 strided interval 집합을 만들었다. strided interval 집합은 조합되지 않는 strided interval의 집합인데, 한계치인 K보다 많은 원소를 가질 때 strided interval 집합은 조합될 수 있다. 한계치는 semantic 통찰력과 확장성의 균형을 제어할 수 있다. (높은 K는 높은 정확도를 갖지만, 분석 복잡도 비용이 높아진다.)
대수 solver를 path predicates에 적용
분기 조건 추적은 조건문 종료나 합병 과정 시 변수를 제약할 수 있어 보다 정확한 분석 결과를 얻을 수 있다. Affine-Relation 분석은 분기 조건 추적 기술로 제안되었지만, 모두 구현이 복잡했고 실제로는 계산 비용이 크다.
따라서 strided interval domain에 적용될 수 있는 경량의 대수 solver를 구현했다. 새로운 경로가 보이면, 변수의 수 범위를 얻기 위해 단순화하고 계산을 한다. 이후 새로 생성된 수 범위와 원리 값을 교차해서 새로운 분기 조건을 만났을 때 fix-point의 정확도를 높일 수 있도록 했다.
signedness-agnostic domain 사용
원래는 모든 값이 부호가 있음을 가정하고 부호있는 strided interval에 VSA가 수행된다. 하지만 이는 부호없는 산술 계산 시 과대 근사치를 가져오게 된다. 점프 주소가 부호없는 수이기 때문에 점프 주소 계산은 부호없는 값으로 진행되어 이 과대 근사치는 더 문제가 된다.
이를 해결하기 위해 signedness-agnostic domain을 사용하는 것이다. wrapped interval analysis는 LLVM 코드 분석 위한 interval domain으로 signed와 unsigned 수를 동시에 처리한다.
memory corruption 탐지 위한 VSA의 사용
VSA 분석을 위해 angr는 Value Flow Graph라는 인터페이스를 제공한다.
VFG는 각 프로그램 지점에서의 VSA fix-point를 나타내는 프로그램 상태를 포함한느 CFG를 발전시킨 그래프이다.
VFG에 전달된 파라미터에 따라 하나의 함수만 포함할 수도 있고 함수 호출 트리나 전체 프로그램을 포함할 수 있다.
VFG program state
VFG의 프로그램 상태는 SimuVEX가 제공하는 추상화된 메모리를 나타낸다. 메모리에서 값은 Claripy가 제공하는 value-set 형태로 되어있다.
동적 심볼릭 실행 모듈은 Mayhem 기술 기반으로 만들어졌다. 경로 우선순위 기술과 동일한 메모리 모델을 사용한다.
동적 심볼릭 실행 모듈은 angr의 주요 기능 중 하나로,Veritesting이나 under-constrained symbolic execution 분석은 이 모듈을 기반으로 이루어진다.
SimuVEX가 제공하는 심볼릭 메모리 모델을 놓기 위해 Z3에 Claripy 인터페이스를 사용한다. 각각의 실행 경로는 Path 객체에 의해 관리되며 Path 객체는 경로, path predicate, 경로 관련 정보에 대한 행위를 추적한다. 경로 그룹은 PathGroup 기능에 의해 관리된다. PathGroup은 dynamic symbolic execution 과정에서 경로의 분할, 합병, 필터링을 관리하는 인터페이스를 제공한다.
Veritesting을 Veritesting 분석으로 지원한다.
UC-KLEE에서 제안된 under-constrained symbolic execution을 구현했으며 UC-angr 라 부른다.
under-constrained symbolic execution은 함수 각각에 대해 실행하는 기술이다.
분석 시 어떻게 특정 함수를 가져오는지 알 수 없기 때문에, UCSE에 의해 탐지된 버그는 재연 불가능하다. 각 함수는 문맥(context, 실제 실행에서 호출되는 인자나 전역 변수와 같은) 없이 실행되기 때문에 이 분석은 정확하지 않고 false positive가 있다.
UCSE는 under-constrained 상태로 빠진 문맥을 태그해놓는다. under-constrained 데이터가 포인터로 사용되면, 새로운 under-constrained 영역이 생성되고 포인터는 이 새로운 영역을 가리킨다. 이 필요에 의한 메모리 할당은 분석해야 하는 복잡한 데이터 구조체를 관리할 수 있게 한다. 보안 위반 사항이 식별되면(스택의 리턴 주소에 값을 쓰는 등) 값은 under-constrained 상태로 체크된다. 해당 상태에서 위반 사항은 false positive로 필터링된다.
Global memory under-constraining
기존 UC-KLEE는 전역 메모리를 under-constrained로 접근하는걸 다루지 않는다. 하지만 프로그램 문맥에서 전역 메모리는 UCSE로 예측할 수 없어서 전역 데이터는 덮어 써졌을 수도 있다.
따라서 모든 전역 데이터를 under-constrained로 표시해서 false positive 비율을 낮췄다.
Path limiters
UC-KLEE는 path explosion 문제를 예방하기 위해 몇 가지 제한을 해두었다. 추가 제한으로는 path explosion이 발생할 것 같으면 함수 분석을 그만둔다. 하드코딩으로 제한을 했고, 이러한 함수 분기가 여러 경로에서 보이면 함수를 return문으로 대체하고 함수의 call site에서부터 분석을 다시하도록 했다. 이는 path explosion을 피할 수 있지만 분석 정확도는 낮아진다.
false positive 필터링
익스플로잇 상태를 발견하면, under-constrained 데이터의 constraint 부족으로 이 상태가 익스플로잇 가능하다고 여기지 않는다.
먼저, 상태가 익스플로잇 불가능함을 나타내는 constraint E로 constraint solve를 한다.
예를들면, 리턴 주소를 overwrite하는 위반사항이 발견되면, 이 상태에 제약을 줘서 리턴 주소가 overwrite될 수 없다고 한다. 익스플로잇 못하는 상태에서 가능한 솔루션으로 under-constrained 값을 constrain하고 이를 U라 한다. constraint U를 가지면서 E를 제가해가고계속 상채가 익스플로잇 가능한지 체크한다. 계속 익스플로잇 가능하면 이는 문맥에서 없는 데이터에 의존적이지 않는 흐름임을 의미한다. 하지만 여전히 false positive를 가질 수 있다.
UC-angr은 Simstate 플러그인으로 구현되었다.
이 섹션에서는 symbolic-assisted fuzzing의 요약을 제공하고, 이 퍼저인 Driller는 다른 논문에서 자세히 설명한다.
symbolic-assisted fuzzing
symbolic-assisted fuzzing은 AFL fuzzer을 기반으로 angr를 심볼릭 트래거로 사용한다. AFL 행위를 추적하면서 AFL에서 만든 입력에 따라 언제 심볼릭 추적을 시작할지 결정한다. AFL이 입력을 변형해가면서 새로운 상태 변환을 발견하면 AFL은 이를 보고한다. AFL이 유니크하다고 한 모든 경로에 angr를 호출해서 AFL이 해당 전환에서 찾을 수 없었던 입력을 찾아본다.
symbolically tracing
AFL이 제공하는 concrete 입력 기반으로 angr의 symbolic execution engine은 심볼릭적인 추적을 제공하고, Driller의 symbolic 요소는 angr의 이 symbolic execution engine을 사용하여 구현된다. 각 입력은 하나의 경로에 대응되므로 symbolic execution에서 path explosion 문제를 피할 수 있다. 또한 이러한 입력은 AFL에 의해 추적하기로 유망한 것만 살아남아서 그렇지 않으면 필터링된다.
symbolic-assisted fuzzing에서의 PathGroup 사용
각 입력은 PathGroup 내 한 경로에 대응된다. PathGroup 각 단계마다 모든 분기는 해당 경로로 이끈 가장 최근의 점프 명령어를 ALF이 알지 못한 것으로 체크된다. 이러한 점프가 발견되면, SMT Solver는 이 점프로 이동할 입력을 생성하기 위해 사용된다. 이 입력은 AFL애 피드백되어 추후 퍼징에 사용한다.
입력값과 출력값 사이의 잃어버린 관계를 회복하기 위해 Replayer에서 제안한 접근법을 사용해서 symbolic execution engine 위에 Replayer를 구현했다.
크래시 재연 문제 정의
크래시 재연 알고리즘
프로그램 P에 대해 초기 상태가 sa, 크래시 상태가 qa, 입력이 ia일 때, 알고리즘은 이 입력을 가져온다. 입력 ia는 instrumented(de-randomized) 환경에서 sa를 qa로 가게 한 입력이지만 uninstrumented 환경에서는 재연되지 않는다.
Replayer limitation
자동으로 익스플로잇을 생성하는 것으로 angr 도구의 효율성을 평가할 수 있다.
AEG/Mayhem과 다르고 AXGEN과는 비슷하게, angr를 사용해서 크래시를 발생시키는 입력으로 concolic execution을 통해 익스플로잇을 생성한다.
크래시를 발생시킨 입력이 실행한 경로와 동일한 경로를 추적하면서 concolic execution을 수행한다. 프로그램에 크래시가 발생하면 concolic execution이 멈추고 크래시 원인을 알기 위해 symbolic 상태를 관찰하고 익스플로잇 가능성을 측정한다. 특정 레지스터의 symbolic bit를 세서 크래시를 frame pointer overwrite, instruction pointer overwrite, arbitrary write 등으로 분류한다.
instruction pointer에서 symbolic bit가 탐지되면, instruction pointer가 shellcode나 ROP gadget과 같이 제어 가능한 명령어를 가리키도록 만든다.
Cyber Grand Challenge는 7개의 시스템 호출만 포함하는 custom OS를 사용한다. 시스템 호출 부족은 레지스터 제어나 메모리에 읽기 쓰기가 제한된다.
DARPA 표준으로, CGC에는 다음의 두 가지 타입의 익스플로잇이 존재한다.
현대 방어 기술에도 굳건한 익스플로잇을 위해 Q의 아이디어를 기반으로 ROP chain compiler를 구현했다. 메모리에 데이터 쓰기나 라이브러리 내 임의 함수 호출 등의 원하는 기능을 수행할 수 있는 ROP payload를 자동으로 생성해준다.
ROP gadget을 찾기 위해 모든 실행가능한 코드를 스캔한다. 그리고 가젯의 용도에 따라 분류했다. 분류를 위해 angr의 Path 객체가 제공하는 action history와 Claripy가 제공하는 symbolic relation을 활용했다.
이후 gadget의 배치를 결정한다.
예를 들면, 스택에 데이터를 넣는 가젯은 데이터를 빼는 가젯과 쌍을 이뤄서 데이터를 다른 레지스터로 옮기는 것을 만들 수 있다.
gadget들을 배치한 후, gadget을 체이닝해서 더 고수준 행위를 할 수 있도록 한다.
gadget을 angr의 프로그램 상태로 적고, 주어진 입력에 대해 SMT solver가 결과를 내도록 해서 주어진 인자에 대한 출력을 얻는다.
DARPA의 CGC 바이너리를 평가에 사용했다.
IDA Pro는 CFG 복원 기술을 사용한다.
IDA Pro는 바이너리를 재귀적으로 디스어셈블하면서 심볼과 휴리스틱을 사용해서 함수를 식별하고 간접 점프 타겟을 처리하기 위해 데이터 흐름 분석을 진행한다.
평가 시 복원된 basic block 수와 제어 흐름 이동을 사용했다.
24시간 동안 돌리면서 DARPA 데이터셋 대상으로 취약점 발견 기술 평가를 진행했다.
위 표는 CGC 대회에서 등수별 바이너리 크래시 수를 보여준다.
1등과 7등 팀은 대회에서 모두 symbolically-assisted fuzzing 기술을 사용했다. 연구에서 구현한 Driller는 1등 팀과 같은 수의 취약점을 식별할 수 있었다.
dynamic symbolic execution은 dynamic symbolic execution만을 사용하거나 이 기술에 Veritesting path explosion 예방 기술이 있는 두 방법으로 평가를 진행했다.
예상했듯이 dynamic symbolic execution에는 path explosion 문제가 빈법하게 나타났다. 16개의 CGC 바이너리 중에서 취약점들을 찾을 수 있었다. path explosion 문제를 예방하기 위해 만들어진 Veritesting의 경우 11개의 취약점만 식별할 수 있었다. Veritesting이 dynamic symbolic execution보다 적은 취약점을 찾았다는 것이 놀라웠다. Veritesting은 path explosion 문제 때문에 효율적으로 path 합병을 진행하는데, 이러한 path 합병은 복잡한 표현을 사용해서 constraint solver에 오버헤드가 발생한다.
symbolic-assisted fuzzer는 AFL를 사용한다. 입력이 어떤 코드 섹션에 들어가는지 식별하기 위해 각 AFL이 만드는 입력은 dynamic symbolic execution engine에 의해 추적된다. 모든 입력은 concrete(입력은 분기하지 않음)하기 때문에 추적 과정에서 path explosion이 없고 DSE engine은 code coverage를 증가시키지 않는 입력은 필터링한다.
symbolic-assisted fuzzer로 77개의 취약점을 찾았고, AFL만을 사용했을 때는 66개 취약점을 찾았다.
fuzzing이 DSE보다 거의 3배는 더 많은 취약점을 찾을 수 있었다.
크래시를 발생시키는 입력을 분석했을 때, dynamic symbolic execution은 short path를 나타내는 경향이 있었다.
dynamic symbolic execution이 찾을 수 없었던 취약점을 살펴보면 분석 복잡성 증가가 원인임을 알 수 있었다. 주어진 경로 A에서, A가 다음 점프 끝에서 분할될 것이라는 가능성 pa가 있고, A1은 점프가 가지는 경로를 따라가고 A2는 그렇지 않은 경로를 따라간다. 다음 조건 점프에서, a1와 A2가 복제될 가능성이 있으므로 기하 급수적으로 분석해야 하는 경로가 증가하게 된다. (path explosion)
따라서 dynamic symbolic execution은 많은 basic block을 실행할 필요가 없는 크래시를 찾는데 적합하다.
code coverage 측정
기술의 효율성을 평가하기 위해 테스트 케이스에서 만들어진 code coverage를 계산했다. symbolic execution이 바이너리 당 평균 330 block 을 커버할 수 있었고, fuzzing은 689, symbolic-assisted fuzzing은 698을 커버할 수 있었다.
fuzzing이나 symbolic-assisted fuzzing에서 만들어진 경로가 그래프에 결합되면, CFGAccurate보다 더 많은 code coverage가 가능한 CFG가 가능하다.
UC-angr는 CGC 바이너리에서 371개의 취약점을 발견할 수 있었다. 하지만, 이 접근법의 경우 함수는 문맥이 없기 때문에 결과에 많은 false positive가 있고 재연 불가능할 수 있다.
UC-angr의 결과에서 346개의 false positive를 식별했고 false positive 비율이 93%이다.
UC-angr와 비슷하게 VSA 결과는 재연 불가능하고 false positive가 있다. VSA는 27개의 취약점을 찾을 수 있었고 나머지는 130개의 false positive였다. VSA의 false positive 비율은 82.8%이다.
재연 불가능한 기술인 VSA와 under-constrained symbolic execution은 낮은 성능을 보였는데, 재연성 대신 더 많은 code coverage가 가능하다. 문맥이 부족했기 떄문에 결과에 많은 false positive가 있었다. false positive 비율을 낮추기 위해 false positive 필터링을 했는데, 많은 true positive도 필더링되었다.
실제 바이너리에 정적 분석 기술을 적용하려면 아직은 더 많은 연구가 필요하다.
취약점 분석 기술들로부터 크래시가 식별된 후, 이 크래시를 재연하고 익스플로잇하는 작업을 진행했다.
환경 데이터와 de-randomized 분석 환경 때문에 크래시를 발생시킨 입력은 실제로 재연 불가능할 수 있다.
취약점 분석 기술로는 발견할 수 없었던 취약점에 대한 입력을 DARPA가 제공하는 것을 참고하여 각 바이너리에 대한 크래시를 분석했다. 이 크래시를 발생시키는 입력 중 6개는 재연이 불가능했다.
DARPA는 랜덤 데이터에 의한 제어 흐름을 허용하지 않는다. 이는 Replayer의 한계가 이 CGC 바이너리에 대해서는 없다는 것이다.
더 복잡한 제어 흐름을 갖는 리얼월드 바이너리를 대상으로 기술을 적용하려면 더 많은 연구가 필요하다.
Replayer로 크래시 식별 후, 타겟 애플리케이션에서 믿을 만한 크래시를 내는 input specification을 해야 한다. 하지만 이러한 입력은 여전히 익스플로잇 불가능하다.
CGC 애플리케이션 대상으로 자동으로 익스플로잇을 만들기 위해 AEG 시스템에서 제안한 기술을 사용했는데, 이 기술로는 4개의 크래시만 익스플로잇이 가능했다. 바이너리를 더 깊이 분석해보면, CGC의 목표는 익스플로잇이 아닌 바이너리에 대한 크래시를 찾는 것이기 때문에 실제로 익스플로잇이 불가능한 것이다. 또한 CGC 바이너리는 실제 익스플로잇 시나리오를 가지고 있는데 AEG에서 제안한 기술은 대부분 이에 적용할 수 없었다.
복잡한 취약점에 대한 자동화된 익스플로잇을 제공하려면 추가 연구가 필요하다.
익스플로잇 가능한 취약점도 현대 보호기법으로 불가능할 수 있어서 exploit hardening이 필요하다. Q에서 제안된 기술로 구현했고, AEG에서 만든 것으로 익스플로잇을 굳건히 했다.
Q로 구현한 것은 바이너리에 대한 충분한 정보를 활용할 수 없어서 익스플로잇이 불가능한 것도 있었다. 예를 들면, 스택에 공각자가 제어할만한 데이터가 없고 stack pivot이 다른 프로그램 부분의 공격자가 제어할 수 있는 데이터를 사용해야 했다. Q 접근법은 이러한 동작에 대한 처리가 없어서 이러한 익스플로잇은 현대 보호기법 대상으로는 불가능했다.
이 논문에서는 자동화된 식별과 바이너리에서 취약점을 익스플로잇하는 많은 기술들이 구현된 프레임워크인 angr를 소개했다. 하나의 시스템에 여러 분석 접근들을 구현해서 기술 평가 시 효율성을 판단할 수 있었다. 또한 평가 결과로 기존의 기술들을 향상하는데 연구 방향을 알 수 있었다.
angr를 오픈소스화해서 바이너리 분석 분야에서 사용할 수 있도록 하였다.