pwn基础学习

pwn的介绍

入侵到别人的机器里。

一、基础介绍

1.进程中的栈帧

在计算机中,栈是一块在内存中动态开辟的存储空间,用于为程序执行函数提供环境(保存有局部变量,函数的参数,ebp,以及函数返回地址)。

栈通常保存着传递给该函数的参数,返回地址,返回时的ebp,以及函数的局部变量(它们也是按该序入栈的)。

进程运行过程中,用户栈的布局如下图所示:

2.gdb的基础命令

可以简写命令,如run -> r  。

1.start :开始调试,启动程序

2.disass <函数名>  :查看函数反汇编后的代码

3.run:运行程序

4.b <函数名> / *0x地址  :下断点,注意地址前要加‘ *5.delete  <断点ID>:删除断点,不指定就删除全部

6.x  <地址>:查看该地址的内容(默认四个字节)

7.n :执行下一条指令

 还有:

info r :查看寄存器信息

info b:查看断点信息

set *(0x地址) =内容:修改内容

c:继续执行完当前函数

finish:执行完当前函数返回到调用它的函数处,或运行到函数返回。

u:使用时光标必须停在循环头部,作用是执行到循环结束。

j n:跳转到第n行指令执行,也就是跳过前面的指令不执行,如果跳转后无断点将会继续执行不停止(在一个函数范围内)。

return:强制返回当前函数

cyclic  n:生成n个字符,通过向scanf等存在溢出漏洞的函数写入大量的字符引起异常报错。当返回地址被错误覆盖会产生错误,这时可以通过报错信息知道返回地址在堆栈中的位置,从而实现快速定位要覆盖的位置。

cyclic -l 0x地址:根据上一条命令,获得输入的位置距离0x地址的间隔字符数。

注:gdb中‘@’操作符可以提供查看连续内存空间,使用方法是:首地址‘@’查看长度(字为单位)。

 

 

3.保护机制

ASLR地址随机化

ASLR (内存地址随机化,又称PIE保护):用于将程序基址进行随机化,让程序加载进来的基地址都随机发生改变。

通过该文件/proc/sys/kernel/randomize_va_space来设定开启地址随机化。

0:关闭。

1:表示保留的随机化,共享库、栈、mmapO以及VDSO随机化。

2:表示完全的随机化 在1的基础上,通过brk()分配的内存空间也将被随机化。

 

PIC(Position Independent Code,位置无关代码)

用于将导入库的基址进行随机化,让它们每次被程序加载进来的基地址都发生改变。

gcc -fPIE -o test test.c   //启用PIE

gcc -fPIC -o test test.c   //启用PIC

 

误区:PIC != PIE

PIE 负责代码段以及数据段的随机化工作。

PIC能使程序像共享库一样在主存任何位置装载。

Canary (栈保护) 

一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致。

gcc -fstack-protector -o test test.c   //启用canary栈保护

gcc -fno-stack-protector -o test test.c   //禁用canary栈保护

gcc -fstack-protector -o test test.c        //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码

gcc -fstack-protector-all -o test test.c   //启用堆栈保护,为所有函数插入保护代码

 

PS:gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all编译参数以支持栈保护功能,4.9新增了-fstack-protector-strong编译参数让保护的范围更广。

NX  (又称 DEP )

内存中数据页不可执行,栈数据不可执行。总之,可写的不可执行,可执行的不可写。

RELRO

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT表的攻击。

 

4.关于堆栈内存对齐

and esp,0XFFFFFFF0:编译规则规定 “ 程序访问的地址必须向16字节对齐(被16整除)”,因为内存对齐之后可以提高访问效率。在32位的机器上,CPU一次可以读取4个字节的数据,如果数据不按内存对齐,可能会出现本可以一次完成读取的数据要两次才能完成。

5.可执行文件PE或ELF中的一些段

.bss段:通常存放未初始化的全局变量,属于静态分配的存储空间。

.data段:通常存放已初始化的全局变量,属于静态分配的存储空间。

.text段:代码段,存放指令

.rdata段:资源数据段,存放字符串和#define定义的常量,有时会保存一些IAT数据。

6.动态链接

程序中使用一些动态链接库里的函数,是在需要执行的时候再加载进内存(且只能加载进入数据段),进行地址解析(逻辑地址-->物理地址),这里就需要依托plt表和got表。

plt,程序联动表(内部函数表):汇编后的调用指令  call xxx@plt,而plt表中存放1.jmp [xxx_got]、2.push 偏移、3.jmp plt[0]

got,全局偏移表(全局函数表):存放函数的真实地址,但初次加载后才是,未加载前。(注:got表项前三项不存放地址)

首次调用库函数时,[xxx_got]里存的是上述指令2的地址,plt[0]里存放加载函数并解析地址的方法。函数加载进来后,[xxx_got]里才存真实的函数地址。

got[2]存动态链接器的入口地址,got[1]存参数,plt[0]处指令会将获得的真实的地址绑定到got[偏移x]中。

由此,我们可以知道got表是一个可写对象,同时也是一个可攻击对象。通过修改got表的内容,我们可以实现任意函数调用。

 

 

 

二、PWN脚本(py)

一个最简单的脚本样例如下:

from pwn import *

p=process("****")

p.sendline("********")

p.interactive()

 

注:system("/bin/bash"),打开一个shell。

PwnTool库

p = process("./bin/text.out")   //启动一个进序并获得它的句柄

IO模块

p.send(data)                        //向进程p发送数据data

p.sendline(data)                 //向进程p发送一行数据data,自动换行

p.recv(number=4096, timeout=default)       //给出接收字节数number,timeout指定超时时间,相当于输出打印进程的输出信息

p.interactive()                                        //获得进程p的运行环境,即与p的shell交互

ELF模块

用于获取ELF文件的信息

e = ELF('/bin/text.out')           //获取文件句柄

e.address文件装载的基地址

e.symbols[ '函数A' ]   函数A地址

e.got[ '函数A' ]   函数A的GOT表的地址

e.plt[ '函数A' ]   函数A的PLT表的地址

proc模块

proc.pidof(句柄):获得程序pid

pause():暂停,输入回车继续。

PS:使用时最好加上hex()。

u将数字转化成字符串,或者p反过来:p32(data)、p64(data)是打包,u32(data)、u64(data)是解包。

实践

1.通过符号表获取elf文件的某个函数或全局变量的地址:

f = ELF("./test.o")
addr = f.symbols["变量名/函数名"]  //得到地址(小端形式)

2.recvuntil(" ** "):接收输出的数据直到数据里出现 " ** "为止不再继续接收。

 

 

 

三、peda的使用

peda插件快速定位

pattern create n:生成n个字符串。

pattern offset 0x地址:定位输入到0x地址的地址间隔。功能等同pwntool里的cyclic。

vmmap:查看堆栈空间的读写执行权限。

 

四、objdump的命令

-t   test.out :查看程序中用到的函数。

-d  test.out:查看函数的反汇编结果。

-d  -j .plt  test.out:-j后跟.text、.data、.bss、.const-M intel:指定架构

 

 

五、shellcode编写模板

系统调用(软中断)作为一种接口,通过系统调用,应用程序能够进入操作系统内核,从而使用内核提供的各种资源。

流程:程序中调用 libc 库中的封装好的系统调用函数 func() ,函数func()执行函数内的指令,其中会有int 0x80它使得进程从用户态切换到内核态,操作系统会调用system_call (系统调用处理程序),system_call()就根据传入的系统调用号从系统调用服务程序数组中寻找对应系统调用服务程序。

系统调用号存储在/usr/include/x86_64-linux-gnu/asm/unistd_32.h。(或者64位的unistd_64.h)

Linux32位版本

eax:系统调用号(execve的调用号为11,执行shell)

ebx:第一个参数(调用号里对于函数的参数,如execve的第一个参数)

ecx:第二个参数

edx:第三个参数

esi:第四个参数

edi:第五个参数

int 0x80

目的:想办法调用execve("/bin/sh",null,null)

需要:1.传入字符串“/bin///sh”(三个斜杆,因为按四个字节对齐)   2.系统调用execve  --------->eax = 11,ebx = "/bin/sh"地址,ecx = 0,edx = 0

push 0x68     
push 0x732f2f2f  
push 0x6e69622f  //因为栈是倒过来读的
mov ebx,esp   //将第一个参数("/bin///bash")传进ebx
xor ecx,ecx   //通过清空让ecx=0
xor edx,edx   //通过清空让edx=0
push 11     
pop eax      //eax=11
int 0x80

 

PS:如果是ARM32位平台下,前4个参数传递给了R0-R3寄存器(依然是从实参表右开始写入参数)。

Linux64位

rax:系统调用号(execve的调用号为11,执行shell)

rdi:第一个参数(调用号里对于函数的参数,如execve的第一个参数)

rsi:第二个参数

rdx:第三个参数

rcx:第四个参数

r8:第五个参数

r9:第六个参数

syscall

xor rax,rax
add rax,0x3b
xor rdi,rdi
push rdi        //字符串必须以0结尾
mov rdi,0x68732f2f2f6e69622f  //因为64位的存储器足够存放"/bin/bash"
push rdi 
lea rdi,[rsp]      //第一个参数,字符串的地址
xor rsi,rsi        //第二个参数,0
xor rdx,rdx            //第三个参数,0
syscall

 

PS:win64系统中,开始的4个参数传递至RCX,RDX,R8,R9寄存器,之后到栈中。

六、asm文件编译为elf文件

nasm -f elf32 -o shellcode.o shellcode.asm   //汇编
ld -m elf_i386 -o shellcode shellcode.o   //链接生成elf文件
objcopy -O binary shellcode shell_exe  //将代码段拷贝出来,把elf转成bin文件

这里最后得到的二进制文件shell_exe是一个原始的二进制文件,仅包含执行的指令,而IAT,elf文件头等信息并不包含。只需要将程序加载到其起始地址,就可以执行。

 

posted @ 2020-03-11 19:25  An2i  阅读(456)  评论(0)    收藏  举报