취약점분석/Reversing

[Reversing] angr CTF 문제 풀이 - 3 (12 ~ 14 )

poiri3r 2026. 1. 3. 11:58

오늘은 마찬가지로 angr CTF문제를 이어서 풀어보도록 하겠습니다.

 

12_angr_veritesting

veritesting은 검증적 테스팅으로 angr가 지원하는 강력한 기능 중 하나라고 합니다.

문제 보면 scanf에서 인자를 엄청나게 많이 받는데, 저 인자들에 대해 if문과 같은 분기에 들어가면 경로폭발이 발생할 수 있을 것 같습니다.

문제 풀이 파일을 봤는데

# When you construct a simulation manager, you will want to enable Veritesting:
# project.factory.simgr(initial_state, veritesting=True)
# Hint: use one of the first few levels' solutions as a reference.

이거 3줄밖에 없더라고요 .. ?? 

처음 몇단계의 솔루션을 가져오고, simgr에서 veritesting 모드를 활성화 하라고 합니다.

일단 잘 모르겠어서 기존 02_angr_find_condition의 solve를 가져와서 veritesting=True만 켜주었습니다.

solve 전문입니다.

# When you construct a simulation manager, you will want to enable Veritesting:
# project.factory.simgr(initial_state, veritesting=True)
# Hint: use one of the first few levels' solutions as a reference.

import sys
import angr

def main(argv):
    project = angr.Project("./12_angr_veritesting")
    initial_state = project.factory.entry_state(
        add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                        angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS})
    simulation = project.factory.simgr(initial_state, veritesting = True)
   
    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]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')
if __name__ == '__main__':
    main(sys.argv)

 

실행 결과입니다.

veritesting이라는 개념 자체가 좀 어려운 개념인 것 같은데, 해당 기능을 angr라는 툴에서는 쉽게 지원한다! 라는 취지의 문제인 듯 싶습니다. 해당 개념에 관해서는 논문이 한두편 있던데 이건 지금 읽어보고 있는 논문만 읽고 읽어보고 올려드리겠습니다.

 

 

13_angr_static_binary

이번 문제는 제목처럼 정적 링크 문제입니다.

문제를 보면 굉장히 평범해 보이지만

정적 링킹은 프로그램을 만들 때 필요한 모든 라이브러리 코드를 실행 파일 안에 복사해서 넣는 방식입니다.

실제로 import칸엔 아무 함수도 없습니다.

저번에 scanf를 sim으로 대체한거랑 비슷한 느낌으로 풀면 될 것 같은데, 선언되어 있는 함수가 너무 많아 일일히 연결하기엔 무리가 있습니다.

문제 파일을 보니까

# 이 도전은 첫 번째 도전과 정확히 동일하지만
# 정적 이진 파일로 컴파일됩니다. 일반적으로 Angr은 표준을 자동으로 대체합니다
# 라이브러리는 훨씬 더 빠르게 작동하는 SimProcedures와 함께 작동합니다.
#
# 문제를 해결하려면 다음과 같은 표준 라이브러리 c 함수를 수동으로 연결합니다
# 를 사용합니다. 그런 다음, 실행을 시작할 때
# 주 함수. entry_state를 사용하지 않습니다.
#
# 다음은 Angr이 이미 작성한 몇 가지 SimProcedures입니다. 그들은 다음을 구현합니다
# 표준 라이브러리 함수. 모든 것이 필요하지 않을 것입니다:
# angr.SIM_PRODUESS ['libc']['malloc']
# angr.SIM_PRODUESS ['libc']['열림']
# angr.SIM_PRODUESS ['libc']['fclose']
# angr.SIM_PRODUESS ['libc']['작성']
# angr.SIM_PRODUESS ['libc']['getchar']
# angr.SIM_PRODUESS ['libc']['strncmp']
# angr.SIM_PRODUESS ['libc']['strcmp']
# angr.SIM_PRODUESS ['libc']['scanf']
# angr.SIM_PRODUESS ['libc']['printf']
# angr.SIM_PRODUESS ['libc']['풋스]
# angr.SIM_PRODUESS ['libc']['출구']
#
# 참고로, 함수를 다음과 유사한 것으로 연결할 수 있습니다:
# project.hook(malloc_address, anggr.SIM_PROCEDUES['libc']['malloc']( )
#
# 더 많은 것들이 있습니다, 보세요:
# https://github.com/angr/angr/tree/master/angr/procedures/libc
#
# 또한 바이너리가 실행될 때 주 함수는 다음과 같습니다
# 첫 번째 코드 조각이 호출되었습니다. _시작 함수에서 __libc_start_main은 다음과 같습니다
# 프로그램을 시작하기 위해 호출됩니다. 이 함수에서 발생하는 초기화
# Angr을 사용하면 시간이 오래 걸릴 수 있으므로 SimProcessure로 교체해야 합니다.
# angr.SIM_PRODUESS ['glibc']['_libc_start_main']
# 'libc' 대신 'glibc'를 주목하세요.

 

라는 힌트가 있는데 github.com/링크로 들어가보겠습니다.

음 슥 보니 저번 문제 풀때는 scanf를 대신하는 클래스를 저희가 만들었었는데, 이번에는 angr가 이미 기능으로 만들어뒀기에 저희는 편하게

# As a reminder, you can hook functions with something similar to:
# project.hook(malloc_address, angr.SIM_PROCEDURES['libc']['malloc']())

이런식으로 연결만 하면 될 것 같습니다.

문제 설명에서는 main함수도 glibc __libc_start_main으로 교체하라고 하네요.

위 코드를 말하는 듯 싶습니다.

일단 코드를 먼저 짜보겠습니다.

import angr
import claripy
import sys

def main(argv):
    project = angr.Project("./13_angr_static_binary")
    initial_state = project.factory.entry_state()

    project.hook(0x804AF00, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
    project.hook(0x804CBC0, angr.SIM_PROCEDURES['libc']['scanf']())
    project.hook(0x804CBF0, angr.SIM_PROCEDURES['libc']['printf']())
    project.hook(0x8057E90, angr.SIM_PROCEDURES['libc']['puts']())
    project.hook(0x8064400, angr.SIM_PROCEDURES['libc']['strcmp']())

    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]
        print(solution_state.posix.dumps(sys.stdin.fileno()))
    else:
        raise Exception('Could not find the solution')

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

문제 코드는 비교적 간단하게 나왔습니다.

실행결과입니다.

14_angr_shared_library

크게 어려워 보일 것 없는 바이너리지만 제목에서 알 수 있듯이 공유 라이브러리를 사용합니다.

입력값을 검사하는 함수인 validate가 14_angr_shared_library.so에 따로 포함되어 있습니다.

다행이 angr는 공유 라이브러리를 같이 분석할 수 있는 기능이 있습니다.

# The shared library is compiled with position-independent code. You will need
  # to specify the base address. All addresses in the shared library will be
  # base + offset, where offset is their address in the file.
  # (!)
  base = ???
  project = angr.Project(path_to_binary, load_options={
    'main_opts' : {
      'base_addr' : base
    }
  })

진짜 모르겠어서 제미나이한테 이것 저것 물어봤는데,

project = angr.Project(path_to_binary, auto_load_libs=True)

auto_load_libs 옵션을 켜주면 공유 라이브러리도 angr가 알아서 분석해주는 기능이 있더라고요.

import angr
import sys

def main(argv):
    path_to_binary = "./14_angr_shared_library"
    project = angr.Project(path_to_binary, auto_load_libs=True)
    initial_state = project.factory.entry_state()
    simulation = project.factory.simulation_manager(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]
        print("[!] 정답 발견:", solution_state.posix.dumps(sys.stdin.fileno()))
    else:
        print("[-] 정답을 찾지 못했습니다.")

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

이렇게 간단한 코드만으로도 정답이 찾아집니다.

하지만 저희는 출제자의 의도를 찾아봐야하기 때문에 다시 처음부터 해보겠습니다 ..

base에는 파일의 base주소를 찾아서 넣었습니다

# Initialize any symbolic values here; you will need at least one to pass to
  # the validate function.
  # (!)
  buffer_pointer = claripy.BVV(???, ???)

여기에는 가짜 비밀번호를 저장할 메모리 주소이고 아무 값이나 넣으면 됩니다.

initial_state = project.factory.call_state(
                    validate_function_address,
                    buffer_pointer,
                    claripy.BVV(8,32)
                  )

해당 코드는 validate를 실행하기 직전으로 이동해서 함수 인자 규약을 맞추는 것입니다.

  password = claripy.BVS( ???, ??? )
  initial_state.memory.store( ??? , ???)

password은 8바이트니까 설정해주고, 

해당 값을 아까 만들어둔 메모리주소에 저장합니다.

 

check_constraint_address = base + ???
simulation.explore(find=check_constraint_address)

이건 공유 라이브러리에서 ret주소를 찾아서 넣는 과정입니다. odjdump로 봤을때

마지막 ret이 들어있는 주소를 확인하면 됩니다.

# Determine where the program places the return value, and constrain it so
    # that it is true. Then, solve for the solution and print it.
    # (!)
    solution_state.add_constraints( ??? )
    solution = ???

여기서 solution은 함수의 반환값입니다.

함수의 리턴값은 항상 eax 레지스터에 저장되므로 eax 레지스터가 1이 되어야 한다는 제약을 걸면 됩니다.

solution_state.add_constraints(solution_state.regs.eax == 1)

 

안되서 중간중간 주소값들을 변경했습니다.

일단 path_to_binary에서 대상이 .so파일이고, base는 0x0

function 주소도 실행파일에서가 아닌 .so파일에서의 주소인 1234를 넣어줬습니다.

전체 코드는 다음과 같습니다.

import angr
import claripy
import sys

def main(argv):
  path_to_binary = "./14_angr_shared_library.so"
 
  base = 0x0
  project = angr.Project(path_to_binary, load_options={
    'main_opts' : {
      'base_addr' : base
    }
  })
 
  buffer_pointer = claripy.BVV(0x77777777, 32)
 
  validate_function_address = 0x1234
  initial_state = project.factory.call_state(
                    validate_function_address,
                    buffer_pointer,
                    claripy.BVV(8,32)
                  )
 
  password = claripy.BVS( 'password', 8*8 )
  initial_state.memory.store( buffer_pointer , password )
 
  simulation = project.factory.simgr(initial_state)
 
  check_constraint_address = base + 0x12e0
  simulation.explore(find=int(check_constraint_address))

  if simulation.found:
    solution_state = simulation.found[0]
 
    solution_state.add_constraints(solution_state.regs.eax == 1)
    solution =solution_state.solver.eval(password, cast_to=bytes)
    print(solution)
  else:
    raise Exception('Could not find the solution')

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

 

실행 결과입니다.

이상입니다. 

이제 3문제 남았는데, arbitrary_read, write, jump입니다.

일단 3문제로 포스팅을 마무리 하도록 하겠습니다.

아마 다음편이 angr 마지막편이 될 것 같은데 단기간에 참 열심히한 것 같네요.

이 ctf 시리즈가 끝나고 나면 angr를 이용해서 다른 무언가를 해보고 싶습니다.

뭘 해야될진 생각해봐야겠네요.

읽어주셔서 감사합니다~