본문 바로가기

System/Linux

System/Linux/Angr 정리

반응형
# 설치
- 종속성 해결
    •  sudo apt-get install virtualenvwrapper python2.7-dev build-essential libxml2-dev libxslt1-dev git libffi-dev cmake libreadline-dev libtool debootstrap  debian-archive-keyring libglib2.0-dev libpixman-1-dev libqt4-dev graphviz-dev binutils-multiarch nasm libc6:i386 libgcc1:i386 libstdc++6:i386 libtinfo5:i386 zlib1g:i386
- 경로 찾기
1
find / -name virtualenvwrapper.sh

- ~/.bashrc 파일에 추가
1
2
export WORKON_HOME="~/.environments"
source path/virtualenvwrapper.sh

- .bashrc 적용
1
source ~/.bashrc

- 가상 환경 생성
1
mkvirtualenv angr

- angr 설치 (angr-dev)
1
2
3
git clone https://github.com/angr/angr-dev.git
cd angr-dev
./setup.sh --p angr

- 가상환경 다루기
  • 실행 : workon angr
  • 종료 : deactivate
  • 가상 환경 파일 지우기 : ~/.environments/angr
# 파일 로드
- [변수이름] = angr.Project('[파일이름]')

# 기본 속성
- 아키텍처 : [변수이름].arch
  • <arch AMD64 (LE)>
    • 리틀엔디안 AMD64 아키텍처
  • 메인 arch 클래스에 선언된 속성(@property)을 불러옴
    • arch.bits
    • arch.bytes
    • arch.name
    • arch.memory_endness
- 엔트리 포인트 : [변수이름].entry
- 파일 이름 : [변수이름].filename

# 로더
- 가상 주소 공간을 다루기위해 CLE라는 모듈을 사용합니다. 로더라고 불리며, '.loader' 속성으로 사용할 수 있습니다.
  • [변수이름].loader
    • loader.shared_objectss
    • loader.min_addr
    • loader.max_addr
    • loader.main_object
      • main_object.execstack : 실행가능한 스택이 존재하냐?
      • main_object.pic : 바이너리가 위치 독립이냐?

# 팩토리
- angr에서 사용하는 공통적인 객체에 대한 생성자를 제공합니다. 
- Blocks : 주어진 주소에 기본코드를 추출하는 공간
  • block.pp() : 디스어셈블된 코드 출력
  • block.instructions() : 명령 갯수
  • block.instruction_addrs : 명령어 위치
  • block.capstone : capstone (건축물에서 가장 위에 올리는 구조물이라고 말함 '갓돌') 디스어셈블
  • block.vex : 내부 파이썬의 주소

- States : angr에서 돌아가는 프로그램 상태는 특정 객체에서 작업됩니다. State가 이 상태들을 담고있고 여러가지 속성으로 현재 상태에 값들을 확인할 수 있습니다.
  • state = [Project변수이름].factory.entry_state()
    • state.regs.[레지스터이름]
      • state.regs.rip
      • state.regs.rax
    • state.mem[proj.entry].int.resolved
  • 파이썬 정수값은 한 CPU에서 같은 의미를 가지지 않기 떄문에 bitvector로 angr에서 나타내는 CPU data를 출력해줍니다.
  • .length로 bit의 길이를 출력합니다.
    • bv = state.solver.BVV(0x1234, 32) : 32비트 길이의 0x1234값을 가지는 비트 벡터 생성
    • eval 함수 사용하여 파이썬 정수로 변환합니다. 
      • state.solver.eval(bv)
        • 출력 : 0x1234
  • 비트 벡터 값을 레지스터와 메모리에 저장합니다.
    • state.regs.rsi = state.solver.BVV(3, 64) 
  • 메모리에 저장한 값을 다시 비트 벡터 값으로 출력
    • state.mem[0x1000].long = 4
    • state.mem[0x1000].long.resolved
      • 출력 : 0x4
      • .long은 타입에 해당하며 아래와 같은 값을 지원합니다.
        • char, short, int, size_t, uint8_t, uint16_t ...
      • 저장된 값은 두가지 형태로 값을 출력하며 resolved는 비트벡터
      • .concrete는 파이썬 정수형으로 값를 출력합니다.

- Simulation Managers : 프로그램 실행이든 시뮬레이션이든 우리가 호출하는 것에 대한 주 인터페이스가 제공됩니다.
  • simgr = proj.factory.simulation_manager(state) : 함수 생성자는 상태의 리스트나 상태 객체를 인자로 가져갑니다.
  • simgr.active : 시뮬레이션을 활성화 합니다.
    • 현재 시뮬레이션에 대한 세부 정보 : simgr.active[0]
      • 나오는 주소값은 현재 시뮬레이션이 한번 실행된 다음 rip 값과 동일합니다. 허나 시뮬레이션 상의 rip와 현재 프로그램의 rip는 다르고 시뮬레이션이 돌아가도 현재 프로그램의 rip가 수정되지 않는 것을 state.regs.rip 로 확인 할 수 있습니다. 
  • 기호 실행 진행 : simgr.step()

- Analyses
  • angr는 몇가지 내장된 분석을 제공합니다.
  • ipython의 자동완성 기능을 통해 지원하는 분석 기능들을 확인 해봅니다.
    • proj.analyses. 까지 입력 후 탭 명령으로 완성
  • 라이브러리 로드 설정 해제
    • proj = angr.Project('/bin/true', auto_load_libs=False)
  • CFG 분석 실행
    • cfg = proj.analyses.CFGFast()
  • 그래프 생성
    • cfg.graph
    • 길이 확인 : len(cfg.graph.nodes())
    • cfg.get_any_node를 이용해 주어진 그래프의 엔트리 노드 주소를 가져 옵니다.
      • entry_node = cfg.get_any_node(proj.entry)
      • len(list(cfg.graph.successors(entry_node)))
# Loading a Binary
- CLE는 angr에서 바이너리를 로드하는 컴포넌트이고, 바이너리를 가져올때 관련된 어떠한 의존적인 라이브러리라도 로드합니다.
- cle.Loader는 로드된 바이너리 객체의 전체를 나타냅니다.
- 각각의 바이너리 객체는 백엔드 로더에 의해 로드됩니다.
  • proj.loader.all_objects : 로드된 모든 바이너리 확인
  • proj.loader.main_object : 메인 바이너리 확인
  • proj.loader.shared_objects : 디렉토리에서의 공유 라이브러리 이름 확인
  • ELF 파일로부터 로드된 모든 객체 확인
    • proj.loader.all_elf_objects
  • 해결되지 않는 import와 angr 내부함수의 주소를 제공하는데 사용
    • proj.loader.extern_object
  • 시스템콜을 사용할 때
    • proj.loader.kernel_object
  • 주어진 주소에대한 레퍼런스를 얻기
    • proj.loader.find_object_containing([주소값])
      • proj.loader.find_object_containing(0x400000)
  • 메타데이터 추출
    • obj = proj.loader.main_object
      • obj.entry : 엔트리 포인트
      • obj.min_addr : 최소 주소 위치
      • obj.max_addr : 최대 주소 위치
      • obj.segments : 세그먼트 출력
      • obj.sections : 섹션 출력
      • obj.find_segment_containing(obj.entry) : 독립적인 세그먼트나 섹션을 주소로 얻을 수 있습니다.
  • 심볼에 대한 PLT 스텁의 주소를 얻기
    • addr = obj.plt['abort']
    • 반대로 심볼을 얻은 주소를 통해 구하기
      • obj.reverse_plt[addr]
  • obj.linked_base : CLE에 의해서 메모리로 매핑 될때 위치와 객체의 이전링크 기준을 출력해줍니다.
- 심볼과 재지정
  • 심볼은 주소에서 이름으로 매핑되는 실행가능한 포맷의 기초적인 개념입니다. 
  • loader.find_symbol : 심볼 얻기
    • malloc = proj.loader.find_symbol('malloc')
    • 심볼 객체의 반환 값 종류
      • .rebased_addr : 전역 주소 공간의 주소
      • .linked_addr : 바이너리의 이전 링크 기준과 관련된 주소
      • .relative_addr : 객체 base(기준)에 관련된 주소 = RVA(relative virtual address)
  • malloc.name : 이름 반환
  • malloc.owner_obj : 객체의 소유자를 반환하며 여기서 malloc의 소스가 들어 있는 파일 이름과 CLE에 의해 매핑된 주소가 반환됩니다.
  • malloc.is_export, malloc.is_import
  • malloc.resolvedby : resolve받는 위치 확인
  • main_malloc = proj.loader.main_object.get_symbol("malloc") : 메인 객체안에 심볼이 로드된 정보를 찾음
    • 이럴 경우 is_import 값이 true
- 로딩 옵션
  • 기본 옵션
    • auto_load_libs : 가능한 자동으로 공유 라이브러리 의존성을 채우려고합니다. 
    • except_missing_libs : 옵션이 true인경우 바이너리가 공유 라이브러리를 가질 떄 자동으로 제외 시킵니다.
    • force_load_libs : 로드되야하는 라이브러리 목록
    • skip_libs : 제외해야할 라이브러리 목록
    • custom_ld_path : 추가로 공유라이브러리를 찾아야할 경로. (기본 값을 프로그램이 로드된 경로 입니다.)
  • 바이너리 마다 주는 옵션
    • 바이너리를 로드할 때 특정 바이너리에 어떤 특수한 옵션이 필요하다면 주는 옵션입니다.
    • main_ops, lib_opts : 특정 옵션의 사전
      • main_opts : 옵션 이름과 값으로 된 맵
      • lib_opts :  옵션 이름과 값으로 매핑된 디렉토리의 라이브러리 이름.
    • backend : 사용할 백엔드 클래스 이름
    • custom_base_addr : 사용할 베이스 주소
    • custom_entry_point : 사용할 EP
    • custom_arch : 사용할 아키텍처 이름
    • Example : 
    • 1
      angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})

  • 백엔드
    • CLE는 백엔드에서 정적으로 로딩 아래 파일들을 로딩할 수 있습니다 자신이 로딩할 때 자동으로 대부분의 경우에서 사용되는 일치하는 백엔드를 찾습니다. 물론 무엇을 백엔드로 가지라고 CLE에게 지정해 줄 수 도 있습니다. 이 경우 custom_arch를 사용하면 됩니다.
    • backend name
      description
      requires custom_arch?
      elf
      Static loader for ELF files based on PyELFTools
      no
      pe
      Static loader for PE files based on PEFile
      no
      mach-o
      Static loader for Mach-O files. Does not support dynamic linking or rebasing.
      no
      cgc
      Static loader for Cyber Grand Challenge binaries
      no
      backedcgc
      Static loader for CGC binaries that allows specifying memory and register backers
      no
      elfcore
      Static loader for ELF core dumps
      no
      ida
      Launches an instance of IDA to parse the file
      yes
      blob
      Loads the file into memory as a flat image
      yes
- 기호 함수 요약
  • SimProcedures라는 내장 프로시저를 이용해 외부 함수 호출을 대신합니다.
    • auto_load_libs : True이면 , 실제 라이브러리 함수를 실행합니다. (기본값 : true) False이면 외부 함수를 쓰지 않습니다. 
    • use_sim_procedures : (cle.Loader가 아닌angr.Project의 파라미터입니다.)  False일 경우 외부에서 가져온 객체만 SimProcedures의 객체로 대치됩니다.
  • 후킹 : angr이 라이브러리 코드를 파이썬 코드로 대체하는 것을 후킹이라고 합니다. 시뮬레이션이 수행 될 때 미 단계마다 현재 주소가 후킹됬는지 검사하고 그렇다면, 그 주소의 바이너리 코드 대신에 후킹 코드(파이썬 코드)를 실행합니다. 
    • API : proj.hook(addr, hook)
    • 관리 : .is_hooked, unhook, .hooked_by
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      >>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS
      >>> proj.hook(0x10000, stub_func())  # hook with an instance of the class
       
      >>> proj.is_hooked(0x10000)            # these functions should be pretty self-explanitory
      True
      >>> proj.unhook(0x10000)
      >>> proj.hooked_by(0x10000)
      <ReturnUnconstrained>
       
      >>> @proj.hook(0x20000, length=5)
      ... def my_hook(state):
      ...     state.regs.rax = 1
       
      >>> proj.is_hooked(0x20000)
      True
    • proj.hoo_symbol(name, hook) : 처음 인자로 심볼의 이름을 제공합니다.

# Solver Engine
- AST : 추상적인 문법 트리를 변환하여 SMT solver에게 제공.
- Bitvector : 유한한 정수의 의미를 해석하는 하나의 비트열

- 비트 벡터 생성
1
2
3
4
5
6
7
8
9
10
11
12
# 1과 100 고정값의 64-비트 비트벡터
>>> one = state.solver.BVV(164)
>>> one
 <BV64 0x1>
>>> one_hundred = state.solver.BVV(10064)
>>> one_hundred
 <BV64 0x64>
 
# 9 고정 값의 27비트 비트벡터
>>> weird_nine = state.solver.BVV(927)
>>> weird_nine
<BV27 0x9>

- 비트 벡터 연산
1
2
3
4
5
6
7
8
9
10
>>> one + one_hundred
<BV64 0x65>
 
# 파이썬 정수와 연산
>>> one_hundred + 0x100
<BV64 0x164>
 
# The semantics of normal wrapping arithmetic apply
>>> one_hundred - one*200
<BV64 0xffffffffffffff9c>

- 64비트 비트벡터와 27비트 비트벡터는 연산할 수 없고 명령어를 수행하는 비트벡터의 길이가 다르기 때문에 같은 비트 벡터만 연산이 가능합니다. 이 방법 또한 연산 할 수있게 하는 방법은 비트 확장 시키는 것입니다.

- 비트 확장
1
2
3
4
>>> weird_nine.zero_extend(64 - 27)
<BV64 0x9>
>>> one + weird_nine.zero_extend(64 - 27)
<BV64 0xa>
  • zero_extend 명령은 비트벡터의 왼쪽에 주어진 zero bit 숫자만큼 채웁니다.
- sign_extend : MSB를 복사하여 채웁니다.  

- 64 비트 비트벡터 : 심볼 x
1
2
3
4
5
6
7
# Create a bitvector symbol named "x" of length 64 bits
>>> x = state.solver.BVS("x", 64)
>>> x
<BV64 x_9_64>
>>> y = state.solver.BVS("y", 64)
>>> y
<BV64 y_10_64>
- 기호 변수 x 와 y를 만듭니다.  값을 돌려받지는 못하지만 AST를 얻습니다.
  • x와 y는 기술적으로 하나의 AST이며, 연산 트리 구조 입니다. 여기서 연산 트리의 의미는 하나의 AST를 만들면 그 안의 값들은 .op와 .args의 인덱스를 통해서 하위 속성까지 접근할 수 있기에 연산 트리라고 말합니다. 
  • 각 AST는 .op와 .args 속성을 갖습니다. op는 수행해야할 명령어의 이름이고 args은 명령이 입력으로 가져온 값입니다.

- arg가 값이 아닌 BVV나 BVS 등이 되면 연산트리의 끝을 의미합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
>>> tree.op
'__div__'
>>> tree.args
(<BV64 x_9_64 + 0x1>, <BV64 y_10_64 + 0x2>)
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(<BV64 x_9_64>, <BV64 0x1>)
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1, 64)

# 기호 제약조건
- 비트 벡터에 대한 조건을 지정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
>>> x == 1
<Bool x_9_64 == 0x1>
>>> x == one
<Bool x_9_64 == 0x1>
>>> x > 2
<Bool x_9_64 > 0x2>
>>> x + y == one_hundred + 5
<Bool (x_9_64 + y_10_64) == 0x69>
>>> one_hundred > 5
<Bool True>
>>> one_hundred > -5
<Bool False>
- 마지막 one_hundred > - 5는 <BV64 0xfffffffffffffffb> 이며, 분명히 100보다 작지 않습니다. 기본이 unsigned으로 연산되기 때문에 signed으로 계산하려면 one_hundred.SGT(-5)로 지정해야합니다.

- 조건식의 참 거짓여부는 아래 함수를 통해서 따져야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> yes = one == 1
>>> no = one == 2
>>> maybe = x == y
>>> state.solver.is_true(yes)
True
>>> state.solver.is_false(yes)
False
>>> state.solver.is_true(no)
False
>>> state.solver.is_false(no)
True
>>> state.solver.is_true(maybe)
False
>>> state.solver.is_false(maybe)
False

# 제약조건 해결
- 제약조건을 추가하는 것으로 답을 만족하는 값을 고려하도록 가정합니다.
- state.solver.eval : eval은 보이듯이 어떤 비트벡터도 python(정수)으로 바꾸는 역할을 합니다. 
1
2
3
4
5
>>> state.solver.add(x > y)
>>> state.solver.add(y > 2)
>>> state.solver.add(10 > x)
>>> state.solver.eval(x)
4

- 주어진 값을 생성하는 입력값 찾기
1
2
3
4
5
6
7
8
# get a fresh state without constraints
>>> state = proj.factory.entry_state()
>>> input = state.solver.BVS('input'64)
>>> operation = (((input + 4* 3>> 1+ input
>>> output = 200
>>> state.solver.add(operation == output)
>>> state.solver.eval(input)
0x3333333333333381

- 현재 표현식에 만족하는 값이 있는지 없는지 state.satisfiable()함수를 통해 체크할 수 있습니다.
1
2
3
>>> state.solver.add(input < 2**32)
>>> state.satisfiable()
False

- 좀더 복잡한 표현식 : 단일 변수를 구하지 않고 x, y 값과 연산 결과를 구합니다.
1
2
3
4
5
6
7
8
9
10
# fresh state
>>> state = proj.factory.entry_state()
>>> state.solver.add(x - y >= 4)
>>> state.solver.add(y > 0)
>>> state.solver.eval(x)
5
>>> state.solver.eval(y)
1
>>> state.solver.eval(x + y)
6

# 부동 소수점
- z3 지원 부동소수점 : IEEE754 부동 소수점 > angr도 동일하게 지원
  • 차이점은 너비 대신에 숫자 정렬이 존재 (? 무슨뜻)
  • 부동 소수점은 FPV FPS값을 가집니다.
- 부동소수점의 제약조건 해결 작업은 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# fresh state
>>> state = proj.factory.entry_state()
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE)
>>> a
<FP64 FPV(3.2, DOUBLE)>
 
>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE)
>>> b
<FP64 FPS('FP_b_0_64', DOUBLE)>
 
>>> a + b
<FP64 fpAdd('RNE', FPV(3.2, DOUBLE), FPS('FP_b_0_64', DOUBLE))>
 
>>> a + 4.4
<FP64 FPV(7.6000000000000005, DOUBLE)>
 
>>> b + 2 < 0
<Bool fpLT(fpAdd('RNE', FPS('FP_b_0_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(0.0, DOUBLE))>
 
>>> state.solver.add(b + 2 < 0)
>>> state.solver.add(b + 2 > -1)
>>> state.solver.eval(b)
-2.4999999999999996

- 비트 벡터로 부동 소수점 표현
1
2
3
4
5
6
7
8
9
>>> a.raw_to_bv()
<BV64 0x400999999999999a>
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_0_64', DOUBLE))>
 
>>> state.solver.BVV(064).raw_to_fp()
<FP64 FPV(0.0, DOUBLE)>
>>> state.solver.BVS('x'64).raw_to_fp()
<FP64 fpToFP(x_1_64, DOUBLE)>

1
2
3
4
5
6
>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> a.val_to_bv(12)
<BV12 0x3>
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
<FP32 FPV(3.0, FLOAT)>
- 12비트 비트벡터 값 3
- 부동 소수점 FPV 3.0

# More Solving Methods
- solver.eval(표현식) : 주어진 표현식에 하나의 솔루션을 제공
- solver.eval_one(expressioN) : 주어진 표현식에 하나의 솔루션을 주거나 솔루션이 하나 이상일 때 에러를 던짐
- solver.eval_upto(expression, n) : n개의 솔루션을 제공, n개보다 적으면 n개보다 적게 반환
- solver.eval_atleast(expression, n) : 주어진 표현식에 n개의 솔루션을 제공 적다면 에러를 던짐
- solver.eval_exact(expression, n) n개의 솔루션을 제공하고 n보다 적거나 많으면 에러를 던짐
- solver.min(expression) : 가능한 적은 솔루션을 제공
- solver.max(expression) : 가능한 많은 솔루션을 제공
- extra_constraints : 제약조건 튜플을 넘김.
- cast_to : 결과로 캐스팅할 데이터 타입을 넘김.

# Program State
- 메모리, 레지스터 기타 등등의 상태
  • angr에서 프로그램 상태는 시뮬레이트된 프로그램 상태이고 그 상태는 SimState 객체의 값을 의미합니다.
- 메모리와 레지스터 읽기 쓰기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> import angr
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()
 
# copy rsp to rbp
>>> state.regs.rbp = state.regs.rsp
 
# store rdx to memory at 0x1000
>>> state.mem[0x1000].uint64_t = state.regs.rdx
 
# rdx에 값 1을 추가한 후 할당
>>> state.regs.rdx = 0x1
>>> state.mem[0x1000].uint64_t = state.regs.rdx
>>> state.mem[0x1000].uint64_t
<uint64_t <BV64 0x1> at 0x1000>
 
# dereference rbp
>>> state.regs.rbp = state.mem[state.regs.rbp].uint64_t.resolved
 
# add rax, qword ptr [rsp + 8]
>>> state.regs.rax += state.mem[state.regs.rsp + 8].uint64_t.resolved
- 기본 실행
  • 기본적인 작업에 단계 진행으로 state.step() 은 기호 실행의 한단계를 수행하고 SimSuccessors 객체를 반환합니다.
    • 이 객체는 자신의 후속 successor 상태를 포함하는 리스트로 반환됩니다.
    • 이 후속상태는 기호 실행마다 변합니다(mutate). 
  • true 분기와 false분기의 state 둘다 생성됩니다. 한번은 true 조건의 시뮬레이션 한번은 false 조건의 시뮬레이션을 진행합니다. 
    • 예시로 x > 4 분기의 true 조건이 한번 실행 되면, !(x>4) 조건 분기도 한번 실행 되어 두가지 successor state를 남깁니다.

- fake firmware image를 사용한 예제
  • 어떤 유저네임도 admin으로 인증되는 백도어 Password SOSNEAKY.

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> proj = angr.Project('examples/fauxware/fauxware')
>>> state = proj.factory.entry_state()
>>> while True:
...     succ = state.step()
...     if len(succ.successors) == 2:
...         break
...     state = succ.successors[0]
 
>>> state1, state2 = succ.successors
>>> state1
<SimState @ 0x400629>
>>> state2
<SimState @ 0x400699

- 제약조건을 풀고 입력으로 가능한 값을 찾기위해, 표준입력으로 실제 내용을 참조할 필요가 있습니다. 
  • 나중에 다루지만 현재는 아래 명령으로 간략히 사용합니다.
    state.posix.files[0].all_bytes()
1
2
3
4
5
6
7
>>> input_data = state1.posix.files[0].all_bytes()
 
>>> state1.solver.eval(input_data, cast_to=str)
'\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00\x00\x00'
 
>>> state2.solver.eval(input_data, cast_to=str)
'\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x00\x80N\x00\x00 \x00\x00\x00\x00'
- true인 state1 경로에서는 일치하는 SOSNEAKY가 보입니다. false인 state2 경로에서는 보이지 않습니다.

- 상태 프리셋
  • 매번 사용하는 project.factory.entry_state()는 프로젝트 팩토리에서 제공하는 상태 구조체중 하나일 뿐입니다. 아래의 몇가지가 더 있습니다.
    • .black_state() : 대부분의 데이터가 초기화 되지않은 상태 구조체, 이 구조체에 접근할 때 제한되지 않은 기호값이 리턴 됩니다.
    • .entry_state() : 메인 바이너리에서 실행할 준비가 된 상태 구조체
    • .full_init_state() : 메인 바이너리의 EP전 시행할 필요가있는 이니셜라이저를 통해 실행할 준비가된 상태 구조체
      • 예를 들면 공유 라이브러리 구조체나 사전 초기화 프로그램이 있습니다.
      • 실행이 다되면 EP로 이동합니다.
    • .call_state() : 주어진 함수를 실행할 준비가된 상태 구조체
  • 특징
    •  모든 구조체는 정확한 시작주소를 제공 받음. .addr
    • args를 통해 커맨드 인자를 제공 가능
    • argc를 기호적으로 사용하려면, argc 비트벡터를 entry_state나 full_init_state에 줄 수 있음
- 메모리의 저레벨 인터페이스
  • .state.mem 인터페이스 : 메모리에서 로드된 데이터에 대한 유용한 기능 제공
  • state.memory : 데이터 유형 > 데이터 로드와 저장의 통로로 사용될 목적이지만, endness 인자에 넣어줄 키워드로도 넘길 수 있습니다. 
    • 이 유형은 바로 .load(addr,size)나 .store(addr, val) 메소드에 사용가능
1
2
3
4
>>> s = proj.factory.blank_state()
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef0123456789abcdef128))
>>> s.memory.load(0x40046# load-size is in bytes
<BV48 0x89abcdef0123>
  • 빅엔디안으로 저장
1
2
3
>>> import archinfo
>>> s.memory.load(0x40004, endness=archinfo.Endness.LE)
<BV32 0x67453201>
  • 리틀엔디안으로 로드
- 상태 플러그인
  • SimState안에 저장되는 모든 것은 실제로 사태에 붙는 플러그인 안에 저장됩니다. 이 속성들은 memory, registers, mem, regs, solver, 기타 등등에 관한 것입니다.
  • 이 디자인은 코드 모듈성 뿐만 아니라 에뮬레이트된 상태의 다른 측면이나, 대체하여 플러그인을 수행하는 능력에 대한 새로운 종류의 데이터 스토리지를 쉽게 구현하는 것을 허용합니다.
  • 실제 메모리 플러그인은 단편 메모리 공간에서 시뮬레이션하지만, 분석은 추상 메모리 플러그인을 선택할 수 있습니다.
  • 글로벌 플러그인 : state.global : 상태에 임의의 데이터를 저장하는 것을 가능하게하는 표준 파이썬 dict의 인터페이스를 제공합니다.
  • 히스토리 플러그인 : state.histroy : (매우중요) 실행중 가져온 상태의 경로에 대한 기록적인 데이터 저장을하는 중요한 플러그인 입니다.
    • 히스토리 노드 연결 리스트로 구현되고, 매 노드는 단일 실행을 나타냅니다. 
    • 추적 : state.history.parent.parent
    • 이 플러그인은 효과적인 이터레이터(반복자)를 제공합니다.
      • history.recent_NAME : (반복자 : history.NAME)
      • ex)
      • 1
        for addr in state.history.bbl_addrs: print hex(addr)
      • 위 예시는 바이너리의 기본 블럭 주소 추적을 출력합니다.
      • 위 state.history.recent_bbl_addrs는 가장 최근 단계에 실행한 기본 블록 리스트입니다.
      • 특정 리스트를 얻어야한다면 .hardcopy를 통해 얻어냅니다.
        • ex) state.history.bbl_addrs.hardcopy
    • 히스토리로 저장되는 값의 간략한 목록
      • history.description : 상태에서 수행된 실행의 각각 하나를 설명한 문자열 목록
      • history.bbl_addrs :  상태에 대해 실행된 기본 블럭 주소의 목록. 매번 둘 이상의 실행이 있을 수 있으며 모든 주소가 바이너리 코드와 일치하지 않을 수 있습니다.
      • history.jumpkinds : VEX enum로 역사 제어 흐름 변화 정책 목록. 
      • history.guards : 상태를 마주쳤을 때의 매 분기 마다 막는 조건 목록
      • history.events : "interesting events" 의 의미론적인 목록입니다. 기호 점프 조건의 존재나, 메세지 박스를 띄우는 프로그램 팝업, 종료 코드의 실행을 예로 들 수 있습니다.
      • history.actions : 실제로 비어있지만, angr.options.refs을 상태에 추가한다면, 모든 메모리의 기록과 프로그램이 수행한 임시값 접근의 로그를 올릴것입니다.
  • 호출스택 플러그인
    • angr는 에뮬레이트 프로그램의 콜 스택을 추적합니다.
    • 모든 호출 명령 마다 추적된 콜스택의 상위에 프레임을 추가합니다.
    • 그 때 마다 스택 포인터는 프레임의 가장 상위를 가리키는 아래로 떨어지고 프레임은 올라갑니다.
    • 히스토리 플러그인과 비슷하게 호출스택 플러그인은 연결리스트 노드로 구성되지만, 이터레이터(반복자)를 제공하지 않습니다.  
    • state.callstack : 최신에서 오래된 것으로 정렬된 활성화된 프레임의 호출 스택을 얻어냅니다.
    • 가장 상위 프레임을 얻길 원한다면, stat.callstack을 씁니다.
      • callstack.func_addr : 현재 실행되어진 함수의 주소
      • callstack.call_site_addr : 현재 함수를 호출한 기본 블록의 주소
      • call.stack.stack_ptr : 현재 함수의 시작 스택 포인터의 값
      • callstack.ret_addr : 현재 함수가 반환한 위치값
  • Posix 플러그인 아직 설명안함 (가이드에서)
- 파일 시스템에서 작동
  • 상태 초기화 시키기
    • concrete_fs , chroot 옵션 사용
    • fs 옵션을 사용하면 SimFIle 객체에 파일 이름을 전달하여 초기화 시킬 수 있습니다. (파일 내용에 대한 구체적인 크기 제한을 설정하는 등)
    • concrete_fs :현재 파일 시스템의 파일을 시뮬레이션 도중 사용합니다. (True로 활성화)
      • 존재하지 않는다면 오류를 발생시킵니다.
    • chroot : 이 옵션으로 선택적인 루트 경로를 지정할 수 있습니다. 
    • 1
      2
      >>> files = {'/dev/stdin': angr.storage.file.SimFile("/dev/stdin""r", size=30)}
      >>> s = proj.factory.entry_state(fs=files, concrete_fs=True, chroot="angr-chroot/")
- 복사와 합치기
  • 현재 프로젝트의 특정 상태 복사와 합치기
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    >>> proj = angr.Project('/bin/true')
    >>> s = proj.factory.blank_state()
    >>> s1 = s.copy()
    >>> s2 = s.copy()
     
    >>> s1.mem[0x1000].uint32_t = 0x41414141
    >>> s2.mem[0x1000].uint32_t = 0x42424242
     
    # merge will return a tuple. the first element is the merged state
    # the second element is a symbolic variable describing a state flag
    # the third element is a boolean describing whether any merging was done
    >>> (s_merged, m, anything_merged) = s1.merge(s2)
     
    # this is now an expression that can resolve to "AAAA" *or* "BBBB"
    >>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t
# Simulation Manager
- Stepping : 시뮬레이션의 기본 블록을 진행 시킵니다.
1
2
3
4
5
6
7
8
9
10
>>> import angr
>>> proj = angr.Project('examples/fauxware/fauxware', auto_load_libs=False)
>>> state = proj.factory.entry_state()
>>> simgr = proj.factory.simgr(state)
>>> simgr.active
[<SimState @ 0x400580>]
 
>>> simgr.step()
>>> simgr.active
[<SimState @ 0x400540>]

- 첫번째 기호 분기를 만날 때 까지 진행
1
2
3
4
5
6
7
8
9
10
11
12
13
# Step until the first symbolic branch
>>> while len(simgr.active) == 1:
...    simgr.step()
 
>>> simgr
<SimulationManager with 2 active>
>>> simgr.active
[<SimState @ 0x400692><SimState @ 0x400699>]
 
# Step until everything terminates
>>> simgr.run()
>>> simgr
<SimulationManager with 3 deadended>

- Stash 관리
  • stash 사이 상태를 하려면 .move()를 사용합니다. 예로 아래 코드는 결과에서 특정 문자열을 갖는 모든것을 이동시킵니다. (의역이나 아직 사용해보지 않아서 이동이 제거의 의미인지 변경의 의미인지 파악하지 못했습니다.)
  • 1
    2
    3
    >>> simgr.move(from_stash='deadended', to_stash='authenticated', filter_func=lambda s: 'Welcome' in s.posix.dumps(1))
    >>> simgr
    <SimulationManager with 2 authenticated, 1 deadended>
- stash는 단순히 목록(리스트)이며, 인덱스나 반복자를통해서 각각 특정 상태에 접근할 수 있습니다. 이외에 도 상태에 접근하는 몇가지 대체 메소드가 있습니다.
  • one_이 statsh에 붙는다면 첫번째 상태를 의미하고 mp_이붙는다면 다중화된 버전의 MP stash가 반환됩니다.
  • 또한 step, run과 단일 stash 경로에 대한 명령을 하는 다른 메소드는 수행할 stash를 지정하는 stash 인자를 가져갈 수 있습니다.
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> for s in simgr.deadended + simgr.authenticated:
    ...     print hex(s.addr)
    0x1000030
    0x1000078
    0x1000078
     
    >>> simgr.one_deadended
    <SimState @ 0x1000030>
    >>> simgr.mp_authenticated
    MP([<SimState @ 0x1000078><SimState @ 0x1000078>])
    >>> simgr.mp_authenticated.posix.dumps(0)
    MP(['\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00',
        '\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x80\x80\x80\x80@\x80@\x00'])
- Stash 유형
Stash
Description
active
This stash contains the states that will be stepped by default, unless an alternate stash is specified.
deadended
A state goes to the deadended stash when it cannot continue the execution for some reason, including no more valid instructions, unsat state of all of its successors, or an invalid instruction pointer.
pruned
When using LAZY_SOLVES, states are not checked for satisfiability unless absolutely necessary. When a state is found to be unsat in the presence of LAZY_SOLVES, the state hierarchy is traversed to identify when, in its history, it initially became unsat. All states that are descendants of that point (which will also be unsat, since a state cannot become un-unsat) are pruned and put in this stash.
unconstrained
If the save_unconstrained option is provided to the SimulationManager constructor, states that are determined to be unconstrained (i.e., with the instruction pointer controlled by user data or some other source of symbolic data) are placed here.
unsat
If the save_unsat option is provided to the SimulationManager constructor, states that are determined to be unsatisfiable (i.e., they have constraints that are contradictory, like the input having to be both "AAAA" and "BBBB" at the same time) are placed here.

- 단순 탐색
  • 기호 실행에서 아주 흔한 명령어는 다른 주소로 가도록하는 모든 상태를 던지는 특정 주소에 도달하는 상태를 찾는 것 입니다.
  • 시뮬레이션 매니저는 이 패턴을 .explore() 메소드로 제공합니다.
  • find 인자와 함께 .explore()를 실행할 때 정지 명령 주소나 정지 명령 주소의 목록 또는 상태를 가져고 특정 범주에 맞는지 반환하는 함수가 될 수 있는 find 조건에 맞는 것을 찾은 상태까지 실행할 것입니다.
  • 활성화된 stash에서 어떤 state가 find 조건과 일치할 때, 그 상태를  found stash로 이동 시킵니다. 그리고 실행이 종료됩니다. 이 found state를 탐색할 수 있고, 이것을 버릴지 다른 하나와 함께 계속 쓸지 결정할 수 있습니다.
  • 또한 find와 같은 형식의 avoid 조건을 지정할 수 있습니다. avoid과 일치할 때 avoided stash에 넣어지고 실행은 계속 됩니다. 마지막으로 num_find 인자는 (기본값 1) 반환 전 찾아 질 수 있는 다수의 상태를 제어합니다. 
  • 많은 해결책을 찾기 전까지 활성화된 stash에서 state가 다 끝나면, 무조건 실행이 멈춥니다. 
    • simple crackme
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      >>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')
       
      # Simulation 매니저 생성
      >>> simgr = proj.factory.simgr()
       
      # win 조건에 맞는 상태를 찾을 때 까지기호 실행
      >>> simgr.explore(find=lambda s: "Congrats" in s.posix.dumps(1))
      <SimulationManager with 1 active, 1 found>
       
      # 플래그 
      >>> s = simgr.found[0]
      >>> print s.posix.dumps(1)
      Enter password: Congrats!
       
      >>> flag = s.posix.dumps(0)
      >>> print(flag)
      g00dJ0B!
- 탐색 기술

  • angr는 탐색 기술로 불리는 시뮬레이션 매니저의 행동을 커스터마이즈하여 기능적으로 뭉친   몇가지 조각들을 사용합니다.
  • DFS: 깊이 우선 탐색, deferred stash안의 나머지가 deadends나 에러에 도달할 때 까지 한번 활성화한 오직 하나의 상태를 유지합니다.
  • LoopLimiter: 많은 수의 루프를 수행하는 경향의 상태를 버리는 대략적인 루프 카운트를 사용합니다. 이 카운트는 spinning stash안에 넣어지고 다르게 실행한 상태를 벗어나면 꺼내집니다.
  • LengthLimiter: 상태가 통과하는 최대 길이의 경로 제한점을 둡니다.
  • ManualMergepoint: 프로그램에서 합병 포인트로 주소를 정합니다. 그렇게 상태는 잠깐 동안 유지되고, 다른 상태가 다른 상태가 시간 제한안에 같은 지점이 도달하면 합쳐집니다.
  • Veritesting: 시뮬레이션 매니저 구조체에서 veritesting 인자가 True일 때 합병 지점을 자동으로 식별합니다.
  • Tracer: 소스를 기록하는 동적 추적 탐색 기술
  • Oppologist: The "operation apologist" is an especially fun gadget - if this technique is enabled and angr encounters an unsupported instruction, for example a bizzare and foreign floating point SIMD op, it will concretize all the inputs to that instruction and emulate the single instruction using the unicorn engine, allowing execution to continue.
  • Threading: Adds thread-level parallelism to the stepping process. This doesn't help much because of python's global interpreter locks, but if you have a program whose analysis spends a lot of time in angr's native-code dependencies (unicorn, z3, libvex) you can seem some gains.
  • Spiller: When there are too many states active, this technique can dump some of them to disk in order to keep memory consumption low.


반응형