[原创] angr 使用技巧速通笔记(二) | 宜武汇-ag真人国际厅网站

第一章的时候大概讲了 angr 的一些基本概念和使用,我思量着应该要弄点实际的东西来练练才能把这个工具用熟捻。

 

最经典的使用案例无疑是 angr_ctf 中的那些了:

https://github.com/jakespringer/angr_ctf

 

题目本身都不是很难,甚至大多都是能靠人力完成的工作。但是即便如此,自动化也有自动化的意义对不对。毕竟我们现在需要的不是马上就能用它解决各种难题,而是把简单的问题解决,然后才能开始做复杂问题。

附件使用的是 https://github.com/zero-a-one/angrctf_fitm 仓库下编译好的版本。因为原仓库下只有源代码,而且编译还需要另外去配环境,所以这里直接用了这位师傅编译好的附件。

一般的基本流程如下:

  • 创建项目:angr.project(“./binary”)
  • 创建 state:project.factory.entry_state()
  • 创建 sm:project.factory.simgr(state)
  • 探索路径:sim.explore(find=addr)
  • 给出结果:sim.found

00_angr_find

当然还是得从最简单的开始,题目本身是一个直接用 ida 读就能读明白的简单程序,但出于练习目的,还是得手写一下脚本。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp 1ch] [ebp-1ch]

  char v5[9]; // [esp 23h] [ebp-15h] byref

  unsigned int v6; // [esp 2ch] [ebp-ch]

 

  v6 = __readgsdword(0x14u);

  printf("enter the password: ");

  __isoc99_scanf("%8s", v5);

  for ( i = 0; i <= 7; i )

    v5[i] = complex_function(v5[i], i);

  if ( !strcmp(v5, "jacejgcs") )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

首先需要创建项目:

1

2

import angr

project=angr.project("./00_angr_find",auto_load_libs=false)

创建 state:

1

state=project.factory.entry_state()

创建 sm:

1

sim=project.factory.simgr(state)

搜索路径:

 

探索路径时需要给出需要查找到的路径地址,这里我们通过 ida 可以确定程序输出 “good job.” 时的地址为 0x08048675

1

sim.explore(find=0x08048675)

求解结果:

1

2

3

4

if sim.found:

    res=sim.found[0]

    res=res.posix.dumps(0)

    print(res)

简单说明一下代码。

  • sim.found[0] 代表了探索路径时得到的一条可解的路径。
  • res.posix.dumps(0) 表示去获取对应路径中,stdin 的内容。

01_angr_avoid

程序本身很大,ida 虽然也有办法反编译,但是速度极慢,但用 angr 设定好参数就很快了。

 

前几个步骤是一样的:

1

2

3

4

import angr

project=angr.project("./01_angr_avoid",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

我们不妨试试,如果按照上一题的做法会如何:

1

2

3

4

5

sim.explore(find=0x080485e0)

if sim.found:

    res=sim.found[0]

    res=res.posix.dumps(0)

    print(res)

结果会发现等了很久也没有算出结果,因为分支实在太多了。

 

因此要对代码做一点改进:

1

2

3

4

5

6

7

8

9

#080485e0                 push    offset agoodjob ; "good job."

 

# .text:080485a8 push ebp

# .text:080485a9 mov ebp, esp

# .text:080485ab mov should_succeed, 0

# .text:080485b2 nop

# .text:080485b3 pop ebp

# .text:080485b4 retn

sim.explore(find=0x080485e0,avoid=0x080485a8)

其实只是给 explore 增加了一个 avoid 的参数。当代码模拟执行遇到了该地址时,将会把这段路径放入到 avoided 的一个列表中,用来表示被避开的路径,然后其他照旧,继续执行。

 

之所以通过添加这样的操作就能够得到答案,其实很简单,是为了避免路径爆炸而必要的。

 

我们可以用这么一个二插树来表示路径:

 

 

我们用 1 来表示正确的路径,0 表示错误的路径。可以看见,在这个树中一共有 8 条不同的路径,而正确的路径只有一个。

 

假设所有涉及到 0 的路径都会进入到某个地址 x 处。那么如果没有使用 avoid 参数,angr 就会遍历这 8 条路径,然后求解出最左的那条路径所需的输入。

 

而如果我们添加了 avoid=x ,那么当 angr 从根节点进入到右子树时,由于接下来立刻进入到 x 地址处,因此停止分析这条路径,将其加入到 avoided 中,从而将下面的 4 条路径全都舍弃,将所需的时间直接减少了一半。

 

同理,当它进入左子树时,仍然存在分叉,而进入右子树的分叉会因为相同的原因被舍弃,从而再次减少一半的时间。

 

在路径极其庞大的情况下,比如说 2^31 条路径,通过这种方法能够极大程度降低消耗。

02_angr_find_condition

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp 18h] [ebp-40h]

  int j; // [esp 1ch] [ebp-3ch]

  char v6[20]; // [esp 24h] [ebp-34h] byref

  char v7[20]; // [esp 38h] [ebp-20h] byref

  unsigned int v8; // [esp 4ch] [ebp-ch]

 

  v8 = __readgsdword(0x14u);

  for ( i = 0; i <= 19; i )

    v7[i] = 0;

  qmemcpy(v7, "vxrrjeur", 8);

  printf("enter the password: ");

  __isoc99_scanf("%8s", v6);

  for ( j = 0; j <= 7; j )

    v6[j] = complex_function(v6[j], j 8);

  if ( !strcmp(v6, v7) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

还是这个模板:

1

2

3

4

import angr

project=angr.project("./02_angr_find_condition",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

这题的情况和 00_angr_find 有些不太一样。尽管 ida 将它们反编译后的结果看起来很像,但是在汇编中却有很大差别:

 

 

可以看见,这行输出在 main 函数里到处都是,所以其实很难找到真正的那条路径的地址。

 

同理的,“try again.” 也一样,因此需要修改 find 参数:

1

2

3

4

5

6

7

8

def succ(state):

    res=state.posix.dumps(1)

    if b"good job." in res:

        return true

    else:

        return false

 

sim.explore(find=succ)

可以发现,find 参数除了能是一个具体的地址外,还可以是一个函数。该函数返回 true 时会将路径记录下来,返回 false 时则表示路径并非我们想找的。

 

而区别路径的关键在于 state.posix.dumps(1) ,通过该方法,可以将 stdout 中的内容 dump 出来进行比较。如果输出包含了 good job. ,我们就认为是想要的路径。这样就能避开直接使用地址了。

 

当然了,avoid 也可以这么用,读者可以自行试试。

03_angr_simbolic_registers

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int v3; // ebx

  int v4; // eax

  int v5; // edx

  int v6; // st1c_4

  unsigned int v7; // st14_4

  unsigned int v9; // [esp 8h] [ebp-10h]

  unsigned int v10; // [esp ch] [ebp-ch]

 

  printf("enter the password: ");

  v4 = get_user_input();

  v6 = v5;

  v7 = complex_function_1(v4);

  v9 = complex_function_2(v3);

  v10 = complex_function_3(v6);

  if ( v7 || v9 || v10 )

    puts("try again.");

  else

    puts("good job.");

  return 0;

}

还是老三样:

1

2

3

4

import angr

project=angr.project("./03_angr_symbolic_registers",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

有些特殊的地方是,输入使用 get_user_input ,而该函数如下:

1

2

3

4

5

6

7

8

9

10

11

int get_user_input()

{

  int v1; // [esp 0h] [ebp-18h]

  int v2; // [esp 4h] [ebp-14h]

  int v3; // [esp 8h] [ebp-10h]

  unsigned int v4; // [esp ch] [ebp-ch]

 

  v4 = __readgsdword(0x14u);

  __isoc99_scanf("%x %x %x", &v1, &v2, &v3);

  return v1;

}

前文曾提到过,angrscanf 这类使用格式化字符串的函数支持并不是很好,不过或许是最近的版本更新,直接这样写也同样能得到结果了:

1

2

3

4

5

6

7

sim.explore(find=0x80489e9)

if sim.found:

    res=sim.found[0]

    res=res.posix.dumps(0)

    print(res)# b'b9ffd04e ccf63fe8 8fd4d959'

else:

    print("no")

不过既然是学习,还是照例看看最标准的写法应该是什么吧。

 

根据汇编可以看到,该函数的实际操作是将值储存在寄存器中:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

.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

.text:0804893c                 mov     ecx, [ebp var_14]

.text:0804893f                 mov     ebx, ecx

.text:08048941                 mov     ecx, [ebp var_10]

.text:08048944                 mov     edx, ecx

因此我们可以直接将该函数钩取,然后手动设置寄存器的值:

1

2

3

import angr

project=angr.project("./03_angr_symbolic_registers",auto_load_libs=false)

state=project.factory.blank_state(addr=0x08048980)

由于现在我们再从 entry_point 进入了,而需要跳过 get_user_input 函数,因此使用 blank_state 来初始化状态,并将开始地址设定在该函数之后的第一条指令处。

 

接下来创建三个位置的符号向量,将他们设定为寄存器:

1

2

3

4

5

6

7

8

9

import claripy

input1=claripy.bvs("input1",32)

input2=claripy.bvs("input2",32)

input3=claripy.bvs("input3",32)

state.regs.eax=input1

state.regs.ebx=input2

state.regs.edx=input3

sim=project.factory.simgr(state)

sim.explore(find=0x80489e9)

此处引入另外一个 claripy 包来创建符号向量: claripy.bvs(name,size) 。创建完成后即可生成 sm 并开始探索了。

 

完成探索后,最后需要求解符号向量的值:

1

2

3

4

5

6

7

8

if sim.found:

    res=sim.found[0]

    res1=res.solver.eval(input1)

    res2=res.solver.eval(input2)

    res3=res.solver.eval(input3)

    print(hex(res1) " " hex(res2) " " hex(res3))#0xb9ffd04e 0xccf63fe8 0x8fd4d959

else:

    print("no")

04_angr_symbolic_stack

1

2

3

4

5

6

7

8

9

10

11

12

13

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.");

}

到这一步其实就差不多轻车熟路一把梭搞定了:

1

2

3

4

5

6

7

8

9

import angr

project=angr.project("./04_angr_symbolic_stack",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x080486e4)

if sim.found:

    res=sim.found[0]

    res=res.posix.dumps(0)

    print(res)#b'1704280884 2382341151'

不过这道题实际上和上一题类似,但输入值储存在栈中,因此标准做法其实是将内存符号化进行求解:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

import angr

project=angr.project("./04_angr_symbolic_stack",auto_load_libs=false)

state=project.factory.blank_state(addr=0x08048694)

 

import claripy

input1=claripy.bvs("input1",32)

input2=claripy.bvs("input2",32)

state.regs.ebp=state.regs.esp

state.regs.esp-=0x1c

state.memory.store(state.regs.ebp-0xc,input1)

state.memory.store(state.regs.ebp-0x10,input2)

 

sim=project.factory.simgr(state)

sim.explore(find=0x080486e4)

if sim.found:

    res=sim.found[0]

    res=res.solver.eval(input1)

    print(res)

    res=sim.found[0]

    res=res.solver.eval(input2)

    print(res)

通过 state.memory.store(addr,value) 可以对内存进行符号化,从而在路径发现以后进行求解。

05_angr_symbolic_memory

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp ch] [ebp-ch]

 

  memset(&user_input, 0, 33);

  printf("enter the password: ");

  __isoc99_scanf("%8s %8s %8s %8s", &user_input, &unk_a1ba1c8, &unk_a1ba1d0, &unk_a1ba1d8);

  for ( i = 0; i <= 31; i )

    *(i 169583040) = complex_function(*(i 169583040), i);

  if ( !strncmp(&user_input, "njpurzpcdyeaxcsjzjmpsombfddlhbvn", 32) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

这道题同样因为现在的 angr 功能强大而不需要以前那样复杂的技巧了:

1

2

3

4

5

6

7

8

9

import angr

project=angr.project("./05_angr_symbolic_memory",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x0804866d)

 

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))#b'naxthgnr jvsftpwe lmgauhwc xmdcpalu'

而题目的本意是让我们将内存符号化,其实和上一题一样,直接对内存进行存储就行了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import angr

project=angr.project("./05_angr_symbolic_memory",auto_load_libs=false)

state=project.factory.blank_state(addr=0x080485fe)

 

import claripy

pwd1=claripy.bvs("pwd1",64)

pwd2=claripy.bvs("pwd2",64)

pwd3=claripy.bvs("pwd3",64)

pwd4=claripy.bvs("pwd4",64)

state.memory.store(0x0a1ba1c0,pwd1)

state.memory.store(0x0a1ba1c0 8,pwd2)

state.memory.store(0x0a1ba1c0 8 8,pwd3)

state.memory.store(0x0a1ba1c0 8 8 8,pwd4)

 

sim=project.factory.simgr(state)

sim.explore(find=0x0804866d)

 

if sim.found:

    res=sim.found[0]

    print(res.solver.eval(pwd1))

    print(res.solver.eval(pwd2))

    print(res.solver.eval(pwd3))

    print(res.solver.eval(pwd4))

06_angr_symbolic_dynamic_memory

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

int __cdecl main(int argc, const char **argv, const char **envp)

{

  _byte *v3; // ebx

  _byte *v4; // ebx

  int v6; // [esp-18h] [ebp-24h]

  int v7; // [esp-14h] [ebp-20h]

  int v8; // [esp-10h] [ebp-1ch]

  int v9; // [esp-8h] [ebp-14h]

  int v10; // [esp-4h] [ebp-10h]

  int v11; // [esp 0h] [ebp-ch]

  int i; // [esp 0h] [ebp-ch]

 

  buffer0 = malloc(9, v6, v7, v8);

  buffer1 = malloc(9, v9, v10, v11);

  memset(buffer0, 0, 9);

  memset(buffer1, 0, 9);

  printf("enter the password: ");

  __isoc99_scanf("%8s %8s", buffer0, buffer1);

  for ( i = 0; i <= 7; i )

  {

    v3 = (_byte *)(buffer0 i);

    *v3 = complex_function(*(char *)(buffer0 i), i);

    v4 = (_byte *)(buffer1 i);

    *v4 = complex_function(*(char *)(buffer1 i), i 32);

  }

  if ( !strncmp(buffer0, "uodxlzbi", 8) && !strncmp(buffer1, "uaorrayf", 8) )

    puts("good job.");

  else

    puts("try again.");

  free(buffer0);

  free(buffer1);

  return 0;

}

和上一题不同的地方在于,这次的存储位置为堆内存,我们不能直接给出一个地址然后去存储。

 

一把梭还是可行的:

1

2

3

4

5

6

7

8

import angr

project=angr.project("./06_angr_symbolic_dynamic_memory",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x08048759)

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

而标准做法是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

import angr

project=angr.project("./06_angr_symbolic_dynamic_memory",auto_load_libs=false)

state=project.factory.blank_state(addr=0x08048699)

buff0=0x0abcc8a4

buff1=0x0abcc8ac

 

import claripy

pwd1=claripy.bvs("pwd1",64)

pwd2=claripy.bvs("pwd2",64)

 

state.memory.store(buff0,0xffffff00,endness=project.arch.memory_endness)

state.memory.store(buff1,0xffffff80,endness=project.arch.memory_endness)

 

state.memory.store(0xffffff00,pwd1)

state.memory.store(0xffffff80,pwd2)

 

sim=project.factory.simgr(state)

sim.explore(find=0x08048759)

if sim.found:

    res=sim.found[0]

    print(res.solver.eval(pwd1))

    print(res.solver.eval(pwd2))

通过这题就能够理解符号执行的一个好处了。由于它并不是真的去执行,只是模拟执行代码而已,所以对地址本身没有限制,完全可以随意设定内存的使用方法。

 

另外 endness 参数用于指定储存的端序,而 project.arch.memory_endness 将会反映程序所在平台的默认端序,此处为小端序。

07_angr_symbolic_file

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int result; // eax

  int i; // [esp ch] [ebp-ch]

 

  memset(&buffer, 0, 64);

  printf("enter the password: ");

  __isoc99_scanf("ds", &buffer);

  ignore_me(&buffer, 64);

  memset(&buffer, 0, 64);

  fp = fopen("ojksqydp.txt", "rb");

  fread(&buffer, 1, 64, fp);

  fclose(fp);

  unlink("ojksqydp.txt");

  for ( i = 0; i <= 7; i )

    *(_byte *)(i 134520992) = complex_function(*(char *)(i 134520992), i);

  if ( strncmp(&buffer, "aqwlctxb", 9) )

  {

    puts("try again.");

    exit(1);

  }

  puts("good job.");

  exit(0);

  _libc_csu_init();

  return result;

}

可以发现程序调用了 fopen 去打开文件,对于这种情况,angr 也同样提供了模拟文件的系统。

 

同样的,照旧一把梭也能搞定:

1

2

3

4

5

6

7

8

9

import angr

project=angr.project("./07_angr_symbolic_file",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x080489b0)

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

#b'azommmzm\x00@\x04\x00\x01\x01\x01\x01\x01\x00\x00\x00\x02\x00\x01\x00\x80\x04\x80\x00\x02\x01\x04\x00\x02\x80\x08\x01\x00\x02\x01\x01\x01@\x01\x00\x08\x08\x04\x80\x04\x01\x80\x01\x04\x80\x02\x00\x00@\x00\x00\x00\x00\x00\x00'

不过还是来看看它的模拟文件系统吧:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import angr

import claripy

project=angr.project("./07_angr_symbolic_file",auto_load_libs=false)

state=project.factory.blank_state(addr=0x080488ea)

filename = 'ojksqydp.txt'

pwd1=claripy.bvs("pwd1",64*8)

 

pwdfile=angr.storage.simfile(filename,content=pwd1,size=64)

state.fs.insert(filename,pwdfile)

 

sim=project.factory.simgr(state)

sim.explore(find=0x080489b0)

if sim.found:

    res=sim.found[0]

    print(hex(res.solver.eval(pwd1)))

 

#0x415a4f4d4d4d5a4d0000000000000000000000000002000020000000000200000000000000008000000000401002000000000000000000000004001000000000

前几个还是照旧,但是也有一些新东西:

1

2

pwdfile=angr.storage.simfile(filename,content=pwd1,size=64)

state.fs.insert(filename,pwdfile)

angr.storage.simfile 提供了一个模拟文件系统,通过 state.fs.insert 可以将该模拟出来的文件插入到 state 符号中。这样在模拟执行时就会用该文件替代真实情况下的文件了。

 

angr.storage.simfilefilename 参数表示文件名,content 参数表示文件内容,size 参数表示文件大小,单位为字节。

08_angr_constraints

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp ch] [ebp-ch]

 

  qmemcpy(&password, "aupdnnproezrjwkb", 16);

  memset(&buffer, 0, 17);

  printf("enter the password: ");

  __isoc99_scanf("s", &buffer);

  for ( i = 0; i <= 15; i )

    *(i 134520912) = complex_function(*(i 134520912), 15 - i);

  if ( check_equals_aupdnnproezrjwkb(&buffer, 16) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

bool __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 ( *(i a1) == *(i 134520896) )

       v3;

  }

  return v3 == a2;

}

在这里就能遇到之前所说的 “路径爆炸” 问题了。

 

照例试试一把梭:

1

2

3

4

5

6

7

import angr

project=angr.project("./08_angr_constraints",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x08048694)

if sim.found:

    print("yes")

会发现这次就没办法那么顺利得到答案了,angr 求解了半天却一直没有给出 “yes” 的回答,因此这次我们必须手动去优化求解的过程。

 

分析 check_equals_aupdnnproezrjwkb 函数可以发现,该函数实际上是在对输入和 password 对比,而 password 的值是固定的 aupdnnproezrjwkb

 

因此第一种缓解路径爆炸的方法是,只需要探索到进入该路径即可。而此后的求解过程通过人为的方法手动增加。

 

首先还是创建状态,这里我们跳过了 scanf

1

2

3

import angr

project=angr.project("./08_angr_constraints",auto_load_libs=false)

state=project.factory.blank_state(addr=0x08048625)

接下来我们为 buffer 创建符号,并开始探索:

1

2

3

4

5

6

import claripy

pwd=claripy.bvs("pwd",16*8)

state.memory.store(0x0804a050,pwd)

 

sim=project.factory.simgr(state)

sim.explore(find=0x08048565)

此处地址 0x08048565 对应了 check_equals_aupdnnproezrjwkb 函数的第一行指令。这样就不必进入到会引发路径爆炸的循环中了。

 

最后,在找到路径以后,为求解器主动添加条件:

1

2

3

4

5

if sim.found:

    res=sim.found[0]

    now_str=state.memory.load(0x0804a050,16)

    res.solver.add("aupdnnproezrjwkb"==now_str)

    print(res.solver.eval(pwd))

我们需要保证的是,在进入 check_equals_aupdnnproezrjwkb 函数时,buffer 处的内容和字符串 aupdnnproezrjwkb 相同,因此直接添加条件即可求解。

09_angr_hooks

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

int __cdecl main(int argc, const char **argv, const char **envp)

{

  bool v3; // eax

  int i; // [esp 8h] [ebp-10h]

  int j; // [esp ch] [ebp-ch]

 

  qmemcpy(&password, "xymkbkuhniqynqxe", 16);

  memset(&buffer, 0, 17);

  printf("enter the password: ");

  __isoc99_scanf("s", &buffer);

  for ( i = 0; i <= 15; i )

    *(_byte *)(i 134520916) = complex_function(*(char *)(i 134520916), 18 - i);

  equals = check_equals_xymkbkuhniqynqxe(&buffer, 16);

  for ( j = 0; j <= 15; j )

    *(_byte *)(j 134520900) = complex_function(*(char *)(j 134520900), j 9);

  __isoc99_scanf("s", &buffer);

  v3 = equals && !strncmp(&buffer, &password, 16);

  equals = v3;

  if ( v3 )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

而上一题的操作总归来说是解一时之急,因为函数正好在最后的位置,所以停在那边就足够了。但是如果路径爆炸发生在中途,就不能这么做了,我们需要更好的方法解决它。

 

首先是路径爆炸会发生在 check_equals_xymkbkuhniqynqxe 函数中,它和上一题的函数是一样的。

 

前几个还是一样:

1

2

3

4

import angr

import claripy

project=angr.project("./09_angr_hooks",auto_load_libs=false)

state=project.factory.entry_state()

接下来是对该函数进行钩取:

1

2

3

4

5

@project.hook(0x080486b3, length=5)

def skip_check(state):

    compare_str="xymkbkuhniqynqxe"

    now_str=state.memory.load(0x0804a054,16)

    state.regs.eax=claripy.if(compare_str==now_str,claripy.bvv(1, 32),claripy.bvv(0, 32))

钩取方法可以通过 @project.hook 宏完成。第一个参数为对应的机器码地址,第二个参数为钩取的指令长度。此处因为我们只需要钩取 call 指令,因此长度为 5。

 

而钩子下面对应的需要定义钩子函数,此处我们将 buffer 的内容读取出来进行比较,并根据结果使用 claripy.if 来设置 eax 寄存器。

 

最后探索路径即可:

1

2

3

4

5

6

sim=project.factory.simgr(state)

sim.explore(find=0x08048768)

 

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

此方法为第二个缓解路径爆炸的方法,即直接对地址进行钩取。

10_angr_simprocedures

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp 20h] [ebp-28h]

  char v5[17]; // [esp 2bh] [ebp-1dh] byref

  unsigned int v6; // [esp 3ch] [ebp-ch]

 

  v6 = __readgsdword(0x14u);

  memcpy(&password, "orsddwxhzurjrbdh", 16);

  memset(v5, 0, sizeof(v5));

  printf("enter the password: ");

  __isoc99_scanf("s", v5);

  for ( i = 0; i <= 15; i )

    v5[i] = complex_function(v5[i], 18 - i);

  if ( check_equals_orsddwxhzurjrbdh(v5, 16) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

第 10 题看起来和上一题一样,但是还是那个问题,如果调用点很多该怎么办?虽然 ida 分析出的结果相似,但是通过交叉引用可以发现:

 

 

显然不太可能每次都对地址进行钩取,因此需要有一个方法直接钩取函数:

1

2

3

4

import angr

import claripy

project=angr.project("./10_angr_simprocedures",auto_load_libs=false)

state=project.factory.entry_state()

接下来钩取函数:

1

2

3

4

5

6

7

class replacecmp(angr.simprocedure):

    def run(self,arg1,arg2):

        cmp_str="orsddwxhzurjrbdh"

        input_str=self.state.memory.load(arg1,arg2)

        return claripy.if(cmp_str==input_str,claripy.bvv(1,32),claripy.bvv(0,32))

 

project.hook_symbol("check_equals_orsddwxhzurjrbdh", replacecmp())

首先需要声明一个类,并定义 run 方法,而该方法将取代想要钩取的函数。其参数会和钩取的函数有相同的参数列表,但除此之外还需要一个 self

 

至于 run 函数的实现则各不相同了。这里我们就直接模仿比较函数的最终效果,返回比较的结果。

 

然后调用 project.hook_symbol 方法直接以函数名为参数对函数进行钩取即可。

11_angr_sim_scanf

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp 20h] [ebp-28h]

  char v6[20]; // [esp 28h] [ebp-20h] byref

  unsigned int v7; // [esp 3ch] [ebp-ch]

 

  v7 = __readgsdword(0x14u);

  print_msg();

  memset(v6, 0, sizeof(v6));

  qmemcpy(v6, "dcluesmr", 8);

  for ( i = 0; i <= 7; i )

    v6[i] = complex_function(v6[i], i);

  printf("enter the password: ");

  __isoc99_scanf("%u %u", &buffer0, &buffer1);

  if ( !strncmp(&buffer0, v6, 4) && !strncmp(&buffer1, &v6[4], 4) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

发现一把梭能解决:

1

2

3

4

5

6

7

8

9

import angr

project=angr.project("./11_angr_sim_scanf",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

sim.explore(find=0x0804fca1)

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

    #b'1146242628 1296386129'

不过原题的目的是让我们去钩取 scanf 函数。其实做法和上一题一样,这里就不再重复了。不过有一点我们必须抱有疑问,我们知道这类函数的参数数量是不确定的,但如果想要钩取一个函数,我们就需要给定一个确定的参数列表,这样才能定义 run 方法。

 

这个问题我们留待以后阅读源代码再做考虑。至少目前来看,angr 已经完善了 scanf 函数的 hook 了,我们可以直接一把梭解决这个问题。

12_angr_veritesting

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

// bad sp value at call has been detected, the output may be wrong!

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int v3; // ebx

  int v5; // [esp-10h] [ebp-5ch]

  int v6; // [esp-ch] [ebp-58h]

  int v7; // [esp-8h] [ebp-54h]

  int v8; // [esp-4h] [ebp-50h]

  const char **v9; // [esp 0h] [ebp-4ch]

  int v10; // [esp 4h] [ebp-48h]

  int v11; // [esp 8h] [ebp-44h]

  int v12; // [esp ch] [ebp-40h]

  int v13; // [esp 10h] [ebp-3ch]

  int v14; // [esp 10h] [ebp-3ch]

  int v15; // [esp 14h] [ebp-38h]

  int i; // [esp 14h] [ebp-38h]

  int v17; // [esp 18h] [ebp-34h]

  int v18[9]; // [esp 1ch] [ebp-30h] byref

  unsigned int v19; // [esp 40h] [ebp-ch]

  int *p_argc; // [esp 44h] [ebp-8h]

 

  p_argc = &argc;

  v9 = argv;

  v19 = __readgsdword(0x14u);

  print_msg();

  memset(

    v18 3,

    0,

    33,

    v5,

    v6,

    v7,

    v8,

    v9,

    v10,

    v11,

    v12,

    v13,

    v15,

    v17,

    v18[0],

    v18[1],

    v18[2],

    v18[3],

    v18[4],

    v18[5]);

  printf("enter the password: ");

  __isoc99_scanf("2s", v18 3);

  v14 = 0;

  for ( i = 0; i <= 31; i )

  {

    v3 = *(v18 i 3);

    if ( v3 == complex_function(87, i 186) )

       v14;

  }

  if ( v14 != 32 || v19 )

    puts("try again.");

  else

    puts("good job.");

  return 0;

}

既然我们是通过钩取函数来解决某个函数的路径爆炸问题,那么就肯定会遇到这么一种情况:函数的某部分引发路径爆炸,但其他部分在做必要的运算

 

本题就可以发现,循环判断语句嵌在 main 函数中,我们显然不能直接把整个 main 函数 hook 掉,那样就和直接读代码逆向没区别了。

 

angr 提供了一种名为 veritesting 的算法,它能够让符号执行引起在 动态符号执行dse静态符号执行sse 之间协同工作从而减少路径爆炸的问题。

 

angr 中只需要为 project.factory.simgr 添加一个参数 veritesting=true 即可开启。

1

2

3

4

5

6

7

8

9

10

import angr

project=angr.project("./12_angr_veritesting",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state,veritesting=true)

 

sim.explore(find=0x08048684)

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

b'cxsnidytojezupkfavqlgbwrmhcxsnid'

不过不得不说的是,这个方法看起来好像很万能,其实并没有想象中的那么好用。对于本题的这个体量来说,笔者执行了约 5 次才有一次能够迅速的算出结果。可想而知,对于体积稍微大一些,类似的循环稍微多一些的程序来说,这个方法并不能带来多大的提升,反而会让人难以猜测程序究竟是卡在路径爆炸中还是仍然处于计算。

 

因此对于一些简单的问题,笔者虽然推荐这个方法,但只要问题稍微复杂一点,它甚至会增加人力负担。

13_angr_static_binary

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

int __cdecl main(int argc, const char **argv, const char **envp)

{

  int i; // [esp 1ch] [ebp-3ch]

  int j; // [esp 20h] [ebp-38h]

  char v6[20]; // [esp 24h] [ebp-34h] byref

  char v7[20]; // [esp 38h] [ebp-20h] byref

  unsigned int v8; // [esp 4ch] [ebp-ch]

 

  v8 = __readgsdword(0x14u);

  print_msg();

  for ( i = 0; i <= 19; i )

    v7[i] = 0;

  qmemcpy(v7, "ljvnepau", 8);

  printf("enter the password: ");

  _isoc99_scanf("%8s", v6);

  for ( j = 0; j <= 7; j )

    v6[j] = complex_function(v6[j], j);

  if ( !strcmp(v6, v7) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

程序本身也并不复杂,和上一题的主要区别在于,这次使用了静态编译去生成二进制文件。

 

本身 angr 是在库函数装载时钩取这些函数的,静态编译的程序没有这个过程,因此道理上就会被主动分析,这就会带来很大的消耗了,因此本题需要钩取那些静态编译生成的库函数。

 

其实差异不大,在上一篇文章中提到过,angr 内置了多个库函数,既然现在它无法自动钩取,由我们手动去做这件事就行了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import angr

project=angr.project("./13_angr_static_binary",auto_load_libs=false)

state=project.factory.entry_state()

 

project.hook(0x0804ed40,angr.sim_procedures['libc']['printf']())

project.hook(0x0804ed80,angr.sim_procedures['libc']['scanf']())

project.hook(0x0804f350,angr.sim_procedures['libc']['puts']())

project.hook(0x08048d10,angr.sim_procedures['glibc']['__libc_start_main']())

project.hook(0x0805b450,angr.sim_procedures['libc']['strcmp']())

 

sim=project.factory.simgr(state,veritesting=true)

sim.explore(find=0x080489e1)

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))#lyzgmmmv

14_angr_shared_library

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

bool __cdecl validate(int a1, int a2)

{

  _byte *v3; // esi

  char v4[20]; // [esp 4h] [ebp-24h] byref

  int j; // [esp 18h] [ebp-10h]

  int i; // [esp 1ch] [ebp-ch]

 

  if ( a2 <= 7 )

    return 0;

  for ( i = 0; i <= 19; i )

    v4[i] = 0;

  qmemcpy(v4, "wlkgljwh", 8);

  for ( j = 0; j <= 7; j )

  {

    v3 = (j a1);

    *v3 = complex_function(*(j a1), j);

  }

  return strcmp(a1, v4) == 0;

}

这道题的特殊情况在于程序加载了额外的动态库并使用其中的函数。由于这个动态库是用户编写的,angr 不能找到替代品去 hook 。而我们其实也不方便直接加载它,因为通过 auto_load_libs 会把其他无关紧要的东西一起加载进来。

 

不过好在,这道题的主要逻辑全都放在了动态库中,这就能简化我们的操作了。

 

我们可以使用 call_state 来完成操作:

1

2

3

import angr

project=angr.project("./lib14_angr_shared_library.so",auto_load_libs=false)

state=project.factory.call_state(0x000006d7 0x400000,arg1,claripy.bvv(8, 32))

  • 参数一:入口点地址
  • 参数二:该函数对应的参数 1
  • 参数三:该函数对应的参数 2
  • ……

另外,我们将该函数的加载基址设到了 0x400000

 

然后就是对参数的内容进行符号化:

1

2

pwd = claripy.bvs('pwd', 8*8)

state.memory.store(arg1, pwd)

最后就是求解方程了:

1

2

3

4

5

6

7

sim=project.factory.simgr(state)

 

sim.explore(find=0x783 0x400000)

if sim.found:

    res=sim.found[0]

    res.add_constraints(res.regs.eax!=0)

    print(res.solver.eval(pwd))#6293577405752494919

不过因为校验返回值的内容并不在库文件,所以我们需要手动通过 add_constraints 来为状态添加约束。

 

当然,用 res.solver.add 也是可以的:

1

2

3

4

5

sim.explore(find=0x783 0x400000)

if sim.found:

    res=sim.found[0]

    res.solver.add(res.regs.eax!=0)

    print(res.solver.eval(pwd))#6293577405752494919

不过需要区别的是,add_constraints 的约束是对状态所做的,而 res.solver.add 是对约束器做的。在本题中两个方法都行,但不能混用。

15_angr_arbitrary_read

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

int __cdecl main(int argc, const char **argv, const char **envp)

{

  char v4; // [esp ch] [ebp-1ch] byref

  char *v5; // [esp 1ch] [ebp-ch]

 

  v5 = try_again;

  print_msg();

  printf("enter the password: ");

  __isoc99_scanf("%u s", &key, &v4);

  if ( key == 19511649 )

    puts(v5);

  else

    puts(try_again);

  return 0;

}

这次的题目就比较特殊了,它要求我们用 angr 自动求解一个 payload,使得最终会溢出到变量 v5 来修改 puts 的参数。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

import angr

project=angr.project("./15_angr_arbitrary_read",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

 

def check_puts(state):

    puts_parameter = state.memory.load(state.regs.esp 4, 4, endness=project.arch.memory_endness)

    is_vulnerable_expression = puts_parameter == 0x594e4257

    if is_vulnerable_expression!=0:

        return true

    else:

        return false

 

def is_successful(state):

    puts_address = 0x8048370

    if state.addr == puts_address:

        return check_puts(state)

    else:

        return false

 

sim.explore(find=is_successful)

 

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

其实思路很朴素,在函数调用 pust 时检查一下参数,看看它是不是 good job 字符串的地址即可。

 

不得不说,angr 的功能确实强大,连自动化求解 payload 都能做到了。

16_angr_arbitrary_write

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

int __cdecl main(int argc, const char **argv, const char **envp)

{

  char v4[16]; // [esp ch] [ebp-1ch] byref

  void *v5; // [esp 1ch] [ebp-ch]

 

  v5 = &unimportant_buffer;

  memset(v4, 0, sizeof(v4));

  strncpy(&password_buffer, "password", 12);

  print_msg();

  printf("enter the password: ");

  __isoc99_scanf("%u s", &key, v4);

  if ( key == 24173502 )

    strncpy(v5, v4, 16);

  else

    strncpy(&unimportant_buffer, v4, 16);

  if ( !strncmp(&password_buffer, "dvtbogzl", 8) )

    puts("good job.");

  else

    puts("try again.");

  return 0;

}

目的是显然的,通过 __isoc99_scanf 来溢出,让 v5 指向 password_buffer

 

笔者本来是打算直接直接将结果卡在 strncpy

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import angr

import claripy

project=angr.project("./16_angr_arbitrary_write",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

 

def is_successful(state):

    if state.addr== 0x08048611:

        return true

    else:

        return false

 

sim.explore(find=is_successful)

 

if sim.found:

    print("yes")

    res=sim.found[0]

    print(res.posix.dumps(0))

else:

    print("no")

最后会发现这个写法是有问题的,angr 最终会给出 no 。可以发现 angrfind 的判断取决于每次进入基本块的第一个地址。

 

因为它并不以每一条指令进行判断,而是对每次状态执行一次 step 执行完整个基本块后,再调用 find 的条件进行判断。

不过,如果 find 本身是一个地址的话,却能够正常发现,有点奇怪,这个问题留到以后看源代码吧。

 

最后笔者试着这样去完成:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

import angr

import claripy

project=angr.project("./16_angr_arbitrary_write",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

 

def check_v5(state):

    arg0=state.memory.load(state.regs.ebp-0xc,4,endness=project.arch.memory_endness)

    arg1=state.memory.load(state.regs.ebp-0x1c,4,endness=project.arch.memory_endness)

    now_str=state.memory.load(arg1,8)

    if state.solver.symbolic(arg0) and state.solver.symbolic(now_str):

        does_src_hold_password=arg0==0x4655544c

        does_dest_equal_buffer_address=now_str[-1:-64] == 'dvtbogzl'

        if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)):

            state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address)

            return true

        else:

            return false

    else:

        return false

 

 

def is_successful(state):

    if state.addr== 0x08048604:

        return check_v5(state)

    else:

        return false

 

sim.explore(find=is_successful)

 

if sim.found:

    print("yes")

    res=sim.found[0]

    print(res.posix.dumps(0))

else:

    print("no")

它能帮我算出 keyv4 的最后四字节,但是中间的前几位却一直算不出结果。如果您知道为什么还请告诉我。

1

b'0024173502 \xf0\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ltuf'

最后还是修改了钩子钩取的地址来完成本题:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

import angr

import claripy

project=angr.project("./16_angr_arbitrary_write",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state)

 

def check_v5(state):

    arg0=state.memory.load(state.regs.esp 4,4,endness=project.arch.memory_endness)

    arg1=state.memory.load(state.regs.esp 8,4,endness=project.arch.memory_endness)

    now_str=state.memory.load(arg1,8)

    if state.solver.symbolic(arg0) and state.solver.symbolic(now_str):

        does_src_hold_password=arg0==0x4655544c

        does_dest_equal_buffer_address=now_str[-1:-64] == 'dvtbogzl'

        if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)):

            state.add_constraints(does_src_hold_password,does_dest_equal_buffer_address)

            return true

        else:

            return false

    else:

        return false

 

 

def is_successful(state):

    if state.addr== 0x8048410:

        return check_v5(state)

    else:

        return false

 

sim.explore(find=is_successful)

 

if sim.found:

    res=sim.found[0]

    print(res.posix.dumps(0))

    #b'0024173502 dvtbogzl\x00\x00\x00\x00\x00\x00\x00\x00ltuf'

else:

    print("no")

可以看见,如果我将判断的地址添加在 0x8048410 处,也就是 strncpy 的 plt 表上,就能够顺利解决这个问题了。

 

有些迷惑。

17_angr_arbitrary_jump

1

2

3

4

5

6

7

8

int __cdecl main(int argc, const char **argv, const char **envp)

{

  print_msg();

  printf("enter the password: ");

  read_input();

  puts("try again.");

  return 0;

}

1

2

3

4

5

6

int read_input()

{

  char v1[30]; // [esp 1ah] [ebp-1eh] byref

 

  return __isoc99_scanf(&unk_4d4c4860, v1);

}

unk_4d4c4860 处为 %s

 

显然就是一个栈溢出了,但是这次需有让 angr 自动去覆盖返回地址到 print_good 函数:

1

2

3

4

5

6

int print_good()

{

  puts("good job.");

  exit(0);

  return read_input();

}

同样还是最开始那几个,但是注意,本题需要额外添加一个参数:

1

2

3

4

import angr

project=angr.project("./17_angr_arbitrary_jump",auto_load_libs=false)

state=project.factory.entry_state()

sim=project.factory.simgr(state,save_unconstrained=true)

save_unconstrained=true 会让 angr 保存那些不受约束的状态。其实默认情况下的状态就是未约束的。将这些路径保存下来以后,进行遍历:

1

2

3

4

5

6

7

8

9

d=sim.explore()

 

for state in d.unconstrained:

    typ=project.arch.memory_endness

    next_stack=state.memory.load(state.regs.esp,4,endness=typ)

    state.add_constraints(next_stack==0x4d4c4749)

    state.add_constraints(state.regs.eip==0x4d4c4785)

    print(state.posix.dumps(0))

#b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x85glmiglm\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00

为状态添加约束,去寻找同时满足 next_stack==0x4d4c4749state.regs.eip==0x4d4c4785 的状态,然后给出该状态对应的 stdin

其实做完这么多题目,尽管感叹 angr 确实厉害的同时,也不得不承认它仍然有很多的问题,也并没有想象中那么完美。或许要让它走向更加实用的方向还需要一定的积累吧。

最后于 2023-4-14 18:04 被tokameine编辑 ,原因:

原文链接:https://bbs.kanxue.com/thread-276860.htm

网络摘文,本文作者:15h,如若转载,请注明出处:https://www.15cov.cn/2023/08/27/原创-angr-使用技巧速通笔记二/

发表评论

邮箱地址不会被公开。 必填项已用*标注

网站地图