starrycan的pwn随笔——初识pwn
一.Introducation
0x01 简介CTF
0x02 什么是pwn
”Pwn”是一个黑客语法的俚语词,是指攻破设备或者系统 。发音类似“砰”,对黑客而言这就是成功实施黑客攻击的声音--砰的一声,被“黑”的电脑或手机就被你操纵了。
CTF中的pwn
CTF中的PWN主要是针对于二进制漏洞挖掘与利用,通常情况下选手需要对于一个有漏洞的可执行文件进行分析,找到漏洞,然后利用漏洞读取远程服务器上的FLAG。
0x03 CTF中pwn的出台方向
传统的glibc PWN:堆、栈、shellcode编写、iofile等。
进阶:
arm架构、loT、内核、vm、浏览器等。
注:什么是glibc
glibc是GNU发布的libc库,是Linux系统中最底层的API,其他运行库通常依赖它。它提供系统服务封装及多种功能实现,如字符串处理、动态加载、线程库、加密算法等。glibc的广泛使用使其成为GNU/Linux操作系统的重要组成部分。通过getconf命令可获取版本信息,ldd命令则能显示其在程序中的链接位置。
glibc简而言之就是linux下的可执行环境 glibc是linux下重要的c语言库也关系到系统运行的核心部分
注:什么是shellcode
Shellcode 是指经过精心设计的一串指令,一旦注入正在运行的应用程序中即可运行,常用于栈和基于堆的溢出。术语Shellcode意思指的便是用于启动一个命令Shell的已编写好的可执行代码。
shellcode就基于漏洞利用的基础之上 编写可运行的文件 类似于windows下exe 文件
注:什么shell
Shell 是一个命令解释器,用户可以用shell来启动、挂起、停止甚至是编写一些程序(也是一个编程语言)。
shell其实就是Linux里的一个翻译官,负责把命令翻译成二进制机器语言数字。
内核拥有最高级别、最底层的权限,在接受到shell命令以后可以直接控制硬件。
0x04 生活中的攻击面
office 浏览器 操作系统 硬件驱动 通信协议 路由器
0x05 实际生活中PWN
心脏滴血(cve-2014-0160)——泄露通信数据
脏牛dirty cow(cve-2016-5195)——linux本地提权root
永恒之蓝——wannacry
0x05 教练,我也想学pwn
1.pwn只是一个简单称谓或者叫做泛称,我们学习的主要目的是为了解出ctf里面的题目,给自己一个上升的空间和机会,也算一个小小的跳板吧,如你有志向于网络安全方向,CTF的给你提供一个就业的基础和出名的机会。如果你志向于考研,学习网安可以为你的考研跨方向提供一个选择,并且可以成为带动你学习专业的契机,此外二进制漏洞方向也网络安全技术论文领域的绝对高地,是发表各大刊物的有力契机,说不定下一个发表SCI就是你!
pwn一直是CTF中的热门方向 同时也各大战队所需要和缺少的重要部分 每一个方向的入门和深入都需要一番功夫 江山代有人才出各领风骚数百年 希望大家能在这个方面有所建树
2.glibc的学习路线
想要入门 狠多同学面对繁杂的知识体系 感到无从下手 面对一些的繁杂的知识
感到无从下手 如果从头学习的话 一门门尖酸晦涩的科目 如汇编语言 数据结构 操作系统 计算机组成原理让大家望而却步 于是就有一句话叫 pwn从入门到放弃
学习pwn的难点不在知识的难度 而在于找到一个切入点慢慢深入 逢山开路 遇水架桥 慢慢就会前路复明
理论+实践+实践才能更近一步
3.推荐一些刷题网站(pwn)
https://www.hetianlab.com/合天网安实验室
https://pwnable.kr 适合新手入门pwn
https://adworld.xctf.org.cn/攻防世界
https://www.jarvisoj.com/作者筛选过的质量不错的题目
https://pwnable.tw 质量很好,但是有一定难度
0x06 开学吧!
二.汇编语言基础
0x01 1byte =8bit
1个16进制数占半个字节
0x02 计算机的寻址方式
主流操作系统都是以B(字节)来进行寻址
0x03前言 真正能被运行的是什么?
计算机并不能直接运行高级语言
我们编写的高级语言程序需要进行编译后才能在计算机上运行高级语言经过编译之后,经过编译器处理,被打包成一个可执行文件的格式
那么,计算机真正能够被运行的是什么?
真正能被运行的只有机器码也就是一个个01 为了遍观看 人们把机器码翻译为16进制数字 又把一个个16进制数字翻译为汇编语言
汇编语言就是机器码的助记符
0x04 寄存器
计算机的指令都是由CPU来执行,
在计算机系统结构中,CPU和内存是分开的,
寄存器存在于CPU中,是CPU的直接操作对象。
0x05 寻址方式
立即寻址 1234h 直接访问1234h
直接寻址 [1234h] 内存地址1234h
寄存器寻址 RAX 访问RAX寄存器
寄存器间接寻 [RAX] 访问RAX寄存器存储的值这样内存地址
变址寻址 [RAX+1234H] 访问RAX寄存器的值+1234h这一内存地址
0x06数
1.基础知识
数学是科学的基石,任何科技都离不开数学的支撑。
在计算机中,无论数据是以二进制或者十六进制十进制表示,本质都是代表个数。
尽管数据在计算机内部有很多存储的形式(补码、原码、反码等)。但是本质都是数。
2.计算机不能存储无线大的数,这个数值有一定的上限和下限
3.数字的符号
如果是unsigned 也就是无符号数,数据的每一位都是代表数据
如果是signed有符号数,那么数据的最高位会被当作符号位处理
0代表正数,1代表负数。
0x07数字的溢出
1.什么是溢出
数值有上下限范围,那么就不可避免的会有溢出情况。
以32位int为例,有以下四种溢出
无符号上溢:0xffffffff+1变成0
无符号下溢:0-1变成0xffffffff
有符号上溢:有符号正数0x7fffffff+1变成负数0x80000000
无符号下溢:有符号数0x80000000-1变成正数0x7fffffff
2.溢出的原因
1.存储位数不够
2.符号位溢出
到这里其实我就学习到了pwn方向中的第一个溢出漏洞,我们称之为整数溢出,整数溢出一般配合别的漏洞来使用.
三.linux基础
0x01 学习Linux的主要知识方面
1.linux的保护机制的内核 2.Linux的组成部分 其中包括shellcode的编写 3.linux下基本的操作命令
0x02linux的基础结构和权限
1.linux的保护层级 分为4个层级 ring0-ring3 常用的是两个0和3 0是内核 3是用户
2.权限等级 root admin user 等等等级
用户和用户组 多个用户组合在一起就是用户组 同一个用户组下的用户权限一般都相同
3.权限 一般就是三种 r w x read 读权限 wirte 写权限 x 可执行权限
4.权限操作的一些命令 ls -a 查看所有文件以及权限 例子 -rw-rw-rw- 第一位-或者d代表文件或者目录 后面3位为一组 -rw 为当前文件的权限 -rw是当前用户所属的用户组对文件的权限 最后一组是其他用户组对文件的权限
0x03 物理内存和虚拟内存
1.物理内存就是真正的内存 我们买电脑时候 16G+512G中的16就是内存
2.虚拟内存是物理内存经过MMU转换(页表) 目前无论是windows或linux都是使用的虚拟内存
0x04学会看源码
1.Iinux是开源的,他的代码实现都可以查到,我们研究Iinux的机制,最重要的武器就是源码,分析源码是一个安全研究者必备的技能。
2.https://elixir.bootlin.com https://code.woboq.org
0x05计算机内部存储数据的形式
1.计算机内部有两种数据的存储形式:大端序、小端序
2.大端序:数据高位存储在计算机地址的低位,数据低位存储在地址的高位 (高低低高)
小端序:数据高位存储在计算机地址的高位,数据低位存储在地址(高高低低)
3.linux数据存储的格式是小端序
4.linux是小端序,所以如果我们以字符串的形式输入一个数字时,要注意格式
比如输入0xdeadbeef这个数字
字符串输入就是”\xeflxbelxadlxde”传入给程序
好在有pwntools,p32(0xdeadbeef)即可完成自动转换
0x06 文件描述符号
1.Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/0操作的系统调用都会通过文件描述符。
2.每个文件描述符会与一个打开的文件相对应,不同的文件描述符也可能指向同一个文件
3.相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开
招全长口
4.我们会在open、read、write这些常见函数中见到,
5.0标准输入(stdin)、1标准输出(stdout)、2标准错误(stderr)
6.read(0,buf,size) 从stdin中读size个数据到buf中
7.write(1,buf,size)从buf中取size个数据到stdout中
0x07 基本的数据结构
1.栈 stack 我们可以把栈看成 成一个薯片桶 我们对栈的操作都是在栈的顶端完成
2.栈可以被看做一个阉割版的数组 那我们为社么不用数组呢 一把菜刀可以干很多事情 切菜削皮剁骨 但是我们在削皮时候还是采用削皮刀 原因就是简单方面
3.栈是一种数据结构,他是一种先进后出(LIFO)的数据结构。
4.栈的基本操作有两种:push(压栈)和pop(弹栈)
5.由于函数调用顺序也是LIFO,所以我们能接触到的绝大多数系统,都是通过栈这一数据结构来维护函数调用关系。
6.函数调用顺序也先进后出
0x08函数的调用流程
1.函数的调用流程也和栈一样遵循lifo 系统在运行程序会给每一个函数分配一个栈帧
2.小明蒸饭的例子 小明要去蒸饭(main)然后要去淘米(fun a),结果发现米没有了,要去买米(fun b)
事情的发生结果是 main——fun a ——fun b
实际的干活顺序 funb——funa——main
3.c语言举例
发生顺序 期待输出a——需要定义a——调用main
实际执行顺序 调用main——定义a——输出a
0x09Linux中的栈
1.我们接触到一些算法,很多都是用栈来实现的,比如DFS深度优先搜索算法
2.很多的这种LIFO算法都会以递归的形式实现
3.其实,递归的形式实现这些算法本质上来说也是利用栈结构,只不过他没有在程序中另外申请一个栈,而是用的函数调用栈。
4.Linux中的栈是从高地址向地址生长的 这里我产生了一个疑问 所有的操作系统中的栈都是由高地址向低地址生长的吗?
以下是chatgpt的解答
四.函数的调用流程和调用约定
0x01基础知识
1.函数调用关系通过栈来维护,函数调用关系符合LIFO.
2.操作系统为每个程序都设置了一个栈,程序每个独立的函数都有独立的栈帧。
3.栈地址从高地址向低地址生长。
4.栈有两种基本操作:push和pop
pop指令就是我们所说的出栈指令也叫弹栈指令,将栈顶指针弹出到寄存器,将栈顶数据弹出到寄存器,然后将栈顶指针下移一个单位 pop rax,作用就是 mov rax [rsp], add rsp 8;
push 指令就是我们所说入栈指令也叫压栈指令,将栈顶指针向上移动一个单位的距离,然后将一个寄存器的值放在栈顶,具体来说push rax,sub rsp 8;mov [rsp] rax;
5.函数调用指令:call,返回指令return,jmp跳转指令
a.jmp指令是立即跳转,不涉及函数调用,用于循环,条件句(if else)这种场合 具体来说就是jmp 1234h,效果就是mov rip 1234h
b.call指令就是函数调用,需要返回地址 具体来说就是call 1234h,效果就是push rip;mov rip 1234h;
6.栈中保存函数返回地址、栈帧、局部变量等信息
0x02真正的函数调用流程
接下来我将通过自己的学习以图画的形式帮助理解真正的函数调用流程
1.函数的调用顺序是main——>fun_b——>fun_a
在这里我手写了一些过程进行分享
a.左上的话就是main函数没被调用之前的一个初始状态
b.右上的话就是开始调用main函数之后的一个状态 mian函数中嵌套有fun_b函数 其实在这期间执行了3步
push rsp 保存栈顶地址
mov rbp rsp 让栈底指针指向栈顶
sub rbp xx 栈顶部指针向下移动数个位置 开辟新的空间
经过以上三部的操作就到了右上方的图示
c.左下方就是继续运行程序 调用fun_a 函数就和前一步差不多了 这里不再多写
2.leave指令和retun
在一个函数执行结束返回时,会执行leave;ret;
实际效果就是:movrsp rbp;pop rbp;pop eip:
eip寄存器用于存储当前正在执行的指令的地址
func_a执行完毕返回后,栈布局如图
可以与之前的func b未调用func a前的栈帧对比
一模一样,说明已经恢复了栈帧。
唯一不同之处在于此时程序的rip已经指向了c=1
后面一条指令,说明func_a已经执行完毕。
0x03函数调用小结
1.调用函数:只需要将rip压栈,即push rip,然后讲rip赋值为被调用函数的起始地址,这一操作被隐性的内置在call指令中。
2.被调用函数:push rbp;mov rbprsp;sub rsp 0xxxx。即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟栈空间给自己用,此时rbp就变成了被调用函数的栈底。
3.函数返回:leave;ret;翻译过来就是:movrsprbp;poprbp;poprip;即恢复栈帧,返回调用函数的返回地址。
0x04调用约定
1.返回值:一般来说,一个函数的返回值会存储到RAX寄存器
2.X86-64函数的调用约定为:
3.从左至右参数一次传递给rdi,rsi,rdx,rcx,r8,r9。
4.如果一个函数的参数多于6个,则从右至左压入栈中传递
5.syscall指令
用于调用系统函数,请调用时需要指明系统调用号
系统调用号存在rax寄存器中,然后布置好参数,执行syscal即可
注:在高级语言我们调用函数或者自己定义函数直接写函数名称即可但是在操作系统的内部我们都要调用syscall函数
6.常见的系统调用号
0x05函数调用流程小结
1.调用函数:只需要将rip压栈,即pushrip,然后讲rip赋值为被调用函数的起始地址,这一操作被隐性的内置在call指令中。
2.被调用函数:push rbp;mov rbprsp;sub rsp 0xxxx。即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟空间给自己用,此时rbp就变成了被调用函数的栈底。
3.函数返回:leave;ret;翻译过来就是:movrsprbp;poprbp;poprip:即恢复栈帧,返回调用函数的返回地址,