취약점분석/Reversing

[Reversing] angr CTF 문제 풀이 - 2 (04 ~ 07)

poiri3r 2026. 1. 2. 17:19

안녕하세요 오늘은 angr CTF 문제풀이 2번째 가져왔습니다.

4번째 문제부터 바로 들어가겠습니다.

 

04_angr_symbolic_stack

main은 아주 간단하고, handle_user()함수에서 사용자 입력을 처리하는데, 저희가 입력한 문자가 스택에 쌓이고, 스택에 쌓인 입력값을 비교해가면서 조건을 탐색해야 합니다. 

scaffold04.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,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
  initial_state.regs.ebp = initial_state.regs.esp

0 = claripy.BVS('password0', ???)
  ...
  padding_length_in_bytes = ???  # :integer
  initial_state.regs.esp -= padding_length_in_bytes
 
  initial_state.stack_push(???)  # :bitvector (claripy.BVS, claripy.BVV, claripy.BV)
  ...

  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.solver.eval(password0)
    ...

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

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

주석에 달려있던 내용을 요약해보겠습니다.

먼저, 시작지점을 잘 선택해야합니다. 전 문제와 마찬가지로 initial_state가 blank_stack으로 되어있는데, 스택 주소값도 알아야 할 것 같고, 스택 구성도 직접 해야될 것 같습니다.

문제의 의도는 알 것 같은데 스택에 대해 본 지 너무 오래되서 .. 가물가물하네요

어셈블리어를 들여다본지 너무 오래된 것 같은데 한번 분석해보겠습니다.

먼저 스택 베이스의 크기는 18h인걸 확인 가능하고, 

변수들의 위치는 [EBP-12]와 [EBP-16] 이네요

약 이런 형태로 형성되있는걸 알 수 있습니다

 start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

먼저 시작주소입니다.

스택베이스가 생기고  변수 위치가 설정 된 뒤의 주소로 start address를 잡아주겠습니다. 

initial_state.regs.ebp = initial_state.regs.esp
 password0 = claripy.BVS('password0', ???)
  ...

여기도 스택 상태를 설정하는 곳인데, ebp를 esp와 위치를 같게 설정해두었습니다.

인자가 두개 들어가기 때문에 password0이랑 1을 32비트로 만들어주겠습니다.

padding_length_in_bytes = ???  # :integer
  initial_state.regs.esp -= padding_length_in_bytes

 

padding값을 넣는 곳입니다. ebp에서 0x8까지 패딩이 존재하기 때문에 8을 넣어줍니다.

이러면 esp가 8만큼 감소되었으니 ebp-8에 esp가 존재할 것 입니다. esp-8 ~ esp-0C까지는 4바이트의 password0이 존재합니다.

 initial_state.stack_push(???)  # :bitvector (claripy.BVS, claripy.BVV, claripy.BV)
  ...

스택 푸쉬하는 코드입니다.

angr에서 initial_state.stack_push는 어셈블리어의 push와 똑같이 작동한다고 합니다.

해당 코드에는 password0먼저 들어간 뒤 password1이 들어가면 됩니다.

그리고 나머지 쉬운 부분들은 다 채웠습니다. 전체 익스 코드입니다.

import angr
import claripy
import sys

def main(argv):
  project = angr.Project("./04_angr_symbolic_stack")

  start_address = 0x8049392
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
 
  initial_state.regs.ebp = initial_state.regs.esp
  password0 = claripy.BVS('password0', 32)
  password1 = claripy.BVS('password1', 32)

 
  padding_length_in_bytes = 8  
  initial_state.regs.esp -= padding_length_in_bytes
 
  initial_state.stack_push(password0)
  initial_state.stack_push(password1)  

  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)
    print(f"{solution0}, {solution1}")
  else:
    raise Exception('Could not find the solution')

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

돌려보면

두 숫자가 나오고,

입니다.

조금 헷갈릴만한 부분을 추가하자면,

start address 이전의 어셈블리 코드들을 세팅해서 이후 프로그램 진행에 문제가 발생 안하도록 진행하는 것 입니다

 

05_angr_symbolic_memory

제목에서부터 알 수 있듯 symbolic memory에 관한 문제입니다.

 start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}

시작 주소는 scanf이후의 주소로 넣어주겠습니다.

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

이것도 이전 문제들과 비슷하게 password0의 크기를 설정해주는데, 문제 main 코드를 보면 8바이트 입력을 받으므로 8*8해서 64로 설정해줍니다.

  # The binary is calling scanf("%8s %8s %8s %8s").
  # (!)
  password0 = claripy.BVS('password0', 64)
  password1 = claripy.BVS('password1', 64)
  password2 = claripy.BVS('password2', 64)
  password3 = claripy.BVS('password3', 64)

다음 password의 주소입니다.

 password0_address = ???
  initial_state.memory.store(password0_address, password0)
  ...

initial_state.memory.store은 가상 메모리의 특정 주소에 값을 주입해줍니다.

해당 값들은 IDA에서 확인해서 넣어주겠습니다.

해당 주소들에 맞게 넣었습니다.

전체 코드입니다.

import angr
import claripy
import sys

def main(argv):
 
  project = angr.Project("./05_angr_symbolic_memory")

  start_address = 0x8049289
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
 
  password0 = claripy.BVS('password0', 64)
  password1 = claripy.BVS('password1', 64)
  password2 = claripy.BVS('password2', 64)
  password3 = claripy.BVS('password3', 64)
 
  password0_address = 0x094BFE00
  initial_state.memory.store(password0_address, password0)
  password1_address = 0x094BFE08
  initial_state.memory.store(password1_address, password1)
  password2_address = 0x094BFE10
  initial_state.memory.store(password2_address, password2)
  password3_address = 0x094BFE18
  initial_state.memory.store(password3_address, password3)
   

  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,cast_to=bytes).decode()
    solution1 = solution_state.solver.eval(password1,cast_to=bytes).decode()
    solution2 = solution_state.solver.eval(password2,cast_to=bytes).decode()
    solution3 = solution_state.solver.eval(password3,cast_to=bytes).decode()

    print(f"{solution0}, {solution1}, {solution2},{solution3} ")

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

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

실행결과입니다.

 

 

06_angr_symbolic_dynamic_memory

이번 문제는 동적 할당 메모리에 관한 문제입니다.

저희는 scanf_ 뒤에 password0과 password1을 만들거기 때문에 스택문제 풀 때 했던 것처럼, 가짜 힙을 하나 만들어서 거기에 변수를 설정해둬야할 듯 합니다.

fake_heap_address0 = ???
pointer_to_malloc_memory_address0 = ???
initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  ...

fake_heap_address는 아무곳이나 넣어도 괜찮을 듯 싶습니다. 0x5000000으로 넣어줬습니다.

pointer_to_malloc_memory_address0은 포인터 변수의 주소입니다.

해당 주소의 값들을 채워주었습니다.

fake_heap_address0 = 0x50000000
  pointer_to_malloc_memory_address0 = 0x09F3C328
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  fake_heap_address1 = 0x50000010
  pointer_to_malloc_memory_address0 = 0x09F3C32C
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)

 

solve 전문입니다.

import angr
import claripy
import sys

def main(argv):
  project = angr.Project("./06_angr_symbolic_dynamic_memory")

  start_address = 0x080492E0
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
 
  password0 = claripy.BVS('password0', 64)
  password1 = claripy.BVS('password1', 64)
 
  fake_heap_address0 = 0x50000000
  pointer_to_malloc_memory_address0 = 0x09F3C328
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  fake_heap_address1 = 0x50000010
  pointer_to_malloc_memory_address1 = 0x09F3C32C
  initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)
 
  initial_state.memory.store(fake_heap_address0, password0)
  initial_state.memory.store(fake_heap_address1, password1)


  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,cast_to=bytes).decode()
    solution1 = solution_state.solver.eval(password1,cast_to=bytes).decode()
 
    print(f"{solution0} {solution1}")
  else:
    raise Exception('Could not find the solution')

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

 

실행 결과입니다.

조금 어려운 부분이 있었는데, start address 기준으로 이전 할당과 같은 값들을 세팅해 나가는 과정들을 익히는게 툴의 폭 넓은 사용에 도움이 될 것 같습니다.

 

07_angr_symbolic_file

이번 문제는 파일을 심볼로 지정하는 문제입니다.

내부의 ignore_me 함수입니다.

전체적인 로직을 보면 사용자에게 64비트를 입력받고, HMGIBNZL파일에 기록합니다. 

그리고 그 파일에서의 우리가 입력한 비밀번호를 가져온 뒤 암호화하고 결과값을 비교합니다.

 

좀 복잡해보이는 느낌이 있는데, 목표를 설정해보면 HMGIBNZL라는 파일 심볼을 angr상에서 생성을 해서 들어가야하는 값이 뭔지 비교를 하는 거네요.

  filename = ???  # :string
  symbolic_file_size_bytes = ???

file name에는 HMGIBNZL.txt를 size는 64바이트니까 0x40을 넣어줬습니다.

password = claripy.BVS('password', symbolic_file_size_bytes * 8)

해당 코드는 저희가 미지수로 설정할 비밀번호를 설정하는 것입니다.

여기 주석으로 설명이 되어있는데

 # Hello world, my name is John.
  # ^                       ^
  # ^ address 0             ^ address 24 (count the number of characters)

이런 문장이 저장되어 있을 때, John이라는 4글자만 비밀번호로 알아내야 하는 경우 

name_bitvector = claripy.BVS('symbolic_name', 4*8)

형식으로 설정을 해서 4글자 * 8비트만 symbol로 설정을 할 수 있습니다.

하지만 저희는 모든 바이트를 알아내야 하므로 0x40이 저장되어 있는 symbolic_file_size_bytes를 넣어줍니다.

password_file = angr.storage.SimFile(filename, content=???)

해당 코드는 저희가 생성한 변수를 파일 심볼에 연결하는 코드입니다.

SimFile은 angr이 프로그램을 분석할 때 자신만의 가상 메모리를 사용하기에, 메모리상의 가짜 파일을 만드는 것입니다.,

파일 제목과 content가 있는데 제목은 아까 HMGIBNZL.txt로 설정해놨으니 content에는 password가 들어가면 되겠네요.

 

전체 Solve입니다.

import angr
import claripy
import sys

def main(argv):
  project = angr.Project("./07_angr_symbolic_file")

  start_address = 0x0804945C
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
 
  filename = "HMGIBNZL.txt" # :string
  symbolic_file_size_bytes = 0x40
 
  password = claripy.BVS('password', symbolic_file_size_bytes * 8)
 
  password_file = angr.storage.SimFile(filename, content=password)
 
  initial_state.fs.insert(filename, password_file)

  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]

    solution = solution_state.solver.eval(password,cast_to=bytes).decode(errors='ignore')

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

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

  solution = solution_state.solver.eval(password,cast_to=bytes).decode(errors='ignore')

위의 코드는

유니코드 디코딩 에러가 나오기 때문에 추가해줬습니다. 근데 사실 생각해보니까

여기부분에서 비밀번호를 8글자만 확인하기 때문에 앞에 password길이를 8로 수정해도 바로 가능합니다.

symbolic_file_size_bytes = 0x8

결과입니다.

 

이렇게 04~07번 문제까지의 풀이를 작성해보았습니다. 이제 절반 조금 안되게 했는데, 다음 문제부턴 심볼릭이 아닌 다른 문제들이라 여기서 한번 끊고 가겠습니다.

읽어주셔서 감사합니다~!