최근에 취약점 분석 관련 블로그들을 탐방하던 중 Angr를 간단하게 실습한 글을 발견했다.
예전에 Symbolic Execution 공부할 때 잠깐 봤던 툴인데, 그때 당시에는 어떤 툴인지 정확하게 이해를 못하고 넘어가서 이번 기회에 제대로 공부해보려 한다.

먼저 Angr라는 툴에 대해서 알아보자. 간단하게 설명하면 python 기반인 바이너리 분석 프레임워크다(여러 블로그에서는 플랫폼이라고 하는데, 난 프레임워크라는 표현이 더 와닿아서 프레임워크라 했다).
이 툴은 2015년 DARPA의 CGC(Cyber Grand Challenge)가 시작하면서 유명해졌다. 해당 대회는 바이너리 자동 분석 툴을 만들어서 제한 시간 내에 출제된 문제들에 취약점이 있음을 증명하고 패치해서 점수를 얻는 대회다(보편전인 Jeopardy 형식의 CTF와는 많이 다른 방식). 여기서 Angr 툴을 제작한 Shellphish라는 팀이 3등을 했다. python으로 제작되어 활용성이 좋다는 점과 문서화가 잘 되어있어서 다른 툴들보다 매력적이다.
Angr는 기본적으로 amd64 linux 환경에서 사용하는 것을 가장 권장하는 것 같다.
* 공식 설치 링크
다른 노트북에 우분투 서버를 올려서 편하게 접근이 가능하지만, 내 기본 노트북은 Apple M1 MacBook Air 13이기 때문에 로컬에 직접 Angr를 설치해보고 싶었다.
그래서 이번에는 podman을 사용해서 환경 구축을 해봤다.
podman pull angr/angr --arch x86_64
위 명령어로 이미지를 pull 하면 macOS 위에 qemu를 사용한 x86_64 인텔 이미지를 가져온다
git clone https://github.com/angr/angr-examples.git
그리고 예제를 받아준다.
아래 명령어를 사용하면 해당 예제 디렉토리를 가상환경 내부에서 접근할 수 있도록 컨테이너를 실행해준다.
podman run -v $(pwd)/angr-examples:/home/angr/angr-examples -it angr/angr

예제 디렉토리 내에 defcamp_r100 바이너리로 실습을 진행했다.


fgets 함수로 데이터를 입력받고, 입력받은 데이터를 인자로 sub_4006FD 함수를 실행해서 값을 체크해서 결과를 출력해주는 간단한 바이너리다.
sub_4006FD 함수에선 전달받은 인자 값을 기반으로 수학적 연산을 처리하는걸 볼 수 있다.
이때 이 함수의 반환 결과는 0 혹은 1이다. 답을 얻기 위해선 0을 반환하는 입력값을 찾아야 한다. 원래는 리버싱 문제여서 이 식을 역연산 해서 입력값을 찾아야하지만, angr를 사용하면 알아서 찾아준다.
Angr를 사용해서 payload를 작성할 때 필요한 정보가 몇개 있다. 바로 "Nice!"와 "Incorrect password!" 문자열을 출력해주는 puts 구문의 주소를 알아야 한다. 원리는 "Nice!" 문자열을 출력해주는 주소를 탐색의 목표로 설정하고, "Incorrect password!"를 출력해주는 주소를 피해야 하는 주소로 설정해서 탐색을 진행한다.
각 주소는 IDA를 사용해 구했다.
puts("Nice!"): 0x400844
'puts("Incorrect password!")` : 0x40085A
import angr
def main():
p = angr.Project("./r100", auto_load_libs=False)
simgr = p.factory.simulation_manager(p.factory.full_init_state())
simgr.explore(find=0x400844, avoid=0x40085a)
if simgr.found:
found_state = simgr.found[0]
return found_state.posix.dumps(0).decode()
else:
return "No solution found."
if __name__ == '__main__':
print(main())
angr.Project : 바이너리 파일을 로드해서 분석 가능한 상태로 만들어준다.
auto_load_libs=False : angr가 자동으로 라이브러리(ex. libc)를 로드하지 않도록 설정. 이 설정을 통해 분석 속도가 빨라지며, 바이너리만 집중적으로 분석 가능.p.factory.full_init_state : 바이너리의 초기 상태 생성. 이 상태는 프로그램의 시작 상태로 입력을 받기 전에 프로그램이 실행을 준비하는 초기 상태를 의미함
full_init_state : 프로그램의 초기화 단계(ex. _start 함수 혹은 main 함수가 호출되기 전 상태) 를 포함한 상태로 설정p.factory.simulation.manager : 시뮬레이션 매니저 생성. 매니저는 바이너리의 다양한 실행 경로를 관리하고 탐색함. 이 코드에서는 simgr이라는 변수에 할당함simgr.explore : 경로 탐색 시작 명령어
find = 0x400844 : 탐색의 목표 주소. 이 주소에 도달하는 경로를 찾음avoid = 0x40085a : 탐색의 회피 주소. 이 주소에 도달하는 경로는 제외하고 탐색explore() 함수는 symbolic execution을 통해 여러 경로를 탐색하고, 목표 주소에 도달하는 경로를 찾아내면 해당 경로를 반환해준다.
simgr.found[0] : explore()가 목표 주소에 도달하면, 해당 경로가 simgr.found 리스트에 저장된다. 여기서 목표 주소에 도달하는 경로가 여러개 나올 수 있기 때문에, found[0]으로 첫번째 경로를 가져온다.posix.dumps(0) : 시뮬레이션에서 바이너리에 입력으로 전달된 데이터를 출력. 즉, 목표에 도달하는데 필요한 입력 값을 추출하는 코드decode() : 추출된 입력은 바이트 문자열이기 때문에, 일반 문자열로 변환.결과적으로 목표 주소에 도달하는데 필요한 입력 값을 자동으로 탐색해서 찾은 후 반환하게 된다.
출력해준 값을 바이너리를 실행하여 입력해보면 "Nice!" 문자열이 정상적으로 출력된다.
(angr) angr@3bb5686c5c8f:~/angr-examples/examples/defcamp_r100$ python3 ex.py
Code_Talkers
(angr) angr@3bb5686c5c8f:~/angr-examples/examples/defcamp_r100$ ./r100
Enter the password: Code_Talkers
Nice!
예전에 처음 접했을때 보다는 확실히 더 이해가 됐다. 그때 당시에는 경로가 뭔지, symbolic execution이 뭔지 잘 몰라서 그냥저냥 넘어갔었는데, 이렇게 코드 한줄 한줄 분석해보니 훨씬 더 도움이 된다.
이번 글에서는 진짜 간단한 angr 사용법만 익힌거고, 다른 블로그들을 보면 훨씬 무궁무진하게 활용할 수 있는 툴인것 같다. 나중에 기회가 된다면 이 툴을 사용해서 CTF 문제 풀이까지 도전해볼지도..?