Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

D1N0's hacking blog

03_angr_symbolic_registers 본문

Reversing/angr_ctf

03_angr_symbolic_registers

D1N0 2021. 3. 26. 10:06

지난번 글을 보지 않았다면 먼저 보고 오자

 

02_angr_find_condition

지난번 글을 보지 않았다면 먼저 보고 오자 01_angr_avoid 지난 글을 보지 않았다면 먼저 보고 오자 00_angr_find 들어가기 전에 이 글은 angr에 대해 배운 내용을 정리하는 글이다 C와 파이썬, 간단한 리

d1n0.tistory.com

시작

이제부턴 angr를 더 폭넓게 사용하는 방법을 살펴보겠다

전까지는 단순히 입력과 출력만 가지고 문제를 풀었다면 이번에는 레지스터의 값을 사용해 볼 것이다

예제를 먼저 보도록 하자

undefined4 main(void)

{
  int iVar1;
  int iVar2;
  int iVar3;
  undefined4 unaff_EBX;
  undefined8 uVar4;
  
  printf("Enter the password: ");
  uVar4 = get_user_input();
  iVar1 = complex_function_1((int)uVar4);
  iVar2 = complex_function_2(unaff_EBX);
  iVar3 = complex_function_3((int)((ulonglong)uVar4 >> 0x20));
  if (((iVar1 == 0) && (iVar2 == 0)) && (iVar3 == 0)) {
    puts("Good Job.");
  }
  else {
    puts("Try again.");
  }
  return 0;
}

기드라로 까면 이렇게 생겼다

주목할 부분은 get_user_input함수인데, 까 보면 다음과 같다

undefined8 get_user_input(void)

{
  int in_GS_OFFSET;
  undefined4 local_1c;
  undefined local_18 [4];
  undefined4 local_14;
  int local_10;
  
  local_10 = *(int *)(in_GS_OFFSET + 0x14);
  __isoc99_scanf("%x %x %x",&local_1c,local_18,&local_14);
  if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return CONCAT44(local_14,local_1c);
}

보면 "%x %x %x" 포맷 스트링을 활용해 16진수를 입력을 받는 것을 확인할 수 있다

여기가 문제인데, 분명 %x %x %x면 00000000 00000000 00000000 꼴의 입력일 텐데, angr가 이 긴 입력을 구하려면 너무 오래 걸릴 것이다

따라서 다른 방법을 찾아봐야 하는데, 불행히도 angr는 포맷 스트링 문자열에 대한 부분을 지원하지 않는다

입력을 받는 부분의 어셈을 보자

다행히 입력 받은 값들이 eax, ebx, edx로 들어가는 것을 확인할 수 있다

우리는 이를 이용해서 답을 찾아볼 것이다

 

scaffold03.py을 보자

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)
  
  start_address = ???
  initial_state = project.factory.blank_state(addr=start_address)

  password0_size_in_bits = ???
  password0 = claripy.BVS('password0', password0_size_in_bits)

  initial_state.regs.??? = password0

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.se.eval(password0)

    solution = ???
    print solution
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)
import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)
  
  start_address = ???  # :integer (probably hexadecimal)
  initial_state = project.factory.blank_state(addr=start_address)

가장 먼저 눈에 들어오는 건 전에 없던 claripy를 import 하는 것이다

이에 대해선 조금 이따가 살펴보겠다

그거 말곤 전과 별반 다를 바 없으니 위의 8줄을 가볍게 넘기고 start_address를 보도록 하겠다

이 start_address가 어디에 쓰이는지 봤더니 아랫줄 blank_state의 인자로 넣고 있다

지금껏 entry_state()로 initial_state를 설정하다가 갑자기 새로운 함수가 등장했다

project.factory.blank_state는 angr 프로젝트의 시작점을 설정하는 함수이다

사실 우리가 entry_state()로 사용하던 것도 angr 프로젝트의 시작점을 프로그램 entry point로 설정하는 것이다

이걸 시작점이라고 부르기는 했지만 사실 angr의 state를 만들고 나중에 이를 initial_state로 사용하는 것인데,

일단 넘어가기로 하고 state에 대해 궁금하다면 docs.angr.io/core-concepts/states를 참고하자

아무튼 결론은 우리는 평소 쓰던 entry_state대신 blank_state을 통해 시작 주소를 설정할 것이고, 그 시작 주소가 start_address라는 말이다

 

이 start_address를 정하는 게 정말 중요한데, 이를 정하기 위해 우리가 구하고자 하는 것을 먼저 봐야 한다

우리는 %x %x %x로 입력받는 값을 알고 싶고, 그 3개의 값이 eax, ebx, edx로 가는 것을 알고 있다

다시 말하면, 입력을 받은 후에 eax, ebx, edx에 우리가 입력해야 하는 값(앞으로는 정답이라 하겠다)이 들어가야 된다는 뜻이다

그러니까, 입력 후에 eax, ebx, edx에 정답이 들어있다면 complex_function을 거친 후 Good Job이 나온다는 말이다

아까 잠깐 말했듯, 우리는 angr에 레지스터의 값을 조건으로 줄 것이다

그러니 start_address는 eax, ebx, edx에 정답이 들어있으면 Good Job을 뱉어내는 위치로 설정해야 한다

그러면 그 위치가 어디냐,

를 보기 전에 get_user_input 함수가 끝나고 main에서 저 입력값을 어떻게 사용하는 지를 보겠다

보면 바로 스택에 값들을 넣는다

그러고 나서는 eax, ebx, edx를 다른 용도로 사용한다

때문에 이 뒤에는 레지스터에 다른 값이 있어도 상관없다

결론은 뭐냐면 eax, ebx, edx에 정답이 들어있을 때 Good Job이 나오는,

즉 우리가 start_address에 넣어야 하는 주소는 입력값이 레지스터에 들어간 후부터 레지스터의 값을 main의 스택에 넣기 전까지의 주소인 것이다

그렇다면 그 범위 내의 아무 주소나 넣으면 될까?

마지막 하나의 조건이 더 있는데, 바로 main의 주소를 넣어야 한다는 것이다

현재 우리는 표준 출력의 결과에 따라 입력값이 남즌지 틀린지를 판단한다

근데 현재 프로그램에서 문자열의 출력은 main함수에서 일어난다

그러나 angr의 explore는 시작한 함수 안에서만 돌아가가 때문에, 만약 우리가 get_user_input 내의 주소를 시작 주소로 삼게 되면 표준 출력이 일어나지 않는다

그러면 우리가 넣을 수 있는 주소는 get_user_input이 끝난 바로 직후인 0x08048980밖에 남지 않는다

 

정리하면

1. 이 프로그램은 입력값을 레지스터에 넣고 사용한다

2. 레지스터에 정답이 들어있으면 Good Job를 내놓는 때가 있다

3. get_user_input함수에서 시작하면 표준 출력에 아무것도 나오지 않기 때문에 start_address는 main에서의 주소여야 한다

4. 2번과 3번의 교집합은 0x08048980뿐이다

 

무튼 그리하여 우리는 start_address=0x08048980라는 코드를 짤 수 있는 것이다

 

password0_size_in_bits = ???  # :integer
password0 = claripy.BVS('password0', password0_size_in_bits)

initial_state.regs.??? = password0

이제 드디어 레지스터를 다뤄볼 것이다

먼저 구하려는 값의 비트를 알아야 한다

앞서 말했듯 우리가 구하려는 값은 레지스터 eax, ebx, edx이고, 이것들은 모두 32bit 레지스터이므로 32를 넣으면 된다

두 번째 줄에는 이번에 처음 보는 BVS가 있다

BVS는 아까 import 했던 claripy의 매서드이다

그렇다면 이 claripy는 뭘까?

claripy는 angr의 Solver Engine이고, 기본적으로 z3 solver 같은 SMT solver처럼 사용할 수도 있다

거기에 추가적으로 angr와 엮어서 사용하는 것이 가능한데, 이는 나중에 다루기로 하자

claripy에 대해서는 angr.io/api-doc/claripy.html를 읽어보면 알 수 있다

아무튼 BVS는 bitvector를 만드는 기능을 한다

이 bitvector는 angr에서 바이너리에 사용하는, 일종의 미지수 같은 데이터 타입이라고 생각하면 된다

그래서 BVS의 첫 번째 인자에는 bitvector의 이름이, 두 번째에는 bitvector의 크기(비트 수)가 들어간다

password0 = claripy.BVS('password0', password0_size_in_bits)의 뜻은

password0이라고 부를 32비트 bitvertor변수 password0을 선언하겠다 라는 것이다

그다음 줄은 바로 아까 만든 initial_state의 레지스터를 password0의 값으로 선언하는 것이다

무슨 말이냐면, get_user_input함수가 끝난 직후의 그 상태에서의 레지스터를 미지수로 두겠다는 의미이다

이를 위해 아까 start_address를 그렇게 구한 것이다

따라서 ???에는 우리가 구하고자 하는 레지스터인 eax를 넣어주면 된다(ebx나 edx도 괜찮지만 password0인만큼 첫 입력값인 eax를 선택했다)

추가적으로 우리는 ebx, edx도 구해야 하니 위 코드를 복사해서 password1, password2를 ebx, edx에 대입해준다

 

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.se.eval(password0)

    solution = ???  # :string
    print solution
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

나머지 부분은 전과 다른 게 거의 없다

is_successful함수는 return b'Good Job.' in stdout_output를, should_abort함수는 return b'Try again.' in stdout_output를 반환하면 된다

다만 solution을 구하는 방식이 조금 다른데, 전에는 표준 입력에 어떤 것이 들어갔는지를 solution에 넣었다면, 이번에는 아까 설정했던 레지스터 값들을 알아야 한다

이제 아까 설정했던 password0, 1, 2를 사용한다

angr의 state객체에는 SimSolver 인스턴스인 solver가 있고,

그 solver안의 eval 매소드를 통해 아까 만들었던 bitvector에 들어가야 하는 값을 알 수 있다

위 코드처럼 solution_state.se.eval(password0)처럼 사용하면 된다

그럼 여기서 한 가지 의문이 생긴다

분명 state안에는 solver가 있다면서 뜬금없이 se는 뭐냐는 것인데, 별거 없고 solver를 지칭하는 또 다른 이름이라고 보면 된다

근데 이 se가 딱히 권장되지 않으니 우리는 se 대신 solver를 사용하도록 하자

무튼 password0, 1, 2에 대한 solution0, 1, 2를 각각 만들면 된다

 

마지막 solution에 solution0, 1, 2를 모아서 출력하면 되는데, 그냥 따로따로 출력해도 되지만 보기 좋은 떡이 먹기도 좋다고 깔끔하게 출력해보자

우리가 구하는 입력값은 %x %x %x꼴로 입력을 받기 때문에 출력도 %x %x %x꼴로 해주었다

 

따라서 이 모든 것을 종합한 코드는 다음과 같다

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = 0x08048980
  initial_state = project.factory.blank_state(addr=start_address)

  password_size_in_bits = 32
  password0 = claripy.BVS('password0', password_size_in_bits)    
  password1 = claripy.BVS('password1', password_size_in_bits)    
  password2 = claripy.BVS('password2', password_size_in_bits)    


  initial_state.regs.eax = password0
  initial_state.regs.ebx = password1
  initial_state.regs.edx = password2

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Good Job.' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Try again.' in stdout_output

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.solver.eval(password0)
    solution1 = solution_state.solver.eval(password1)
    solution2 = solution_state.solver.eval(password2)

    print("%x %x %x" % (solution0, solution1, solution2))
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

실행하면 b9ffd04e ccf63fe8 8fd4d959가 나온다

Good Job도 잘 나오는 것을 알 수 있다

 

지금까지 레지스터 값을 활용해 어떤 입력이 필요한지 알아내는 방법을 알아보았다

다음번에는 레지스터 말고 stack을 사용하는 방법을 알아보겠다

'Reversing > angr_ctf' 카테고리의 다른 글

04_angr_symbolic_stack  (0) 2021.04.10
02_angr_find_condition  (0) 2021.02.26
01_angr_avoid  (0) 2021.02.26
00_angr_find  (0) 2021.02.24
Comments