Introduction
이전에 Angr와 Symbolic Execution을 이용한 연구를 진행한 적이 있었습니다.
그 때 알게된 내용과 추가적으로 알게된 내용을 정리해보고자 합니다 😁
p.s. Angr가 워낙 방대하고 다룰 내용이 많아서 시리즈로 진행할 것 같습니다!
Angr는 Symbolic Execution(동적 기호실행)과 바이너리에 대한 다양한 정적 분석 수행 기능을 갖춘 다목적 바이너리 분석 플랫폼입니다.
보안 연구, 취약점 분석, 역공학 등에 유용하게 사용되며, 아직까지 활발하게 contribute되고 있는 프로젝트입니다.
Angr (Github)
Angr (official documentation - Introduction)
Goals
바이너리를 프로그래밍 방식으로 분석하기 위해서는 아래 과제들을 해결해야하며, Angr의 장점은 이를 수행할 수 있는 기능들을 전부 가지고 있다는 것 입니다:)
- 바이너리를을 로드합니다.
- 바이너리를 중간 표현으로 변환합니다.
- 정적/동적 분석을 수행합니다.
3.1. 부분 또는 전체 프로그램의 정적 분석 (i.e. 제어 흐름 그래프, 종속성 분석, 프로그램 슬라이싱)
3.2. 상태 공간에 대한 동적 분석 (i.e. 기호실행, 경로탐색)
정적 분석: 프로그램을 실행하지 않고 코드를 분석하는 기술
동적 분석: 프로그램을 실행하여 코드를 분석하는 기술
바이너리 로드
Angr는 다양한 파일 포맷과 아키텍처를 지원합니다.
중간 표현(Intermediate Representation, IR)
소스 코드를 표현하기 위해 컴파일러 또는 가상 시스템에서 내부적으로 사용하는 데이터 구조 또는 코드
Angr는 바이너리를 IR로 변환하여, 바이너리의 기계어 명령어를 보다 추상적인 형태로 분석할 수 있게 합니다.
이를 통해 복잡한 바이너리 코드를 더 쉽게 분석할 수 있습니다.
제어 흐름 그래프(Control Flow Graph, CFG)
프로그램이 실행 중에 횡단할 수 있는 모든 경로를 그래프 표기법을 사용하여 나타낸 것
프로그램의 구조를 파악할 수 있고, 특정 경로의 “비도달성“을 판단할 수 있는 장점이 있습니다.
종속성 분석(Dependency Analysis)
프로그램 내의 요소(변수, 함수 등) 간의 의존 관계를 분석하는 것
민감한 데이터나 기능에 접근할 수 있는 경로를 파악할 수 있어, 취약한 함수를 실제 공격으로 연결하는 데 편리합니다.
프로그램 슬라이싱(Program Slicing)
프로그램 내의 한 지점으로부터 특정 변수에 직, 간접적으로 영향을 주는 모든 문장들을 추출하는 기법
분석자의 입장에서 보면, 취약한 함수를 발견했을 때 공격 가능성을 분석하기 매우 유용한 기법입니다.
i.e. 10번째 줄의 system(x)에 영향을 미치는 모든 문장들을 가져오기
기호 실행(Symbolic Execution)
입력값을 구체적인 값 대신 기호로 대체하여 프로그램의 실행 경로를 분석하는 방법
모든 가능한 실행 경로를 탐색하고, 각 경로에서 발생할 수 있는 다양한 상태를 추적할 수 있습니다.
광범위한 자동화 취약점 분석에 유용하게 사용할 수 있습니다.
경로 탐색(Path Exploration)
프로그램의 다양한 실행 경로를 탐색하는 과정
기호 실행(Symbolic Execution)과 구체적 실행(Concrete Execution)을 따로 혹은 조합해 수행할 수 있습니다.
의도한 프로그램의 특정 상태에 도달할 수 있는 가능성을 파악하는 데 편리합니다.
Installation
Angr는 python 3.8+ 용 라이브러리로, 사용을 위해선 Python을 설치해야 합니다.
Installing from PyPI** Installing from Source** Angr는 여러 하위 프로젝트들이 존재하며, Angr와 상호작용하며 사용됩니다. 바이너리 및 관련 라이브러리를 로드하고 프로세스 메모리를 추상화하는 작업을 돕는 라이브러리입니다. p.s. 개인적인 견해로, Decompiler API(i.e. Binary Ninja API, IDApython, …)와 비슷한 역할을 하는 것 같습니다. CLE의 핵심 클래스로, 바이너리에서 가상 주소 공간으로의 표현(representation)을 처리합니다. CLE는 특정 파일 형식(ELF, PE, …)들을 처리하기 위해 Backend라는 기능을 제공합니다. 지원되는 Backend 종류는 다음과 같습니다. 또한 다음과 같은 속성들을 이용해 빠르게 분석을 수행할 수 있습니다. CLE를 사용하는 동안 Symbol(기호, 이하 Symbol)을 이용하여 작업할 수 있습니다. i.e. find_symbol(“getc”) Symbol을 사용해 디버깅에 필요한 여러 정보를 얻을 수 있습니다. Symbol을 이용해 동적 연결에 대한 정보를 얻을 수 있습니다. Claripy는 Angr의 Solver Engine으로, Claripy ASTs는 Concrete/Symbolic한 표현과 상호작용하는 방법을 정의합니다. AST는 “해당 연산의 출력이 제공되면, 입력은 어떤 값인가?” 라는 질문을 SMT Solver 형식으로 변환합니다. $$ sequence: x + y = 1, result: 3 $$ Bit-Vector로, 크기(Bit size)와 함께 Symbolic 또는 concrete(with value)하게 설정할 수 있습니다. BV는 사용하지 않으며(deprecated), 대신 BVV, BVS를 사용합니다. Q. BV(Bit-Vector)를 사용하는 이유 따라서 이 문제를 해결하기 위해 BV를 사용합니다. Symbolic한 Bit-Vector를 생성하는 함수 Concrete한 Bit-Vector를 생성하는 함수 크기가 다른 Bit-Vector들을 연산할 경우 오류가 발생하기 때문에, 제로(zero)/부호(sign) 확장을 사용해서 크기를 맞춰주어야 합니다. 두 Bit-Vector의 크기가 달라 오류가 발생하는 모습 제로(zero)/부호(sign) 확장을 사용해 연산이 가능해진 모습 IEEE754 floating point number(부동소수점 숫자)를 표현하기 위해 사용하며, Symbolic한 floating point number(부동소수점 숫자)를 생성하는 함수 Concrete한 floating point number(부동소수점 숫자)를 생성하는 함수 Boolean 연산을 수행하는 데 사용하며, Claripy Solver를 사용해 연산을 수행할 수 있습니다. 연산식에 제약 조건을 추가하는 함수 해당 state가 만족가능한지(명제가 만족가능한지)를 판단하는 함수 표현식 expr에 대해 최대 n개의 해를 반환합니다. 표현식 해의 최소/최대 개수를 출력하는 함수 이번 포스트에서는 Angr의 개념, Claripy와 CLE를 간단하게 정리하였습니다.root:/$ pip install angr
root:/$ sudo apt-get install python3-dev build-essential
root:/$ pip install -r requirements.txt
Getting Started
빠른 이해를 위해 Angr Core에 들어가기 앞서 CLE와 Claripy를 짚고 넘어가도록 하겠습니다.CLE
Loader
이를 통해 바이너리의 함수(function), 데이터(data), 심볼(symbol), 섹션(section), 세그먼트(segment) 등에 접근할 수 있습니다.variable description min_addr 최소 주소 위치 max_addr 최대 주소 위치 all_objects 로드된 전체 객체 main_object 메인 객체 shared_objects 공유 라이브러리 객체 extern_object 해결되지 않은 (심볼이 없는) 객체 kernel_object 가상화된 syscall의 주소 객체 import cle
loader = cle.Loader('test')
# <Loaded prob, maps [0x400000:0x807fff]>
loader.min_addr
# 4194304
loader.max_addr
# 8421375
loader.all_objects
# [<ELF Object chall, maps [0x400000:0x40676f]>, <ExternObject Object cle##externs, maps [0x500000:0x500040]>, <ExternObject Object cle##externs, maps [0x600000:0x607fff]>, <ELFTLSObjectV2 Object cle##tls, maps [0x700000:0x71500f]>, <KernelObject Object cle##kernel, maps [0x800000:0x807fff]>]
loader.main_object
# <ELF Object chall, maps [0x400000:0x4041a7]>
loader.shared_objects
# OrderedDict([('chall', <ELF Object chall, maps [0x400000:0x4041a7]>), ('extern-address space', <ExternObject Object cle##externs, maps [0x600000:0x607fff]>), ('cle##tls', <ELFTLSObjectV2 Object cle##tls, maps [0x700000:0x71500f]>)])
loader.extern_object
# <ExternObject Object cle##externs, maps [0x500000:0x500040]>
loader.kernel_object
# <KernelObject Object cle##kernel, maps [0x800000:0x807fff]>
Backend
즉, 각 바이너리 객체는 Loader Backend(cle.backend 의 하위 클래스)에 의해 로드됩니다.
CLE는 대부분의 경우에 자동으로 맞는 형식(Backend)을 찾아주므로, 따로 지정할 필요가 없습니다.name arch comment elf ELF - pe PE - mach-o Mach-O dynamic linking / rebasing 지원 안함 cgc Cyber Grand Challenge(CGC) - backendcgc Cyber Grand Challenge (CGC) memory specify / register backers 지원 elfcore ELF ELF core dumps 전용 blob - flat image로 메모리 로드
이하 설명은 전부 각각의 객체 클래스에 적용됩니다. (i.e. cle.backend.elf.elf.ELF)variable description entry 진입점(entrypoint) min_addr 최소 주소 위치 max_addr 최대 주소 위치 segments 세그먼트 sections 섹션 plt 해당 함수의 PLT 주소 reverse_plt 해당 PLT 주소의 함수 execstack 스택에 실행 권한 존재 여부 pic Position-independent code (PIE) 여부 imports 사용되는 함수와 그 주소들 obj = loader.main_object
# <ELF Object chall, maps [0x400000:0x40676f]>
obj.entry
# 4198720
obj.min_addr
# 4194304
obj.max_addr
# 4220783
obj.segments
# <Regions: [<ELFSegment flags=0x4, relro=0x0, vaddr=0x400000, memsize=0x7d8, filesize=0x7d8, offset=0x0>, ... <ELFSegment flags=0x6, relro=0x0, vaddr=0x404000, memsize=0x2770, filesize=0x10, offset=0x3000>]>
obj.sections
# <Regions: [<Unnamed | offset 0x0, vaddr 0x400000, size 0x0>, <.interp | offset 0x318, vaddr 0x400318, size 0x1c>, ... <.shstrtab | offset 0x303b, vaddr 0x400000, size 0x10a>]>
obj.plt["getc"]
# 4198704
obj.reverse_plt[0x401130]
# 'getc'
loader.main_object.execstack
# False
loader.main_object.pic
# True
loader.main_object.imports
# {'__libc_start_main': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT object at 0x7fb18cf51600>, '_ITM_deregisterTMCloneTable': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT object at 0x7fb18cf516c0>, ... 'getc': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT object at 0x7fb18cf519c0>}
Symbol & Relocations
기호를 이용하는 가장 쉬운 방법은 이름/주소를 받고 Symbol을 반환하는 함수를 사용하는 것 입니다.Symbol for Debug Informations
Symbol 객체를 통해 주소를 나타내는 방법이 3가지가 있습니다.
전역 공간에서의 주소, 바이너리에서의 상대 주소, 객체 기반의 상대 주소variable description name 이름 owner 소유 객체 rebased_addr 전역 주소 공간에서의 주소 linked_addr 사전 링크된 바이너리에서의 상대 주소 relative_addr 객체 기반의 상대 주소 # find_symbol
sym = loader.find_symbol("getc")
# <Symbol "getc" in cle##externs at 0x500040>
sym.name
# 'getc'
sym.owner
# <ExternObject Object cle##externs, maps [0x500000:0x500040]>
sym.rebased_addr
# 5242944
sym.linked_addr
# 32
sym.relative_addr
# 32
Symbol for Dynamic Linking
variable description is_export export 여부 is_import import 여부 resolvedby Symbol 위치 sym.is_export
# True
sym.is_import
# False
sym.resolvedby
# <Symbol "getc" in libc.so.6 at 0x500040>
Claripy
Claripy AST는 Claripy가 지원하는 수학적 구성 간의 차이를 추상화하여 트리처럼 나타냅니다.
* AST(Abstract Syntax Tree)word description Symbolic 구체적인 값이 없는 것 Concrete 구체적인 값이 있는 것 AST
예를 들어, 다음과 같은 연산식에서 x를 표현하는 것이 SMT Solver의 역할입니다.
$$ x = 2 - y $$property description op 수행해야하는 명령어의 이름 args 명령의 입력 값 x = claripy.BVS("x", 64)
seq = x + 10
x.op
# 'BVS'
seq.op
# '__add__'
seq.args
# (<BV64 x_0_64>, <BV64 0xa>)
BV(Bit-Vector)
Symbolic: Bit-Vector의 값이 정해지지 않고, 변수와 같이 기호로 표현
Concrete: Bit-Vector의 값이 구체적인 숫자로 정해져서 표현
일반적으로 python의 정수형은 overflow/underflow가 발생하지 않습니다.
만일 이 python의 정수형으로 시뮬레이션을 진행하면, 변수 크기에 제한이 없으므로 아무 오류 없이 끝없이 작아지거나 커지는 현상이 생기게 됩니다.
(또한 해당 변수의 크기가 int, char, 등으로 다를 수 있는 점도 연산 시 고려가 불가능합니다)BVS(Bit-Vector Symbolic)
parameter description name 변수 이름 size bit-vector의 크기 (bit 단위임을 유의) min 변수의 최솟값 max 변수의 최댓값 stride 증가폭 (e.g. 10으로 설정할 경우 10, 20, 30, …) # 32bit symbolic bit-vector "x"
claripy.BVS("x", 32)
# <BV32 x_1_32>
BVV(Bit-Vector Value)
parameter description value 값 size 크기 # 32bit concrete bit-vector of "0xdeadbeef"
claripy.BVV(0xdeadbeef, 32)
# <BV32 0xdeadbeef>
비트 확장(Bit Extend)
zero_extend는 Bit-Vector의 왼쪽에 주어진 숫자만큼 zero bit를 채워줍니다.
sign_extend는 Bit-Vector의 왼쪽에 주어진 숫자만큼 최상위 비트의 값을 채워줍니다simgr.solver.BVV(0x100, 32)
simgr.solver.BVV(0x100, 64)
a
# <BV32 0x100>
b
# <BV64 0x100>
a + b
# claripy.errors.ClaripyOperationError: args length must all be equal
a
# <BV32 0x100>
a.zero_extend(32)
# <BV64 0x100>
a.zero_extend(32) + b
# <BV64 0x200>
a
# <BV32 0x100>
a.sign_extend(32)
# <BV64 0x100>
a.sign_extend(32) + b
# <BV64 0x200>
FP(Floating Point)
BackendConcrete, BackendZ3 에서 지원합니다.variable description length 길이 sort 타입 (i.e. FSORT_FLOAT, FSORT_DOUBLE) FPS(Floating Point Symbolic)
parameter description name 이름 sort 타입 (i.e. FSORT_FLOAT, FSORT_DOUBLE) explocit_name Falsee일 경우 고유성 보장을 위해 이름에 식별자 추가 # symbolic floating point "x"
claripy.FPS('x', claripy.fp.FSORT_FLOAT)
# <FP32 FPS(FP_x_6_32, FLOAT)>
FPV(Floating Point Value)
parameter description value 값 sort 타입 (i.e. FSORT_FLOAT, FSORT_DOUBLE) # floating point value of 3.2
claripy.FPV(3.2, claripy.fp.FSORT_DOUBLE)
# <FP64 FPV(3.2, DOUBLE)>
Bool
BackendConcrete, BackendVSA , BackendZ3 에서 지원합니다.x = claripy.BVS('x', 32)
y = claripy.BVS('y', 32)
cmp = x == y
cmp
# <Bool x_7_32 == y_8_32>
Solver
Bit-Vector를 이용해 제약조건을 추가하고, 해를 하나 혹은 여러 개 출력하거나 해가 존재하는 지 확인할 수 있습니다.s = claripy.Solver()
# <claripy.solvers.Solver object at 0x7fd930d21510>
add
x = claripy.BVS("x", 64)
s.add(x > 100)
# (<Bool x_0_64 > 0x64>,)
s.add(x < 105)
# (<Bool x_0_64 < 0x69>,)
satisfiable
s.satisfiable()
# True
# if x = x + 1
s.add(x==x+1)
# (<Bool x_0_64 == x_0_64 + 0x1>,)
s.satisfiable()
# False
eval
Bit-Vector를 python으로 바꾸는 역할(즉, 해를 구하는 역할)을 합니다.parameter description expr 표현식 n 결과의 개수 s.eval(x, 1)
# (101, )
# 최대 n이라 해가 5개 미만이더라도 오류는 나지 않습니다.
s.eval(x, 5)
# (101, 102, 103, 104)
min / max
s.min(x)
# 101
s.max(x)
# 104
Conclusion
다음 포스트엔 Angr Core를 공부하고, 직접 사용해보려고 합니다.References