【angr_ctf】二进制分析工具angr使用与练习-Part II(提高篇)
前言
本文是angr_ctf
练习的第二篇,上一篇【angr_ctf】二进制分析工具angr使用与练习-Part I(基础篇)主要讲解了其中的前7道题目,本篇为提高篇,在难度上会有所提升,许多细节在上一篇说过的,这里不再赘述,直接进入题目分析。
08_angr_constraints
本题考查对约束求解中约束条件的使用,以解决符号执行中常见的路径爆炸的问题
关于符号执行的路径爆炸问题,可以参考阅读符号执行入门 - 知乎 (zhihu.com),这里我们重点放在对题目的理解上,先看一下反编译的源码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int i; // [esp+Ch] [ebp-Ch]
password = 1247302221;
dword_804A044 = 1381587531;
dword_804A048 = 1162562891;
dword_804A04C = 1113212494;
memset(&buffer, 0, 0x11u);
printf("Enter the password: ");
__isoc99_scanf("%16s", &buffer);
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(i + 134520912) = complex_function(*(char *)(i + 134520912), 15 - i);
if ( check_equals_MRXJKZYRKMKENFZB((int)&buffer, 0x10u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
_BOOL4 __cdecl check_equals_MRXJKZYRKMKENFZB(int a1, unsigned int a2)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]
v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 0x804A040) )
++v3;
}
return v3 == a2;
}
程序对输入的16字节数据的每一位做了complex_function
变换,然后对字符串中每一个字符依次比较,虽然函数名称告诉了要比较的字符串为MRXJKZYRKMKENFZB
,但我们还是从源码上解读一下:
程序一开始初始化了4个全局变量,将其转换成字符串表示,得到结果如下:
再看4个变量在.bss
段中存储的地址:
结合比较函数中*(_BYTE *)(i + a1) == *(_BYTE *)(i + 0x804A040)
,刚好实现了对应字符的比较。
这里有个注意的点,因为程序采用小端序,因此保存的字符串是
JXRM
,在比较的时候则是MRXJ
由于在比较的时候循环比较单个字符(共16个),因此做符号执行的时候会存在$2^{16}=65536$条路径,直接原地爆炸,根据我们的分析,实际上只需要在进入check_equals_MRXJKZYRKMKENFZB()
函数时,存储在buffer
中的字符串为MRXJKZYRKMKENFZB
即可,这相当于我们的约束条件,避免进入循环一一比较字符。
因此,这里首先确定两个地址:
buffer
地址:需要往此处插入符号变量check_equals_MRXJKZYRKMKENFZB
函数地址:到此处的时候直接根据约束条件进行求解
通过ida
可以直接查看到这两处数值:
起始地址直接选择scanf
之后的两个地址其一即可,我选择的是0x08048622
本题还用到了state.memory.load(addr, bytes)
函数,用于在指定内存地址中读取指定字节的数据,这部分是为了我们直接获取buffer
的内容,然后和我们的目标字符串比较。
下面是题解,我在必要的地方添加了注释,帮助理解:
import angr
import claripy
def main():
path_to_binary = "./08_angr_constraints"
project = angr.Project(path_to_binary, auto_load_libs=False)
start_address = 0x8048622
buff_addr = 0x0804A050
address_to_check_constraint = 0x08048565
initial_state = project.factory.blank_state(addr=start_address)
passwd_length = 16
password0 = claripy.BVS('password0', passwd_length * 8)
# 在buffer的内存地址中添加符号变量
initial_state.memory.store(buff_addr, password0)
simulation = project.factory.simgr(initial_state)
# 寻找进行字符比较函数的路径,不直接进入该路径
simulation.explore(find=address_to_check_constraint)
if simulation.found:
solution_state = simulation.found[0]
constrained_parameter_address = buff_addr
constrained_parameter_size_bytes = 16
# 利用memory.load接口读取buffer处内存数据,用于比较,读取结果为BV
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes
)
# 约束条件中需要直接比较的字符串
constrained_parameter_desired_value = 'MRXJKZYRKMKENFZB'
# 添加约束条件
solution_state.solver.add(constrained_parameter_bitvector == constrained_parameter_desired_value)
# 约束求解
solution0 = solution_state.solver.eval(password0, cast_to=bytes)
print("[+] Success, flag is: {}".format(solution0.decode('utf-8')))
else:
raise Exception('Could not find the solution')
if __name__ == "__main__":
main()
09_angr_hooks
本题考查对angr
中hook
的使用,替代直接的约束条件
上源码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BOOL4 v3; // eax
signed int i; // [esp+8h] [ebp-10h]
signed int j; // [esp+Ch] [ebp-Ch]
qmemcpy(password, "MRXJKZYRKMKENFZB", 16);
memset(buffer, 0, 0x11u);
printf("Enter the password: ");
__isoc99_scanf("%16s", buffer);
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(i + 134520916) = complex_function(*(char *)(i + 134520916), 18 - i);
equals = check_equals_MRXJKZYRKMKENFZB((int)buffer, 0x10u);
for ( j = 0; j <= 15; ++j )
*(_BYTE *)(j + 134520900) = complex_function(*(char *)(j + 134520900), j + 9);
__isoc99_scanf("%16s", buffer);
v3 = equals && !strncmp(buffer, password, 0x10u);
equals = v3;
if ( v3 )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
_BOOL4 __cdecl check_equals_MRXJKZYRKMKENFZB(int a1, unsigned int a2)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]
v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 0x804A044) )
++v3;
}
return v3 == a2;
}
int __cdecl complex_function(signed int a1, int a2)
{
if ( a1 <= 64 || a1 > 90 )
{
puts("Try again.");
exit(1);
}
return (a1 - 65 + 23 * a2) % 26 + 65;
}
还是一样的味道,程序预先将password
设定为MRXJKZYRKMKENFZB
,然后对用户的输入进行变换比较,在变换的时候采用了单个字符的循环对比,同样地,我们需要在程序执行到这部分的时候接管对比的操作。
之前,我们是先explore
找到check
的地址,然后比较此时buffer
在内存中存储的字符串是否与目标相符。angr
提供了更为强大的操作,即hook
。
百度百科中对hook
的解释:Hook是Windows中提供的一种用以替换DOS下“中断”的系统机制,中文译为“挂钩”或“钩子”。在对特定的系统事件进行hook后,一旦发生已hook事件,对该事件进行hook的程序就会收到系统的通知,这时程序就能在第一时间对该事件做出响应。
在编程中采用hook
,则是希望在指定事件发生时,能够接管操作,跳过相应指令的执行。据此,我们只要找到check_equals_MRXJKZYRKMKENFZB
函数的地址,在此处添加hook
,实现buffer
中字符串与password
的比较即可。
关于
angr
中hook
的具体使用可以参考Programming SimProcedures - angr Documentation
angr
中对于hook
的使用有两种方式:
一是装饰器:传入需要
hook
的地址和要跳过的指令长度,当程序执行到该地址时,执行hook_func
中的内容,返回后继续在hook_addr+skip_length
的地方继续执行(若length=0
则不会触发hook
)。该方法适用于函数不需要参数,也不考虑函数返回值的时候,当你要处理的函数包含对参数的操作,并且需要处理返回值,此时采用方法二@project.hook(hook_addr, length=instruction_length_to_skip) def hook_func(state): # do somethings
二是
hook_symbol
:通过继承angr.SimProcedure
,并重写run
方法,可以实现对一个原始函数的完全模拟,包括函数的参数和返回值,下面演示了一个对原始函数的hook
过程# 原始函数 int add_if_positive(int a, int b) { if (a >= 0 && b >= 0) return a + b; else return 0; } # hook class ReplacementAddIfPositive(angr.SimProcedure): def run(self, a, b): if a >= 0 and b >=0: return a + b else: return 0 func_name = 'add_if_positive' project.hook_symbol(func_name, ReplacementAddIfPositive())
除此之外,还需要思考一个问题,对于hook
装饰的函数,我们如何提供函数的返回值(检查对比结果)?
为此,我们先对函数本身如何处理返回值进行查看:
在angr
中通过claripy.If(条件,条件为True时的返回值,条件为False时的返回值)
实现,我们只需要将条件设置为两字符串比较,相同时返回1(BV),不同时返回0(BV)即可。
最后,我们找一下需要的地址,由于我们目的是接管check
函数部分的指令,因此程序的初始化状态选择入口状态即可(不需要自定义一个空白状态,传入符号变量):
- 检测函数
check_equals_MRXJKZYRKMKENFZB
地址
buffer
地址
注意到下一条指令和函数check_equals_MRXJKZYRKMKENFZB
之间间隔5个字节,因此我们要跳过的长度即5字节
下面是最终的题解,我会在必要的地方添加注释,帮助理解:
import angr
import claripy
import sys
def main():
path_to_binary = '09_angr_hooks'
project = angr.Project(path_to_binary)
# 直接从入口开始
initial_state = project.factory.entry_state()
check_equals_called_address = 0x080486B3
# 检测函数的长度
instruction_to_skip_length = 5
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
# 此时需要从内存中读取buffer的内容,设定buffer的地址和要读取的长度
user_input_buffer_address = 0x0804A054 # :integer, probably hexadecimal
# 字符串长度16字节
user_input_buffer_length = 16
# 从内存中读取数据
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
check_against_string = 'MRXJKZYRKMKENFZB' # :string
# 如果相等返回1,否则返回0
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 True if 'Good Job.' in stdout_output.decode('utf-8') else False
def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return True if 'Try again.' in stdout_output.decode('utf-8') else False
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print('[+] Success, flag is {}'.format(solution.decode('utf-8')))
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main()
10_angr_simprocedures
本题依旧是对hook
的使用,只不过上一题采用函数地址,本次采用函数名,相比函数地址更加方便
看下源码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int i; // [esp+20h] [ebp-28h]
char s[17]; // [esp+2Bh] [ebp-1Dh]
unsigned int v6; // [esp+3Ch] [ebp-Ch]
v6 = __readgsdword(0x14u);
memcpy(&password, "MRXJKZYRKMKENFZB", 0x10u);
memset(s, 0, 0x11u);
printf("Enter the password: ");
__isoc99_scanf("%16s", s);
for ( i = 0; i <= 15; ++i )
s[i] = complex_function(s[i], 18 - i);
if ( check_equals_MRXJKZYRKMKENFZB(s, 16) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
_BOOL4 __cdecl ''check_equals_MRXJKZYRKMKENFZB''(int a1, unsigned int a2)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]
v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 134529096) )
++v3;
}
return v3 == a2;
}
程序与上一题一样,本题需要用到angr.SimProcedure
来替代二进制文件中指定函数的部分,说白了就是自己写一个函数替代原有的函数,很明显我们针对的是check_equals_MRXJKZYRKMKENFZB()
。
angr.SimProcedure
实现了用python
编写替代原有函数的功能,通过继承SimProcedure
,重写run(self, *args)
方法,传入的参数(BV类型)和原有函数的参数对应,并按照pythonic
的方式返回,用以提供给angr
像原来函数一样处理。
下图展示了这一思想:
前面09题说了,我们有两种hook
方式,这里因为我们要处理参数和返回值,因此采用继承angr.SimProcedure
的方式。
下面是题解,我会在必要的地方添加注释,帮助理解:
import angr
import claripy
import sys
def main():
path_to_binary = '10_angr_simprocedures'
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()
# 通过继承angr.SimProcedure实现对模拟函数的创建
class ReplacementCheckEquals(angr.SimProcedure):
# 重写run方法,self之后的参数与原始函数是对应的
def run(self, to_check, length):
# 对比原是函数,我们知道第一个参数是字符串的起始地址,第二个是字符串长度
user_input_buffer_address = to_check
user_input_buffer_length = length
# 利用memory.load方法,从已知内存地址中读取指定长度的内容
user_input_string = self.state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
check_against_string = 'MRXJKZYRKMKENFZB'
# 这部分是我们比较过程,和前面一题是一样的,利用claripy.If,第一个参数是布尔判断条件,为真返回第二个参数,
# 为假返回第三个参数,参数返回值应该是bitvector常量
return claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32),
)
# 调用hook_symbol前,确定需要hook的函数名称,因为hook_symbol是基于函数名进行API调用的
check_equals_symbol = 'check_equals_MRXJKZYRKMKENFZB' # :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 True if 'Good Job.' in stdout_output.decode('utf-8') else False
def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return True if 'Try again.' in stdout_output.decode('utf-8') else False
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print('[+] Success, flag is {}'.format(solution.decode('utf-8')))
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main()
11_angr_sim_scanf
本题学习hook scanf
函数,之前一直是在scanf
之后对输入的结果做符号化,本次直接模拟scanf
的执行
老规矩,源码走一波:
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BOOL4 v3; // eax
signed int i; // [esp+20h] [ebp-28h]
char s[4]; // [esp+28h] [ebp-20h]
int v7; // [esp+2Ch] [ebp-1Ch]
unsigned int v8; // [esp+3Ch] [ebp-Ch]
v8 = __readgsdword(0x14u);
memset(s, 0, 0x14u);
*(_DWORD *)s = 'ZREX';
v7 = 'BNTZ';
for ( i = 0; i <= 7; ++i )
s[i] = complex_function(s[i], i);
printf("Enter the password: ");
__isoc99_scanf("%u %u", buffer0, buffer1);
v3 = !strncmp(buffer0, s, 4u) && !strncmp(buffer1, (const char *)&v7, 4u);
if ( v3 )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
程序就不解释了,因为本题是要模拟scanf
,依照上题对函数模拟的思路:
- 首先,我们先将模拟的部分摸清楚,对于
scanf
,首先第一个传入的参数是格式化字符串%u %u
(函数调用的时候也是先将格式化字符串压栈),然后依次传入buffer0,buffer1
接下来我们要模拟输入两个变量到
buffer
中,本质上就是写入两个符号变量到内存里,因此,在模拟函数中,创建claripy.BVS('scanf', 32)
对象,并利用state.memory.store()
方法将变量写入内存由于定义的符号变量位于
run()
方法内部,使得在simulation.found
之后无法进行约束求解,因此还需要将这个两个局部变量放到全局中,解决方法可以是改用全局变量,然后在run
内部使用global
关键字,或者采用state.globals[key]=value
的方式存储。
下面是题解,我会在必要的地方添加注释,帮助理解:
import angr
import claripy
import sys
def main():
path_to_binary = '11_angr_sim_scanf'
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()
class ReplacementScanf(angr.SimProcedure):
# run现在模拟scanf,第一个参数是格式化字符串,接着传入两个buffer地址
def run(self, format_string, scanf0_address, scanf1_address):
# 定义两个符号变量,用于写入buffer
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的函数名称,用ida反编译得到的
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 True if 'Good Job.' in stdout_output.decode('utf-8') else False
def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return True if 'Try again.' in stdout_output.decode('utf-8') else False
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']
scanf0_solution = solution_state.solver.eval(stored_solutions0)
scanf1_solution = solution_state.solver.eval(stored_solutions1)
print('[+] Success, flag is {} {}'.format(scanf0_solution, scanf1_solution))
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main()
总结
本文提高篇主要核心在于对于约束条件和hook
的学习,通过约束条件能够解决一般的路径爆炸问题(如存在循环),而hook
能够对于程序执行到指定的代码部分,接管操作,跳过会产生路径爆炸的代码或模拟我们不希望执行的代码(达到同样的目的)。这里分别对做完的4道题目总结一下:
- 08_angr_constraints:第一个出现循环的代码,也是产生路径爆炸的地方,为了避免,我们
find
指定位置,并通过对比此时内存中的字符串和目标串,从而无需执行循环也能达到同样的效果。 - 09_angr_hooks:在上一题的基础上利用了
angr
的hook
,接管指定位置的代码程序,这使得程序能够跳过某一部分代码的执行,但此时是依据目标代码的地址和要跳过的字节数确定的。 - 10_angr_simprocedures:跟上题一样,只不过此处是利用函数名进行
hook
,扩展性和可读性都得到了提高,也是认识SimProcedures
的一个过程 - 11_angr_sim_scanf:对
scanf
函数进行了模拟,可以和以前操作scanf
之后修改寄存器,修改内存,修改栈的几题做对比,该方法通用性更高。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!