what is angr & code analyze
위 문제를 풀면서
제작한 angr코드를 바탕으로 설명한다.
angr는 심볼릭 익스큐션을 위한 유틸리티로 내부적으로 SMT ( z3 ) 를 사용하면서
유추를 할 수 있는 방식이다.
리버싱 문제를 풀기 위해서 자주 사용되는 툴로 지금부터 같이 알아보도록 하자
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는 같이 가는 구조이다.
코드를 분석하자면,
p = angr.Project("chall1.exe" , auto_load_libs=False)angr.Project()로 일단 바이너리를 연다. ( PE 파일 , ELF 파일 ) 둘다 가능
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 를 하면 된다.
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와의 호환성을 위해 필요하다 )
이둘을 출력해보면,
<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 은 첫번째 인자이다. 여기서는 문자열의 주소가 된다.
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 에 도달하는 경우를 찾는 것인데
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")이제 만약 해당 success_addr 에 도달하는 경우가 있다면 해당 경우의 상태를 가져와서 symbolic_string 변수에 담긴 값을 가져오는 코드이다.