what is angr & code analyze

위 문제를 풀면서

제작한 angr코드를 바탕으로 설명한다.

angr는 심볼릭 익스큐션을 위한 유틸리티로 내부적으로 SMT ( z3 ) 를 사용하면서

유추를 할 수 있는 방식이다.

리버싱 문제를 풀기 위해서 자주 사용되는 툴로 지금부터 같이 알아보도록 하자

Python
import pefile
import angr
import claripy

pe = pefile.PE("chall1.exe")

print(pe.OPTIONAL_HEADER.ImageBase)

p = angr.Project("chall1.exe" ,  auto_load_libs=False)


base_addr = p.loader.main_object.min_addr

print(hex(base_addr))

start_addr = base_addr + 0x1000
success_addr = base_addr + 0x1289
#avoid_addr = base_addr + 0x1289
data_addr =  base_addr + 0x4680

input_size = 22
symbolic_string = claripy.BVS("input_string", input_size * 8)
symbolic_address = claripy.BVV(data_addr, p.arch.bits)
state = p.factory.call_state(
    start_addr, 
    symbolic_address  
)

state.memory.store(data_addr, symbolic_string)

simgr = p.factory.simulation_manager(state)

simgr.explore(find=success_addr)
#simgr.explore(find=success_addr, avoid=avoid_addr)

if simgr.found:
    solution_state = simgr.found[0]
    solution = solution_state.solver.eval(symbolic_string, cast_to=bytes)
    print(solution)
else:
    print("Solution not found")
     

해당 문제에서 내가 사용한 코드는 다음과 같다.

일단c laripy는 심볼릭 변수의 표현식을 생성하고 제어하는 라이브러리로써 claripy랑 angr는 같이 가는 구조이다.

코드를 분석하자면,

Python
p = angr.Project("chall1.exe" ,  auto_load_libs=False)

angr.Project()로 일단 바이너리를 연다. ( PE 파일 , ELF 파일 ) 둘다 가능

Python
base_addr = p.loader.main_object.min_addr

start_addr = base_addr + 0x1000
success_addr = base_addr + 0x1289
#avoid_addr = base_addr + 0x1289
data_addr =  base_addr + 0x4680

위에서 받은 p로부터 메인 모듈의 min_addr 의 주소를 가져온다.

메인 모듈이면 당연히 main 함수가 있는 chall1.exe가 될것이다. 이것의 min_addr이면 chall1.exe 의 imagebase 라고 보면 된다. ( angr는 직접 메모리에 올리는게 아니기 때문에 ASLR 영향 x )

그리고 이 주소로 부터 내가 원하는 offset을 더해서

시작 주소 → 주로 함수의 시작

원하는 도착 주소 → 함수의 정상적인 끝

피해야 하는 주소 → 중간에 라이브러리로 빠지는 코드나 분기에 의한 비정상 종료

을 설정한다. offset은 ida로 보았을때 명령어가 있는 주소 - imagebase 를 하면 된다.

Python
input_size = 22
symbolic_string = claripy.BVS("input_string", input_size * 8)
symbolic_address = claripy.BVV(data_addr, p.arch.bits)
state = p.factory.call_state(
    start_addr, 
    symbolic_address  
)

이제 claripy 를 사용해 심볼릭 변수를 생성해주면 된다.

변수 생성에는 두가지가 있는 BVS BVV 이렇게 대표적으로 2개가 있다.

BVS 는 이제 내가 아직 모르는 값 ( 내가 angr를 통해 유추해내고 싶은 값 ) 를 설정한다.

BVV 는 상수값을 설정한다. 확실히 정하는 값이다. *불변

지금 내 코드에서는 22byte의 ( 22 * 8bit ) 심볼릭 변수를 설정해 input_string라는 별명을 붙인 것이다.

그리고 이제 data_addr 라는 값을 상수로 symbolic_address 라는 심볼릭변수로 저장한다. ( 굳이? 라고 생각할 수 도 있지만 angr 내에 smt와의 호환성을 위해 필요하다 )

이둘을 출력해보면,

Python
<class 'claripy.ast.bv.BV'>
<BV176 input_string_0_176>
<BV64 0x140004680>

다음과 같이 class 객체로써 첫번째 BVS에는 아직 안정해진 변수인 input_string_0_176 이 들어가 있고 BVV에는 0x140004680 라는 상수가 들어가 있음을 알 수 있다.

이제 p.factory.call_state 문장으로 call 상황을 시뮬레이션을 하기 위한 상태설정을 한건데 첫번째 인자인 start_addr 은 함수가 시작하는 주소, symbolic_address 은 첫번째 인자이다. 여기서는 문자열의 주소가 된다.

Python
state.memory.store(data_addr, symbolic_string)

simgr = p.factory.simulation_manager(state)

simgr.explore(find=success_addr)

이제 기초적인 세팅은 완료되었고 첫번째 인자인 symbolic_address 에는 data_addr의 값이 들어가 있는데 이 주소에 어떤 데이터가 연결되어 있는지 설정해줘야 한다. 우리는 이 주소에 우리가 알고 싶은 문자열 ( 심볼릭 변수 ) 가 있으므로 state.memory.store 로 심볼릭 변수 객체를 해당 주소에 저장해준다.

그리고 나머지 두개의 문장은 angr에서 시뮬레이션을 시작하는 코드이다.

simgr.explore(find=success_addr) 이 코드가 이제 이전에 정한 success_addr 에 도달하는 경우를 찾는 것인데

Python
simgr.explore(find=success_addr, avoid=avoid_addr)

다음과 같이 피해야하는 주소도 같이 지정해 줄수 있다.

Python
if simgr.found:
    solution_state = simgr.found[0]
    solution = solution_state.solver.eval(symbolic_string, cast_to=bytes)
    print(solution)
else:
    print("Solution not found")

이제 만약 해당 success_addr 에 도달하는 경우가 있다면 해당 경우의 상태를 가져와서 symbolic_string 변수에 담긴 값을 가져오는 코드이다.