오늘은 angr CTF 문제 풀이 3편으로 돌아왔습니다.
바로 문제 풀이 들어가보겠습니다.
08_angr_constraints
문제의 의도가 뭔가 했는데, check_equals라는 코드에서 정답을 바로 반환하는게 아니라 다음과 같은 로직을 걸칩니다.
angr에서 이런 무의미한 분기에 빠지게 되면 state가 엄청나게 생성이 되고, 속도도 엄청나게 느려지기 때문에
constraints 즉 제약조건을 걸어 무의미한 분기에 빠지지 않게 하는 과정입니다.
이런경우엔 check_equals_~~라는 함수에 빠지기 전에 제약을 걸어야합니다.
# Angr will not be able to reach the point at which the binary prints out
# 'Good Job.'. We cannot use that as the target anymore.
# (!)
address_to_check_constraint = ???
simulation.explore( find = address_to_check_constraint)
Good Job. 이라는 타겟을 사용할 수 없겠네요/
저희는 해당 분기에서 state를 멈춰주겠습니다.
check_equals~~ 호출 바로 직전입니다.
이때를 기준으로 제약 조건을 걸어줍니다.
if simulation.found:
solution_state = simulation.found[ 0 ]
# Recall that we need to constrain the to_check parameter (see top) of the
# check_equals_ function. Determine the address that is being passed as the
# parameter and load it into a bitvector so that we can constrain it.
# (!)
constrained_parameter_address = ???
constrained_parameter_size_bytes = ???
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes
)
그리고 이때 기준으로 프로그램이 검사하는 password의 값을 정적 분석을 통해 채워넣어야 합니다.
IDA에서
이 password 크기와 그리고 문자열을 가져오면 되겠네요.
parameter_address는 if ( check_equals_LCILGCDAHMGIBNZL(&buffer, 16) )에서 buffer로 저장되기 때문에 buffer의 주소를 가져옵니다.
import angr
import claripy
import sys
def main ( argv ):
project = angr.Project( "./08_angr_constraints" )
start_address = 0x 80492DD
initial_state = project.factory.blank_state(
addr = start_address,
add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
password = claripy.BVS( 'password' , 16 * 8 )
password_address = 0x 0804C028
initial_state.memory.store(password_address, password)
simulation = project.factory.simgr(initial_state)
address_to_check_constraint = 0x 804932E
simulation.explore( find = address_to_check_constraint)
if simulation.found:
solution_state = simulation.found[ 0 ]
constrained_parameter_address = 0x 804C028
constrained_parameter_size_bytes = 16
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes
)
constrained_parameter_desired_value = b "LCILGCDAHMGIBNZL" # :string (encoded)
solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)
solution = solution_state.solver.eval(password, cast_to = bytes )
print (solution.decode( 'utf-8' ))
else :
raise Exception ( 'Could not find the solution' )
if __name__ == '__main__' :
main(sys.argv)
전체 코드는 다음과 같습니다.
이렇게 하면 까다로운 분기에 빠지지 않고 답을 구할 수 있습니다.
하지만 위와 같은 상황에서는 굳이 angr보다 다른 툴들이 좋아보이긴 합니다.
이런 저런 제약이 걸려있는 상황에서도 angr를 써서 풀수는 있지만 굳이 풀 필요는 없어보입니다.
이런 역연산 같은 경우 claude한테 물어보면 사실 1분안에 나옵니다.
09_angr_hooks
이번 문제는 후킹에 관련된 문제입니다.
후킹은 예전 리버싱 포스팅에 많이 써져있으니 참고하시면 될 것 같습니다.
하드코딩된 "LCIGC.." 문자열을 complex_function을 통해 정답으로 바꿉니다.이 이후, 우리가 입력한 값과 해당 check_equals_..를 통해 결과를 검사합니다.
이때 이 check_equals..를 후킹해서 두번째 인자로 들어있는 진짜 password를 찾아내면 됩니다.
# Hook the address of where check_equals_ is called.
# (!)
check_equals_called_address = ???
후킹할 함수를 call하는 주소를 넣으면 됩니다.
해당 주소값을 넣어두겠습니다.
instruction_to_skip_length = ???
@project.hook (check_equals_called_address, length = instruction_to_skip_length)
또 두 주소의 차를 구해야 합니다.
5를 넣어주겠습니다.
def skip_check_equals_ ( state ):
# Determine the address where user input is stored. It is passed as a
# parameter ot the check_equals_ function. Then, load the string. Reminder:
# int check_equals_(char* to_check, int length) { ...
user_input_buffer_address = ??? # :integer, probably hexadecimal
user_input_buffer_length = ???
이건 간단하게 버퍼 주소와 길이를 넣겠습니다.
# gcc uses eax to store the return value, if it is an integer. We need to
# set eax to 1 if check_against_string == user_input_string and 0 otherwise.
# However, since we are describing an equation to be used by z3 (not to be
# evaluated immediately), we cannot use Python if else syntax. Instead, we
# have to use claripy's built in function that deals with if statements.
# claripy.If(expression, ret_if_true, ret_if_false) will output an
# expression that evaluates to ret_if_true if expression is true and
# ret_if_false otherwise.
# Think of it like the Python "value0 if expression else value1".
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV( 1 , 32 ),
claripy.BVV( 0 , 32 )
)
이건 이미 코드에 포함되어있는 주석인데, gcc는 반환 값이 정수인 경우 eax를 사용하여 저장하는데, user_input = check_against_string 의 조건문을 달아서 리턴값을 0또는 1로 설정하는 부분입니다. 해당 값을 설정하지 않으면 함수안에서 계속 죽을 수 있습니다.
# Since we are allowing Angr to handle the input, retrieve it by printing
# the contents of stdin. Use one of the early levels as a reference.
solution = ???
해당 solution부분은 첫 포스팅때 다룬적 있는데 결과값에 대한 사용자 입력값을 넣어야 하므로
solution = solution_state.posix.dumps(0 )가 들어가면 됩니다.
전체 solve는 다음과 같습니다.
import angr
import claripy
import sys
def main ( argv ):
project = angr.Project( "./09_angr_hooks" )
initial_state = project.factory.entry_state(
add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
check_equals_called_address = 0x 804933E
instruction_to_skip_length = 5
@project.hook (check_equals_called_address, length = instruction_to_skip_length)
def skip_check_equals_ ( state ):
user_input_buffer_address = 0x 804C02C # :integer, probably hexadecimal
user_input_buffer_length = 16
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
check_against_string = "LCILGCDAHMGIBNZL" # :string
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV( 1 , 32 ),
claripy.BVV( 0 , 32 )
)
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.posix.dumps( 0 )
print (solution)
else :
raise Exception ( 'Could not find the solution' )
if __name__ == '__main__' :
main(sys.argv)
실행 결과입니다.
조금 복잡할 수 있는 부분이 있어서 핵심만 조금 정리를 해보겠습니다.
@project.hook (check_equals_called_address, length = instruction_to_skip_length)
def skip_check_equals_ ( state ):
해당 문법은 프로그램이 call check_equals를 실행할 때 해당 함수 대신 아래의 skip_check_equals를 실행하는 구문입니다.
user_input_buffer_address = 0x 804C02C # :integer, probably hexadecimal
user_input_buffer_length = 16
이후 사용자가 입력한 값의 주소와 길이를 가져온 뒤
check_against_string = "LCILGCDAHMGIBNZL" # :string
정답의 주 재료가 되는 문자열도 들고옵니다.
(이때의 user_input_buffer는 미지수)
이때 함수의 반환값으로는 무조건 1을 넣고 Eax 값이 1이 되는 값을 찾는것입니다.
10_angr_simprocedures
이번 문제는 프로시저 시뮬레이션 문제입니다.
저도 해당 개념을 처음 본거라 간단하게 설명하면, 기계어 코드 대신 시뮬레이션에서 파이썬 코드들로 동작 기능을 흉내내는 것 입니다.
제 당혹감이 느껴질지 모르겠지만 IDA graph를 봤을 때 정말 굉~~~~장히 많은 무언가가 보입니다..
저게 눌러보면 다 check_equals_LCI ... 함수의 일부로 보이는데 해당 함수를 대체하는 코드를 만들어야 할 것 같습니다.
이걸 그대로 angr에 넣으면 state가 너무 많아져서 폭발합니다.
너무 궁금해서 원본 코드를 확인했는데, 사용자에게 받은 입력을 complex_function을 통해 바꾼 뒤 복잡한 함수에 넣은 값을 비교합니다.
주석에 달린 내용을 확인해보겠습니다.
# 이 챌린지는 이전 챌린지와 유사합니다. 동일한 조건에서 작동합니다
# check_equals_ 함수를 교체해야 한다는 전제. 이 경우
# 그러나 case_check_equals_는 너무 많이 호출되어 다음과 같은 기능을 수행하지 못합니다
# 각 호출된 위치를 연결하는 감각. 대신 SimProcessure를 사용하여 글을 씁니다
# 자신의 check_equals_ 구현을 한 다음 check_equals_ 기호를 연결합니다
# 스캔할 모든 호출을 SimProcessure 호출로 대체합니다.
class ReplacementCheckEquals ( angr . SimProcedure ):
# A SimProcedure replaces a function in the binary with a simulated one
# written in Python. Other than it being written in Python, the function
여기 ReplacementCheckEquals를 통해 기존의 함수를 내가 만든 코드로 갈아껴야 합니다.
def run ( self , to_check , ...???):
# We can almost copy and paste the solution from the previous challenge.
# Hint: Don't look up the address! It's passed as a parameter.
# (!)
user_input_buffer_address = ???
user_input_buffer_length = ???
# Note the use of self.state to find the state of the system in a
# SimProcedure.
user_input_string = self .state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
check_against_string = ???
# Finally, instead of setting eax, we can use a Pythonic return statement
# to return the output of this function.
# Hint: Look at the previous solution.
return claripy.If( ??? , ??? , ??? )
run이라는 함수는 check_equal를 대신 하는 함수입니다. 인자를 동일하게 맞춰줍니다.
user_input_buffer_address와 length는 직접 찾을 필요 없이 위의 run함수 인자 값을 채워줍니다.
직접 채워줘도 크게 상관은 없습니다.
return claripy.IF여기 부분은 바로 전문제 hook에ㅐ서 한것처럼 조건문을 세워줍니다.
return claripy.If(
user_input_string == check_against_string,
claripy.BVV( 1 , 32 ),
claripy.BVV( 0 , 32 )
)
다음은 hook.symbol입니다.
check_equals_symbol = "check_equals_LCILGCDAHMGIBNZL" # :string
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())
어떤 함수를 무엇으로 대체할것인가를 설정해줍니다.
함수 전문입니다
import angr
import claripy
import sys
def main ( argv ):
project = angr.Project( "./10_ang_simprocedures" )
initial_state = project.factory.entry_state(
add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
class ReplacementCheckEquals ( angr . SimProcedure ):
def run ( self , to_check , length ):
user_input_buffer_address = to_check
user_input_buffer_length = length
user_input_string = self .state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
check_against_string = "LCILGCDAHMGIBNZL"
return claripy.If(
user_input_string == check_against_string,
claripy.BVV( 1 , 32 ),
claripy.BVV( 0 , 32 )
)
check_equals_symbol = "check_equals_LCILGCDAHMGIBNZL" # :string
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())
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.posix.dumps( 0 )
print (solution)
else :
raise Exception ( 'Could not find the solution' )
if __name__ == '__main__' :
main(sys.argv)
실행 결과입니다.
개념 자체가 좀 낯설고 어려워서 그렇지 코드로 작성하는 건 크게 어렵지 않은 것 같습니다.
11_angr_sim_scanf
갈수록 문제가 가관이 되가는 것 같습니다.
그래프 노드가 너무 많다고 그래프가 켜지지도 않습니다.
문제 제목처럼 scanf에 대한 대체 클래스를 만들어 교체해야할 것 같은 느낌이 물씬 드는데, 시작할 엄두가 잘 안나서 claude를 돌려보니 10초만에 나오네요.
새삼 AI가 대단하다는 생각이 많이 듭니다 ....... 그래도 하긴 해야하니 문제를 보겠습니다.
# Finish the parameters to the scanf function. Hint: 'scanf("%u %u", ...)'.
# (!)
def run ( self , format_string , scanf0_address , ...):
# Hint: scanf0_address is passed as a parameter, isn't it?
scanf0 = claripy.BVS( 'scanf0' , ??? )
...
# The scanf function writes user input to the buffers to which the
# parameters point.
self .state.memory.store(scanf0_address, scanf0, endness = project.arch.memory_endness)
...
# Now, we want to 'set aside' references to our symbolic values in the
# globals plugin included by default with a state. You will need to
# store multiple bitvectors. You can either use a list, tuple, or multiple
# keys to reference the different bitvectors.
# (!)
일단 scanf의 작동 방식에 대해 좀 알아봐야합니다.
먼저 scanf는 인자의 개수가 정해져 있습니다. 그래서 run(self, arg1, arg2)처럼 파이썬 함수에 인자를 고정하기가 어렵습니다.
하지만 저희가 푸는 문제에서는 불러오는 인자 개수가 정해져 있기 때문에 그냥 인자 2개로 고정해서 해도 될 듯 합니ㅏㄷ.
두번째로 scanf는 값을 리턴하는 것이 아닌 변수의 주소를 받아서 그 위치에 값을 써넣습니다.
=> 저희가 scanf대신 ReplacementScanf라는 클래스와 run을 사용해 인자 2개를 받아 미지수를 직접 넣으면 됩니다.
scanf0 = claripy.BVS( 'scanf0' , 32 )
scanf1 = claripy.BVS( 'scanf1' , 32 )
먼저 32비트짜리 심볼을 2개 만듭니다.
# The scanf function writes user input to the buffers to which the
# parameters point.
self .state.memory.store(scanf0_address, scanf0, endness = project.arch.memory_endness)
self .state.memory.store(scanf1_address, scanf1, endness = project.arch.memory_endness)
이건 저희가 생성한 가짜 입력값을 실제 프로그램의 메모리에 직접 넣는 동작입니다.
즉 scanf0_address에 scanf0이라는 심볼릭 변수를 집어 넣는 것입니다.
self .state.globals[ 'solution0' ] = scanf0
self .state.globals[ 'solution1' ] = scanf1
해당 코드는 angr상의 전역 변수에다가 scanf0과 scanf1을 저장하는 코드입니다. 저희가 scanf를 가로채서 메모리에 값을 직접 넣었기 때문에 posix.dumps(0)을 해도 stdin에 값이 입력되어있지 않습니다. 따라서 전역 변수에 저장해야합니다.
전체 코드입니다.
import angr
import claripy
import sys
def main ( argv ):
project = angr.Project( "./11_angr_sim_scanf" )
initial_state = project.factory.entry_state(
add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
class ReplacementScanf ( angr . SimProcedure ):
def run ( self , format_string , scanf0_address , scanf1_address ):
scanf0 = claripy.BVS( 'scanf0' , 32 )
scanf1 = claripy.BVS( 'scanf1' , 32 )
self .state.memory.store(scanf0_address, scanf0, endness = project.arch.memory_endness)
self .state.memory.store(scanf1_address, scanf1, endness = project.arch.memory_endness)
self .state.globals[ 'solution0' ] = scanf0
self .state.globals[ 'solution1' ] = scanf1
scanf_symbol = "__isoc99_scanf"
project.hook_symbol(scanf_symbol, ReplacementScanf())
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 ]
stored_solutions0 = solution_state.globals[ 'solution0' ]
stored_solutions1 = solution_state.globals[ 'solution1' ]
solution0 = solution_state.solver.eval(stored_solutions0)
solution1 = solution_state.solver.eval(stored_solutions1)
print ( f " { solution0 } { solution1 } " )
else :
raise Exception ( 'Could not find the solution' )
if __name__ == '__main__' :
main(sys.argv)
실행 결과입니다.
점점 헷갈리는게 많아지네요 .. 개념들에 대한 복습도 되면서 그래도 툴의 기능들이 익혀지는 것 같습니다.
유용한 기능들이 너무 많네요.
다만 실제 CTF나 문제 풀이에 적용하기엔 아직 숙련도가 많이 부족할 것 같기도 하고, 실제 문제 환경을 설정하는 과정이 어려울 것 같기도 합니다..
일단 이상으로 angr ctf 문제풀이 3편 포스팅을 마치겠습니다.
읽어주셔서 감사합니다~