# [angr] angr가 뭐죠?

## 배경

바이너리 분석 방법에는 크게 2가지가 있다.

1. 수동: gdb 같은 디버거로 어셈 코드를 한줄씩 실행하면서 레지스터와 메모리의 변환를 추적하는 분석
    
2. 자동: angr와 같은 자동화된 분석 프레임워크를 사용
    

이 2가지는 상호보완적이다.

이 글에서는 자동 바이너리 분석 프레임워크 angr에 대해서 알아보겠다.

## 소개

angr는 바이너리 분석을 위한 다재다능한 파이썬 기반 프레임워크이다. 중요한 철학은 바이너리 분석 작업을 프로그래밍 방식으로 자동화하는 것이다. API를 통해 복잡한 분석 스크립트를 쉽게 작성할 수 있다.

## 원리

아래 컴퍼넌트 들이 서로 상호작용하며 작동한다.

1. Loader: 분석할 바이너리를 메모리에 올림, 바이너를 파싱하고 각 세그먼트를 angr가 이해할 수 있는 주소 공간에 매핑한다.
    
2. State: 프로그램의 특정 시점 스냅샷이다. angr 분석의 기본 단위이며, 초기 상태(entry\_state)에서 시작한다.
    
3. Claripy: 솔버다. 입력값처럼 알 수 없는 값을 심볼로 표현하고 프로그램에 가해지는 수학적 논리적 제약 조건들을 추적한다.
    
4. SimEngine: State를 입력 받아 코드 한 블록을 실행하고 그 결과로 다음 State들을 만들어낸다. 예를 들어 if문을 만나면 2개의 true, false State를 생성한다.
    
5. PathGroup: 탐색할 State를 관리하는 매니저이다.
    

이런 컴포넌트들이 상호작용하며 바이너리 로드 → 초기 상태 설정 → 심볼릭 실행으로 경로 탐색 → 목표 상태 도달 이라는 분석 과정을 자동화한다.

## 실습

아래와 같은 C언어 코드를 컴파일해서 angr로 분석해보자.

```c
// gcc -g -o auth_check auth_check.c
#include <stdio.h>
#include <stdlib.h>

void success() {
    printf("Success!\n");
}

void failure() {
    printf("Failure.\n");
}

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s <password>\n", argv[0]);
        return 1;
    }

    int key = 0xdeadbeef;
    int input_val = atoi(argv[1]);

    if ((key ^ input_val) == 0x12345678) {
        success();
    } else {
        failure();
    }

    return 0;
}
```

이 프로그램은 0xdeadbeef라는 key 값과 사용자가 입력한 input\_val을 XOR해서 결과가 0×12345678과 같은지를 검사한다.

### angr를 이용한 자동분석

```python
import angr
import claripy

# 1. 바이너리 로딩
project = angr.Project('./auth_check', auto_load_libs=False)

# 2. 심볼릭 인자 설정 및 초기 State 생성
# argv[1]은 최대 10자리 숫자로 가정 (null 포함 11바이트)
# Claripy를 이용해 심볼릭 비트벡터(변수)를 생성한다.
sym_argv1 = claripy.BVS('sym_argv1', 10 * 8)
# entry_state가 아닌 full_init_state를 사용하면 argc, argv 설정이 더 용이하다.
state = project.factory.full_init_state(args=['./auth_check', sym_argv1])

# 3. SimulationManager 생성 및 탐색
simgr = project.factory.simulation_manager(state)

# success 함수의 주소를 찾아 'find' 대상으로, failure 함수 주소는 'avoid' 대상으로 설정
find_addr = project.loader.find_symbol('success').rebased_addr
avoid_addr = project.loader.find_symbol('failure').rebased_addr
simgr.explore(find=find_addr, avoid=avoid_addr)

# 4. 결과 추출 및 확인
if simgr.found:
    found_state = simgr.found[0]
    # success 경로에 도달하기 위한 구체적인 입력값을 solver를 통해 얻어낸다.
    solution = found_state.solver.eval(sym_argv1, cast_to=bytes)

    print("🚀 Success! Angr found the solution.")
    # atoi() 함수는 숫자 부분만 인식하므로, null 바이트 이전까지 출력
    print(f"🔑 Input value: {solution.split(b'\\x00')[0].decode()}")
else:
    print("😥 Could not find the solution.")
```

위 스크립트를 실행하면 angr가 심볼릭 실행으로 `(0xdeadbeef ^ atoi(sym_argv1)) == 0x12345678` 라는 제약 조건을 만들고, 솔버가 방정식을 만족하는 값을 찾아준다.

```bash
🚀 Success! Angr found the solution.
🔑 Input value: -862328681
```

## 로우레벨 관점에서 본 심볼릭 실행

1. 심볼릭 레지스터와 메모리: 레지스터와 스택 변수들은 angr의 State객체 안에서 관리된다. 값이 정해져 있으면 구체적인 값으로 알 수 없는 값이라면 Claripy가 만든 심볼릭 표현으로 저장된다. 그리고 `state.reges.eax` 같은 코드로 이 값에 접근을 할 수 있다.
    
2. 경로 분기와 제약 조건: cmp 어셈블리어를 만나면 State가 복제된다. fork한다.
    

## 한계

angr는 직접 추적하기 어려운 복잡한 경로를 자동으로 탐색하고 답을 구하는 강력한 도구이다.

하지만 아래와 같은 한계점이 있다.

* 상태 폭발: 프로그램이 복잡해지고 분기가 많아지면 탐색할 State의 수가 기하급수적으로 증가하고 분석에 필요한 시간과 메모리가 매우 커지게 된다.
