这题如题主要是学习如何使用 angr 求解函数是外部导入在动态库 (.so) 里的题目,这题我们有了两个文件,一个是主程序 14_angr_shared_library,另一个就是库文件 lib14_angr_shared_library.so

我们先来检查一下这两个文件:

syc@ubuntu:~/Desktop/TEMP$ checksec 14_angr_shared_library
[*] '/home/syc/Desktop/TEMP/14_angr_shared_library'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
syc@ubuntu:~/Desktop/TEMP$ checksec lib14_angr_shared_library.so
[*] '/home/syc/Desktop/TEMP/lib14_angr_shared_library.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

我们用 IDA 打开这个文件,看一看函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-1Ch]
  unsigned int v5; // [esp+2Ch] [ebp-Ch]
 
  v5 = __readgsdword(0x14u);
  memset(&s, 0, 0x10u);
  print_msg();
  printf("Enter the password: ");
  __isoc99_scanf("%8s", &s);
  if ( validate(&s, 8) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

这题特殊就特殊在这个关键的 validate 函数,我们在 IDA 分析 14_angr_shared_library 时点击进去看发现无法查看源代码:

int __cdecl validate(int a1, int a2)
{
  return validate(a1, a2);
}

原因很简单,validate 是一个外部导入函数,其真正的二进制代码不在源程序里,在它所处的库文件 lib14_angr_shared_library.so 里面

我们用 IDA 打开并分析库文件 lib14_angr_shared_library.so,找到了 validate 函数的具体实现

_BOOL4 __cdecl validate(char *s1, int a2)
{
  char *v3; // esi
  char s2[4]; // [esp+4h] [ebp-24h]
  int v5; // [esp+8h] [ebp-20h]
  int j; // [esp+18h] [ebp-10h]
  int i; // [esp+1Ch] [ebp-Ch]
 
  if ( a2 <= 7 )
    return 0;
  for ( i = 0; i <= 19; ++i )
    s2[i] = 0;
  *(_DWORD *)s2 = 'GKLW';
  v5 = 'HWJL';
  for ( j = 0; j <= 7; ++j )
  {
    v3 = &s1[j];
    *v3 = complex_function(s1[j], j);
  }
  return strcmp(s1, s2) == 0;
}
int __cdecl complex_function(signed int a1, int a2)
{
  if ( a1 <= 64 || a1 > 90 )
  {
    puts("Try again.");
    exit(1);
  }
  return (41 * a2 + a1 - 65) % 26 + 65;
}

其实和之前的题目并没有什么太大的不同,关键在于如果让 angr 处理这个外部导入函数

动态链接

要详细了解,这里推荐阅读《程序员的自我修养——链接、装载与库

链接、装载与库

在 Linux 下使用 GCC 将源码编译成可执行文件的过程可以分解为 4 个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。一个简单的 hello word 程序编译过程如下:

动态链接的基本思想是把程序按照模块拆分成相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都连接成一个单独的可执行文件。ELF 动态链接文件被称为 动态共享对象(DSO,Dynamic Shared Object),简称共享对象,它们一般都是.so 为扩展名的文件。相比静态链接,动态链接有两个优势,一是共享对象在磁盘和内存只有一份,节省了空间;二是升级某个共享模块时,只需要将目标文件替换,而无须将所有的程序重新链接

共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。为了能够使共享对象在任意地址装载,在连接时对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成,即装载时重定位

这题我们简单理解共享库都是是用位置无关的代码编译的,我们需要指定基址。共享库中的所有地址都是 base + offset,其中 offset 是它们在文件中的偏移地址

我们现在先上 EXP,然后再逐步分析:

import angr
import claripy
import sys
 
def Go():
    path_to_binary = "./lib14_angr_shared_library.so" 
 
    base = 0x4000000
    project = angr.Project(path_to_binary, load_options={ 
        'main_opts' : { 
        'custom_base_addr' : base 
        } 
    })
 
    buffer_pointer = claripy.BVV(0x3000000, 32)
 
    validate_function_address = base + 0x6d7
    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)
 
    success_address = base + 0x783
    simulation.explore(find=success_address)
 
    if simulation.found:
        for i in simulation.found:
            solution_state = i
            solution_state.add_constraints(solution_state.regs.eax != 0)
            solution = solution_state.solver.eval(password,cast_to=bytes)
            print("[+] Success! Solution is: {0}".format(solution))
            #print(scanf0_solution, scanf1_solution)
    else:
        raise Exception('Could not find the solution')
 
if __name__ == "__main__":
    Go()

运行一下查看:

这题直接对库文件 lib14_angr_shared_library.so 进行符号执行求解,但问题在于库文件是需要装载才能运行的,无法单独运行,于是我们需要指定基地址

还记得我们查看的程序信息嘛

syc@ubuntu:~/Desktop/TEMP$ checksec 14_angr_shared_library
[*] '/home/syc/Desktop/TEMP/14_angr_shared_library'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

这题是没有开启 PIE,所以加载基地址是不会变化的,我们可以直接设定 0x8048000

pre-binary 选项

如果你想要对一个特定的二进制对象设置一些选项,CLE 也能满足你的需求在加载二进制文件时可以设置特定的参数,使用 main_optslib_opts 参数进行设置。

  • backend - 指定 backend
  • base_addr - 指定基址
  • entry_point - 指定入口点
  • arch - 指定架构

示例如下:

>>> angr.Project('examples/fauxware/fauxware', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project examples/fauxware/fauxware>

参数 main_optslib_opts 接收一个以 python 字典形式存储的选项组。main_opts 接收一个形如{选项名 1:选项值 1,选项名 2:选项值 2……}的字典,而 lib_opts 接收一个库名到形如{选项名 1: 选项值 1,选项名 2: 选项值 2……}的字典的映射。

lib_opts 是二级字典,原因是一个二进制文件可能加载多个库,而 main_opts 指定的是主程序加载参数,而主程序一般只有一个,因此是一级字典。

这些选项的内容因不同的后台而异,下面是一些通用的选项:

  • backend —— 使用哪个后台,可以是一个对象,也可以是一个名字 (字符串)
  • custom_base_addr —— 使用的基地址
  • custom_entry_point —— 使用的入口点
  • custom_arch —— 使用的处理器体系结构的名字

所以我们可以得到脚本的第一部分

path_to_binary = "./lib14_angr_shared_library.so" 
base = 0x8048000
project = angr.Project(path_to_binary, load_options={ 
		'main_opts' : { 
        'custom_base_addr' : base 
	} 
})

我们这里调用的是使用 .call_state 创建 state 对象,构造一个已经准备好执行 validate 函数的状态,所以我们需要设定好需要传入的参数。先回顾一下 validate 函数的原型

validate(char *s1, int a2)

我们可以通过 BVV(value,size)BVS( name, size) 接口创建位向量,先创建一个缓冲区 buffer 作为参数 char *s1,因为设定的缓冲区地址在 0x3000000,又因为 32 位程序里 int 类型为 4 字节,即 32 比特,故得

buffer_pointer = claripy.BVV(0x3000000, 32)

然后从 IDA 中不难得出 validate 的偏移量为 0x6D7,然后因为需要比较的字符串长度为 8,故利用 BVV 传入参数 int a2,最后得到

buffer_pointer = claripy.BVV(0x3000000, 32)
validate_function_address = base + 0x6d7
initial_state = project.factory.call_state(validate_function_address, buffer_pointer, claripy.BVV(8, 32))

然后利用 BVS 创建一个符号位向量,作为符号化的传入字符串传入我们之前设定好的缓冲区地址中,这里继续利用 memory.store 接口

password = claripy.BVS('password', 8*8)
initial_state.memory.store(buffer_pointer, password)

这里判断我们路径正确的方法有两种

  • 同我们之前 Hook 部分一样,Hook 判断部分
  • 搜索函数执行完的返回地址,然后根据诺正确则 EAX 的值不为 0,添加约束条件求解

这里我们选用了第二种方式

success_address = base + 0x783
simulation.explore(find=success_address)

之后的部分同之前的题目类似,不再赘述