Angr学习记录

2022-10-16
  1. 0x00:理论知识
  2. 0x01:Angr介绍
  3. 0x02:Angr执行环境配置
  4. 0x03:angr & explore
  5. 0x04:angr & symbolic
  6. 0x04:angr & constraints
  7. 0x05:angr & hooks
  8. 0x06:CTF例题

0x00:理论知识

符号执行(Symbolic Execution)是一种程序分析技术,其功能是通过分析程序来得到让特定代码区域执行的输入。

符号执行介绍:https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/5.3_symbolic_execution.html

论文:Symbolic Execution for Software Testing: Three Decades Later

https://people.eecs.berkeley.edu/~ksen/papers/cacm13.pdf


在现实中的程序往往很复杂,分析难度大效率低,以至于无法人工地收集执行到某条路径的输入信息,这时候就可以使用程序(Angr)进行相关的辅助工作。


0x01:Angr介绍

Angr是一个利用python开发的二进制程序分析框架,我们可以利用这个工具尝试对一些CTF题目进行符号执行来找到正确的解答。当然,要注意的是符号执行的路径选择问题到现在依旧是一个很大的问题,换句话说也就是当我们的程序存在循环时,因为符号执行会尽量遍历所有的路径,所以每次循环之后会形成至少两个分支,当循环的次数足够多时,就会造成路径爆炸,整个机器的内存会被耗尽。

Angr使用符号执行分析一个程序时,会将程序的“输入”抽象化为“符号”,而非一般执行程序时使用的具体值。(注意:这里说的“符号”并不是什么字符、字符串,而是类似于数学解方程是的x、y、z等等未知的待解的变量)

Angr将程序中的一块块代码区域描述为“状态”,类型为SimState。在给定一个初始状态为参数,可使用执行模拟器simulation_manager模拟执行程序。在模拟执行的过程中,会得到不同类型的多个的状态(SimState)。通过状态,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码区域的具体值。

使用介绍(官方文档):https://docs.angr.io/core-concepts/toplevel

中文手册:https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/5.3.1_angr.html


angr练习项目:https://github.com/jakespringer/angr_ctf

对应的题解视频:https://space.bilibili.com/386563875


CTF一些例题:https://ke1os.top/2022/05/06/reverse/angr%E7%9A%84%E5%AD%A6%E4%B9%A0/#%E7%AC%A6%E5%8F%B7%E6%89%A7%E8%A1%8C


下面的大部分内容为本人在angr_ctf练习时的记录,练习过程主要的参考文章为看雪的系列文章,里面有较为详细的解释:

angr学习(1):https://bbs.pediy.com/thread-264775.htm

angr学习(2):https://bbs.pediy.com/thread-264878.htm

angr学习(3):https://bbs.pediy.com/thread-268300.htm

angr学习(4):https://bbs.pediy.com/thread-269990.htm


0x02:Angr执行环境配置

本地环境下:

目前使用的是这个,比较方便。

# 直接运行:
sudo apt install ipython3
pip install angr

#执行脚本时:ipython3 exp.py

虚拟环境:
# @@@angr容器
sudo docker run -itd --name angr angr/angr
sudo docker exec -it angr bash
su angr # root用户没有angr的环境,需切换到angr用户
# 如果su angr显示需要密码,但你又不知道密码的话,可以在上一命令加上参数:-u 0
# 即sudo docker exec -u 0 -it angr bash 可以直接root权限打开容器

# @@@安装checksec工具
mkdir /opt/tools
cd /opt/tools
# 官方
# git clone https://github.com/slimm609/checksec.sh.git
# 加速
git clone https://gitclone.com/github.com/slimm609/checksec.sh.git
 
cd checksec.sh
# 建立链接
ln -s /opt/tools/checksec.sh/checksec /usr/local/bin/checksec

# @@@克隆项目
cd /opt
 
# 官方
# git clone https://github.com/jakespringer/angr_ctf.git
# 加速
git clone https://gitclone.com/github.com/jakespringer/angr_ctf.git
 
# 查看二进制文件
cd angr_ctf/dist/

0x03:angr & explore

00_angr_find:

IDA静态分析:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+1Ch] [ebp-1Ch]
  char s1[9]; // [esp+23h] [ebp-15h] BYREF
  unsigned int v6; // [esp+2Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  printf("Enter the password: ");
  __isoc99_scanf("%8s", s1);
  for ( i = 0; i <= 7; ++i )
    s1[i] = complex_function(s1[i], i);
  if ( !strcmp(s1, "JACEJGCS") )
    puts("Good Job.");    //目标位置
  else
    puts("Try again.");
  return 0;
}

// other:
int __cdecl complex_function(int a1, int a2)
{
  if ( a1 <= 64 || a1 > 90 )
  {
    puts("Try again.");
    exit(1);
  }
  return (3 * a2 + a1 - 65) % 26 + 65;
}

//目标所在位置:
.text:08048675 loc_8048675:                            ; CODE XREF: main+9A↑j
.text:08048675                 sub     esp, 0Ch
.text:08048678                 push    offset aGoodJob ; "Good Job."
.text:0804867D                 call    _puts
.text:08048682                 add     esp, 10h

Project是angr中的控制基础,是后续操作的把柄。有了它就能对刚加载的可执行文件进行分析和仿真。几乎在angr中使用的每一个对象都依赖于Project的存在。

程序状态 state 是一个 SimState 类型的对象,angr.factory.AngrObjectFactory 类提供了创建 state 对象的方法。其中,.entry_state():从主对象文件的入口点创建一个 state。

程序执行过程中会产生很多state,模拟管理器(Simulation Managers)则是对这些状态进行管理。创建模拟管理器对象是需要一个状态,没有参数是默认是程序入口点的状态,即.entry_state()

模拟管理器对象实现了.explore() 功能,允许在探索时查找(find)或避免(avoid)某些地址。

import angr   # 引入angr模块
proj = angr.Project('./00_angr_find')  # 将一个二进制文件定义为一个对象,是后续操作的基础
init_state = proj.factory.entry_state() # 程序的入口状态,start函数,并不是main
sm = proj.factory.simulation_manager(init_state)   #使用执行模拟器,执行起点为init_state。simulation_manager()也可以缩写为simgr()
sm.explore(find=0x08048675)  # 开始模拟执行,并设置探索目标为0x08048678处
find_state = sm.found[0]  # 探索到的状态
print(find_state.posix.dumps(0)) # posix.dumps模拟posix环境下的输出输入
# 0为能到达该状态的输入值
# 1时为程序的输出信息
# 结果: b'JXWVXRKX'

01_angr_avoid:

用32IDA打开,发现对main反编译不了,提示说函数太大了。只能先看汇编大概了解一下程序。

pic01

程序和上一程序类似,但main函数中出现超级多次avoid_me函数,导致main不能在IDA反汇编。在模拟执行的explore函数中,参数除了find外还有avoid。因此我们可以通过传入avoid_me函数的地址进行符号执行分析。

目的地:.text:080485E0 push offset aGoodJob ; "Good Job."

需避免的:.text:080485A8 avoid_me proc near ; CODE XREF: main+1CA↓p

// maybe_good函数
int __cdecl maybe_good(char *s1, char *s2)
{
  if ( should_succeed && !strncmp(s1, s2, 8u) )
    return puts("Good Job.");
  else
    return puts("Try again.");
}

// avoid_me函数
void avoid_me()
{
  should_succeed = 0;   
}

另外,避开avoid_me函数不仅仅是因为其数量多导致效率低,而且其还会影响maybe_good的执行路径。

import angr
proj = angr.Project('./01_angr_avoid')
init_state = proj.factory.entry_state()
sm = proj.factory.simulation_manager(init_state)
target = 0x080485E0
bad_addr = 0x080485A8
sm.explore(find=target, avoid=bad_addr)
find_state = sm.found[0]
print(find_state.posix.dumps(0))

02_angr_find_condition:
int __cdecl complex_function(int a1, int a2)
{
  if ( a1 <= 64 || a1 > 90 )
  {
    puts("Try again.");
    exit(1);
  }
  return (31 * a2 + a1 - 65) % 26 + 65;
}

上一程序是一个函数多次调用。这个从main函数的汇编代码看,出现了很多打印Good Job.Try again.的地方。如果使用地址进行约束,就会很繁琐。本程序需要定义函数来检测输出的字符进行约束。

pic04

import angr

def is_successful(state):
    output = state.posix.dumps(1)
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(1)
    return 'Try again.' in str(output)

proj = angr.Project("./02_angr_find_condition")
init_state = proj.factory.entry_state()
sm = proj.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)
find_state = sm.found[0]
print(find_state.posix.dumps(0))

0x04:angr & symbolic

03_angr_symbolic_registers:
int get_user_input()
{
  int v1; // [esp+0h] [ebp-18h] BYREF
  int v2; // [esp+4h] [ebp-14h] BYREF
  int v3[4]; // [esp+8h] [ebp-10h] BYREF

  v3[1] = __readgsdword(0x14u);
  __isoc99_scanf("%x %x %x", &v1, &v2, v3);
  return v1;
}

//IDA
.text:0804891C                 xor     ecx, ecx
.text:0804891E                 lea     ecx, [ebp+var_10]
.text:08048921                 push    ecx
.text:08048922                 lea     ecx, [ebp+var_14]
.text:08048925                 push    ecx
.text:08048926                 lea     ecx, [ebp+var_18]
.text:08048929                 push    ecx
.text:0804892A                 push    offset aXXX     ; "%x %x %x"
.text:0804892F                 call    ___isoc99_scanf
.text:08048934                 add     esp, 10h
.text:08048937                 mov     ecx, [ebp+var_18]
.text:0804893A                 mov     eax, ecx     //v1
.text:0804893C                 mov     ecx, [ebp+var_14]
.text:0804893F                 mov     ebx, ecx     //v2
.text:08048941                 mov     ecx, [ebp+var_10]
.text:08048944                 mov     edx, ecx     //v3
.text:08048946                 nop

get_user_imput函数的作用主要是按顺序对v1、v2和v3输入值,然后v1、v2和v3又分别传值给eax、ebx和edx。


int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx
  int user_input; // eax
  int v5; // edx
  unsigned int v7; // [esp+4h] [ebp-14h]
  unsigned int v8; // [esp+8h] [ebp-10h]
  int v9; // [esp+Ch] [ebp-Ch]
  unsigned int v10; // [esp+Ch] [ebp-Ch]

  printf("Enter the password: ");
  user_input = get_user_input();
  v9 = v5;
  v7 = complex_function_1(user_input);
  v8 = complex_function_2(v3);
  v10 = complex_function_3(v9);
  if ( v7 || v8 || v10 )
    puts("Try again.");
  else
    puts("Good Job.");
  return 0;
}

//IDA
.text:08048978                 add     esp, 10h
.text:0804897B                 call    get_user_input
.text:08048980                 mov     [ebp+var_14], eax   //v7
.text:08048983                 mov     [ebp+var_10], ebx   //v8
.text:08048986                 mov     [ebp+var_C], edx    //v10
.text:08048989                 sub     esp, 0Ch

在get_user_input函数中输入的值,分别作为complex_function_1/2/3的参数进行运算,只有三个函数的返回值都为假才成功。这个程序的scanf函数的格式化字符串比其他程序的要复杂(好像只有单个输入时才能自动模拟🙃),所以需要手动注入数据。0x08048980为输入数据后的第一个指令,所以将这里设为初始状态开始模拟,在创建初始状态后,可通过init_state.regs.对指令传入符号变量,起到get_user_input函数的作用。

pic02

angr的求解引擎为Claripy,用来根据程序所需要的输入来设置符号变量以及收集限制式等等。

创建符号变量:claripy.BVS(‘x’, 32)

创建具体值变量:claripy.BVV(0x12345678, 32)

import angr
import claripy

proj = angr.Project("./03_angr_symbolic_registers")
start_addr = 0x8048980
init_state = proj.factory.blank_state(addr=start_addr) # 自定义起始地址

# 创建符号位向量(angr用于给二进制文件注入符号值的数据类型)
value0 = claripy.BVS('x', 32) # value0为32bit
value1 = claripy.BVS('y', 32)
value2 = claripy.BVS('z', 32)

# 将寄存器设置为符号值。 这是向程序中注入符号的一种方法。
# 将不同寄存器设置为不同的符号值
init_state.regs.eax = value0
init_state.regs.ebx = value1
init_state.regs.edx = value2

sm = proj.factory.simgr(init_state)

def is_successful(state):
    output = state.posix.dumps(1)
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(1)
    return 'Try again.' in str(output)

sm.explore(find=is_successful, avoid=should_abort)

find_state = sm.found[0]

# 使用eval来得到求解值
# 原解题脚本的state.se已经被废弃,根据提示使用state.solver来求解value符号变量的值
s0 = find_state.solver.eval(value0)
s1 = find_state.solver.eval(value1)
s2 = find_state.solver.eval(value2)
s = ' '.join(map('{:x}'.format, [s0,s1,s2]))
print(s)

04_angr_symbolic_stack:
int handle_user()
{
  int v1; // [esp+8h] [ebp-10h] BYREF
  int v2[3]; // [esp+Ch] [ebp-Ch] BYREF

  __isoc99_scanf("%u %u", v2, &v1);     //手动注入数据(栈数据)
  v2[0] = complex_function0(v2[0]);
  v1 = complex_function1(v1);
  if ( v2[0] == 1999643857 && v1 == -1136455217 )
    return puts("Good Job.");
  else
    return puts("Try again.");
}

.text:0804868A                 push    offset aUU      ; "%u %u"
.text:0804868F                 call    ___isoc99_scanf
.text:08048694                 add     esp, 10h
.text:08048697                 mov     eax, [ebp+var_C]    //从这里开始模拟执行
.text:0804869A                 sub     esp, 0Ch
.text:0804869D                 push    eax
.text:0804869E                 call    complex_function0

由于需要往栈里符号化输入,所以需要在启动该程序之前自己构造相应的栈结构。方法是通过state.stack_push(my_bitvector)来将值my_bitvector push到栈顶。state.regs.esp -= [ByteSize]用于填充ByteSize字节的无用数据。

pic03


import angr
import claripy

proj = angr.Project("./04_angr_symbolic_stack")
start_addr = 0x08048697
init_state = proj.factory.blank_state(addr=start_addr)

init_state.regs.ebp = init_state.regs.esp   # 这里有点看不懂
value1 = claripy.BVS('x', 32)
value2 = claripy.BVS('y', 32)
init_state.regs.esp -= 8
init_state.stack_push(v1)  
init_state.stack_push(v2)

sm = proj.factory.simgr(init_state)

def is_successful(state):
    output = state.posix.dumps(1)
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(1)
    return 'Try again.' in str(output)

sm.explore(find=is_successful, avoid=should_abort)
find_state = sm.found[0]
s1 = find_state.solver.eval(value1)
s2 = find_state.solver.eval(value2)
s = ' '.join(map(str, [s1,s2]))   #s1和s2的顺序?
print(s)

05_angr_symbolic_memory:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+Ch] [ebp-Ch]

  memset(user_input, 0, 0x21u);
  printf("Enter the password: ");
  __isoc99_scanf("%8s %8s %8s %8s", user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8);  //内存空间 bss段
  for ( i = 0; i <= 31; ++i )
    *(_BYTE *)(i + 0xA1BA1C0) = complex_function(*(char *)(i + 0xA1BA1C0), i);
  if ( !strncmp(user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 0x20u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

解题思路和上一题是一样的,不过这里使用state.memory.store(address, value)方法为全局变量代替为符号变量(操作一段连续的内存)。

import angr
import claripy

proj = angr.Project("./05_angr_symbolic_memory")
start_addr = 0x080485FE
init_state = proj.factory.blank_state(addr=start_addr)

v1 = claripy.BVS('a', 8*8)
v2 = claripy.BVS('b', 8*8)
v3 = claripy.BVS('c', 8*8)
v4 = claripy.BVS('d', 8*8)

v_start = 0x0A1BA1C0
init_state.memory.store(v_start, v1)
init_state.memory.store(v_start+8, v2)
init_state.memory.store(v_start+0x10, v3)
init_state.memory.store(v_start+0x18, v4)

def is_successful(state):
    output = state.posix.dumps(1)
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(1)
    return 'Try again.' in str(output)

sm = proj.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)

find_state = sm.found[0]
s0 = find_state.solver.eval(v1,cast_to=bytes)
s1 = find_state.solver.eval(v2,cast_to=bytes)
s2 = find_state.solver.eval(v3,cast_to=bytes)
s3 = find_state.solver.eval(v4,cast_to=bytes)
s = s0 + b' '+ s1 + b' ' + s2 + b' ' + s3
print(s.decode('utf-8'))

06_angr_symbolic_dynamic_memory:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v3; // ebx
  char *v4; // ebx
  int v6; // [esp-10h] [ebp-1Ch]
  int i; // [esp+0h] [ebp-Ch]

  buffer0 = (char *)malloc(9u);      //动态的
  buffer1 = (char *)malloc(9u);
  memset(buffer0, 0, 9u);
  memset(buffer1, 0, 9u);
  printf("Enter the password: ");
  __isoc99_scanf("%8s %8s", buffer0, buffer1, v6);
  for ( i = 0; i <= 7; ++i )
  {
    v3 = &buffer0[i];
    *v3 = complex_function(buffer0[i], i);
    v4 = &buffer1[i];
    *v4 = complex_function(buffer1[i], i + 32);
  }
  if ( !strncmp(buffer0, "UODXLZBI", 8u) && !strncmp(buffer1, "UAORRAYF", 8u) )
    puts("Good Job.");
  else
    puts("Try again.");
  free(buffer0);
  free(buffer1);
  return 0;
}

由于系统的ASLR保护机制,所以堆地址并不是固定的,因此我们不能直接对堆空间注入符号变量。但我们可以对一个固定的位置注入数据,然后将固定位置的地址赋给buffer0和buffer1,即可。

import angr
import claripy

proj = angr.Project("./06_angr_symbolic_dynamic_memory")
start_addr = 0x08048699
init_state = proj.factory.blank_state(addr=start_addr)

buf1 = 0x0ABCC8A4
buf2 = 0x0ABCC8AC
fake_addr1 = 0xffff0000
fake_addr2 = 0xffff0010

v1 = claripy.BVS('a', 8*8)
v2 = claripy.BVS('b', 8*8)
init_state.memory.store(buf1, fake_addr1, endness=proj.arch.memory_endness)
init_state.memory.store(buf2, fake_addr2, endness=proj.arch.memory_endness)

init_state.memory.store(fake_addr1, v1)
init_state.memory.store(fake_addr2, v2)

def is_successful(state):
    output = state.posix.dumps(1)
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(1)
    return 'Try again.' in str(output)

sm = proj.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)

find_state = sm.found[0]
s0 = find_state.solver.eval(v1,cast_to=bytes)
s1 = find_state.solver.eval(v2,cast_to=bytes)
s = s0 + b' ' + s1
print(s.decode('utf-8'))

07_angr_symbolic_file:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+Ch] [ebp-Ch]

  memset(buffer, 0, sizeof(buffer));            // buffer: [0x0804A0A0]
  printf("Enter the password: ");
  __isoc99_scanf("%64s", buffer);
  ignore_me((int)buffer, 0x40u);                // 将buffer的数据写入OJKSQYDP.txt
  memset(buffer, 0, sizeof(buffer));
  fp = fopen("OJKSQYDP.txt", "rb");
  fread(buffer, 1u, 0x40u, fp);
  fclose(fp);
  unlink("OJKSQYDP.txt");
  for ( i = 0; i <= 7; ++i )
    *(_BYTE *)(i + 0x804A0A0) = complex_function(*(char *)(i + 0x804A0A0), i);
  if ( strncmp(buffer, "AQWLCTXB", 9u) )
  {
    puts("Try again.");
    exit(1);
  }
  puts("Good Job.");
  exit(0);
}

考点:输入为文件(包括网络、另一个程序的输出和/dev/urandom等)时,符号化输入的方法。

方法就是将整个文件都符号化。在angr中,与文件系统、套接字、管道或终端的任何交互的根源都是一个 SimFile 对象。SimFile 是一种存储抽象,它定义一个字节序列,不管是符号的还是其他的。


通过SimFile创建一个有具体内容的文件:

内容为字符串时:

password = angr.storage.SimFile(filename, content='hello world!\n',size=len('hello world!\n'))

内容为符号值时:

symbolic_file_size_bytes = 64
password = claripy.BVS('password', symbolic_file_size_bytes * 8)
password_file = angr.storage.SimFile(filename, content=password, size=symbolic_file_size_bytes)

接着,如果想让 SimFile 对程序可用,我们需要将它放在文件系统中,模拟的文件系统是 state.fs插件。将模拟文件放入文件系统有两种方法:
一是在创建初始状态的同时将模拟文件存储进去:

initial_state = project.factory.blank_state(addr=start_address,fs={filename:password_file})

二是在创建初始状态之后单独使用insert将文件存储到文件系统中,另外还可以使用 get 和 delete 方法从文件系统中加载和删除文件。更多关于SimFile的内容可看官方文档:https://github.com/angr/angr-doc/blob/master/docs/file_system.md。


import angr
import claripy
import sys

p = angr.Project("./07_angr_symbolic_file")
start_addr = 0x080488D3
filename = "OJKSQYDP.txt"
passwd = claripy.BVS('passwd', 0x40*8)
smfile = angr.storage.SimFile(filename, content=passwd, size=0x40)

init_state = p.factory.blank_state(addr=start_addr, fs={filename:smfile})

def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Good Job.' in str(stdout_output)

def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.' in str(stdout_output)

sm = p.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)

if sm.found:
    solution_state = sm.found[0]
    solution = solution_state.solver.eval(passwd, cast_to=bytes)
    print(solution.decode('utf-8'))
else:
    raise Exception('Could not find the solution')

0x04:angr & constraints

08_angr_constraints:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+Ch] [ebp-Ch]

  qmemcpy(&password, "AUPDNNPROEZRJWKB", 16);
  memset(&buffer, 0, 0x11u);
  printf("Enter the password: ");
  __isoc99_scanf("%16s", &buffer);              // 0x804A050 buffer
  for ( i = 0; i <= 15; ++i )
    *(_BYTE *)(i + 0x804A050) = complex_function(*(char *)(i + 0x804A050), 15 - i);
  if ( check_equals_AUPDNNPROEZRJWKB(&buffer, 16) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

//check_equals_AUPDNNPROEZRJWKB函数
_BOOL4 __cdecl check_equals_AUPDNNPROEZRJWKB(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;
}

这程序比之前的程序多加了个检查函数check_equals_AUPDNNPROEZRJWKB,而且里面含有个循环,循环的次数为16,这么说这一段在探索过程路径就有2^16个。我们需要添加约束,替代该函数。(这里没想得太明白,后续再补充😴)后面用到的hook机制,也可以解决这个问题。

import angr
import claripy
import sys

proj = angr.Project('./08_angr_constraints')
state = proj.factory.blank_state(addr=0x08048625)
password = claripy.BVS('password', 16 * 8)
buffer_addr = 0x0804A050
state.memory.store(buffer_addr, password)
simgr = proj.factory.simgr(state)
simgr.explore(find=0x08048565)
found = simgr.found[0]
found.add_constraints(found.memory.load(buffer_addr, 16) == b'AUPDNNPROEZRJWKB')
print(found.solver.eval(password, cast_to=bytes))

0x05:angr & hooks

09_angr_hooks:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  _BOOL4 v3; // eax
  int i; // [esp+8h] [ebp-10h]
  int j; // [esp+Ch] [ebp-Ch]

  qmemcpy(password, "XYMKBKUHNIQYNQXE", 16);
  memset(buffer, 0, 0x11u);
  printf("Enter the password: ");
  __isoc99_scanf("%16s", buffer);
  for ( i = 0; i <= 15; ++i )
    *(_BYTE *)(i + 0x804A054) = complex_function(*(char *)(i + 0x804A054), 18 - i);
  equals = check_equals_XYMKBKUHNIQYNQXE((int)buffer, 16u);// 防止路径爆炸,使用hook进行替代
  for ( j = 0; j <= 15; ++j )
    *(_BYTE *)(j + 0x804A044) = complex_function(*(char *)(j + 0x804A044), 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;
}

为了防止执行函数check_equals_XYMKBKUHNIQYNQXE时路径爆炸,可以自己编写个函数来代替该函数去实现同样的功能。

import angr
import sys
import claripy

proj = angr.Project("./09_angr_hooks", auto_load_libs=False)
init_state = proj.factory.entry_state()
hooked_addr = 0x080486B3
hooked_len = 5

# hooked_addr:代替的地址, hooked_len:代替字节长度
@proj.hook(hooked_addr, length=hooked_len)   #修饰器
def skip_check_equals(state):
    buffer_addr = 0x0804A054
    buffer_len = 16
    user_input = state.memory.load(buffer_addr, buffer_len)
    check_against_string = "XYMKBKUHNIQYNQXE"
    state.regs.eax = claripy.If(user_input == check_against_string, 
            claripy.BVV(1,32), claripy.BVV(0,32))

def is_successful(state):
    output = state.posix.dumps(sys.stdout.fileno())
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.' in str(output)

sm = proj.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)
if sm.found:
    for i in sm.found:
        solution_state = i
        solution = solution_state.posix.dumps(0)
        print("[+] Success! Solution is: {0}".format(solution.decode('utf-8')))
else:
    raise Exception('Could not find the solution')

10_angr_simprocedures:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+20h] [ebp-28h]
  char s[17]; // [esp+2Bh] [ebp-1Dh] BYREF
  unsigned int v6; // [esp+3Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  memcpy(&password, "ORSDDWXHZURJRBDH", 0x10u);
  memset(s, 0, sizeof(s));
  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_ORSDDWXHZURJRBDH((int)s, 0x10u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

上个程序中参数的地址是固定的,这里的s是个局部变量。可以通过函数名进行hook,这样子函数的参数可以直接引用,从而解决这个问题。

import angr
import sys
import claripy

proj = angr.Project("./10_angr_simprocedures", auto_load_libs=False)
init_state = proj.factory.entry_state()

class ReplacementCheckEquals(angr.SimProcedure):
    def run(self, to_check, length):
        s_addr = to_check
        s_len = length
        user_input = self.state.memory.load(s_addr, s_len)
        check_string = "ORSDDWXHZURJRBDH"
        return claripy.If(user_input == check_string, claripy.BVV(1,32), claripy.BVV(0,32))

check_symbol = "check_equals_ORSDDWXHZURJRBDH"
proj.hook_symbol(check_symbol, ReplacementCheckEquals())

def is_successful(state):
    output = state.posix.dumps(sys.stdout.fileno())
    return 'Good Job.' in str(output)

def should_abort(state):
    output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.' in str(output)

sm = proj.factory.simgr(init_state)
sm.explore(find=is_successful, avoid=should_abort)

if sm.found:
    for i in sm.found:
        solution_state = i
        solution = solution_state.posix.dumps(0)
        print("[+] Success! Solution is: {0}".format(solution.decode('utf-8')))
else:
    raise Exception('Could not find the solution')

0x06:CTF例题

defcamp_r100

链接:https://pan.baidu.com/s/10_7MkZUdAAxS5AzXcBgZVw
提取码:lele


查看文件信息:

tolele@tolele-ubuntu22:~/angr_study/liti $ checksec r100
[*] '/home/tolele/angr_study/liti/r100'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
tolele@tolele-ubuntu22:~/angr_study/liti $ 

IDA分析:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char s[264]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v5; // [rsp+108h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("Enter the password: ");
  if ( !fgets(s, 255, stdin) )
    return 0LL;
  if ( (unsigned int)sub_4006FD((__int64)s) )
  {
    puts("Incorrect password!");
    return 1LL;
  }
  else
  {
    puts("Nice!");     //目标
    return 0LL;
  }
}

__int64 __fastcall sub_4006FD(__int64 a1)
{
  int i; // [rsp+14h] [rbp-24h]
  __int64 v3[4]; // [rsp+18h] [rbp-20h]

  v3[0] = (__int64)"Dufhbmf";
  v3[1] = (__int64)"pG`imos";
  v3[2] = (__int64)"ewUglpt";
  for ( i = 0; i <= 11; ++i )
  {
    if ( *(char *)(v3[i % 3] + 2 * (i / 3)) - *(char *)(i + a1) != 1 )
      return 1LL;
  }
  return 0LL;
}

// 目标
.text:0000000000400840                 test    eax, eax
.text:0000000000400842                 jnz     short loc_400855
.text:0000000000400844                 mov     edi, offset s   ; "Nice!"
.text:0000000000400849                 call    _puts

简单的方法:

import angr
import sys

proj = angr.Project("./r100")
sm = proj.factory.simgr()
sm.explore(find=0x400844)
final_state = sm.found[0]
print(final_state.posix.dumps(sys.stdin.fileno()))

采用hook机制:

import angr

class my_fgets(angr.SimProcedure):
    def run(self, s, num, f):
        simfd = self.state.posix.get_fd(0)
        data, real_size = simfd.read_data(12)
        self.state.memory.store(s, data)
        return 12

def main():
    p = angr.Project("./r100", auto_load_libs=False)
    p.hook_symbol('printf', angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'](), replace=True)
    p.hook_symbol('fgets', my_fgets(), replace=True)
    state = p.factory.blank_state(addr=0x4007E8)
    sm = p.factory.simulation_manager(state)
    sm.explore(find=0x400844, avoid=0x400855)
    return sm.found[0].posix.dumps(0).strip(b'\0\n')

if __name__ == "__main__":
    print(main())


还有很多练习和相关的题目,有时间再继续补充。不过得先对angr有更进一步的了解才行~

返回首页