0x00前言
新接触Pwn不久,以这道题为例,记录一下自己的学习经历。
这道题在网上能搜到WP,比着WP能很容易做出来,但是对没啥pwn基础的我来说想要弄懂却不容易。
写这篇文章希望能帮助到想搞懂这道题目但是却不能被现有的wp满足的pwner们。
0x01理论分析
- 检查了一番,没开启啥保护。
- 根据main函数能看出来是考察的栈溢出,
memset
对s
只开辟了0x10
大小的空间,但是read
却读入了0x30
个字节, 会造成溢出。
这是一张在别的地方拿来的图(32位),
0x28
个字节的栈空间下面是EBP
和返回地址
。64位程序类似, 只不过EBP变成RBP 然后它们的大小都变成了0x8个字节,那些Hello能写到一行,World能写到一行了。
按照一般思路, 需要覆盖掉
EBP
, 然后 再将返回地址覆盖成我们想让程序跳转到的地址,一般是调到能拿到shell
的地方比如System("/bin/sh")
那么搜索关键字符串
/bin/sh
- 并没有找到,但是却有
$0
- 在Linux中:
1 | $# 是传给脚本的参数个数 |
$0
在linux中为为shell或shell脚本的名称
system()
会调用fork()
产生子进程,由子进程来调用/bin/sh -c string
来执行参数string
字符串所代表的命令,此命令执行完后随即返回原调用的进程。
所以如果将
$0
作为system
的参数,能达到传入'/bin/sh'
一样的效果。
- 即
system("$0") == system('bin/sh')
$0
在这一句的末端, 直接查偏移就行,即从0000000000601100
开始往右数,看第几个是$
就行。举个例子:
0000000000601100
代表这一整串,0000000000601101
代表从9往后(包括9)这一整串,0000000000601102
代表从8往后(包括8)这一串…..以此类推得到
$0
的地址为0x000000000060111f
得到了一个
/bin/sh
但是还需要能够调用这个/bin/sh
的函数,也就是system()
函数
- 通过在IDA查看import调用, 找到了
system
函数
- 现在需要得到它的地址。
- 把这个Line prefixes(graph) 前面的勾打上就能显示地址了。
这样得到了
system
函数的地址为0x000000000040075A
根据之前的思路, 我们通过覆盖程序的返回地址来让程序跳转到我们想让它跳到的地方。但是,只调到
system
函数是不够的,因为我们还没有把/bin/sh
这个参数给它传进去。(调用函数前需要先传入参数)所以现在需要先把参数传进去。
64位程序和32位程序的传参方式不一样,32位的函数调用使用栈传参,64位的函数调用使用寄存器传参,分别用
rdi
、rsi
、rdx
、rcx
、r8
、r9
来传递参数(参数个数小于7的时候)。
- 我们的参数只有一个,所以是用rdi来传参的。
小难点
以下内容可能会有些迷,别急,后续会有详细的调试以及讲解。
我们需要将参数pop到rdi中,因此我们需要调用
pop rdi;ret
所以我们去找这条命令的地址。关于为什么是
pop rdi;ret
而不只是pop rdi
,后续也会有讲解。可以利用
ROPgadget
工具进行查找。
- 这样我们得到了
pop rdi;ret
的地址0x00000000004007d3
。
0x02拿Flag
- 先给出脚本,运行脚本前需要安装pwntools:
1 | #coding:utf-8 |
- 运行即可拿到flag
0x03调试与分析
- 通过IDA server来调试一下程序。(这个具体怎么操作,百度就行,有很详细的教程)
- 在大佬的指导下写了另一个用来调试的脚本
exp.py
。
运行
linux_server64
同时 运行exp
在IDA选择
Remote Linux debugger
,然后选择Attach to process...
- 然后
Ctrl+F
搜索 pwn4
选中点击确定即可调试。
在
read
后的一句下断点。
也就是图中紫色这一句。 然后F8单步
在栈窗口能看到我们的payload已经传入了。
第1点
- 那两行414141…就是’A’*0x10
第2点
- 那一行616161…就是’a’*0x8,这里是
RBP
所在的地方(现在依然也是RBP所在的地方,只不过内容变了),现在被这一串61覆盖了。
第3点
- 而
0x00000000004007D3
所在位置,曾经是原本程序的返回地址
,现在这个位置依然是返回地址
,不过变成pop rdi;ret
的地址了, 也就是说程序会返回到pop rdi;ret
的地址执行pop rdi;ret
,而不会按程序原本的逻辑来执行了。
第4点
0x000000000060111F
就是$0
即/bin/sh
的地址了。
第5点
0x000000000040075A
即system函数的地址
单步来到这,是一个
leave
指令,这个指令等价于mov rsp,rbp
,pop rbp
执行前,栈和寄存器的状态是这样的,注意观察
RSP
,RBP
。栈窗口中深色的那一行即为RSP目前指向的位置。
执行后是这样的。
- 这说明了上述第2点和第3点。
补充
我在分析的时候产生了这样的疑惑。
leave
相当于mov rsp,rbp
,pop rbp
那
mov rsp,rbp
以后 rsp应该指向61616161...
那一行啊,为什么现在却指向了61616161...
下面的那一行?请教大佬后得知,这是因为
pop rbp
相当于mov rbp,[rsp]
,add rsp,0x8
32位的话就是
add esp,0x4
这样就导致了rsp指向了现在它所指的位置。
接下来就要执行
retn
指令了。retn
,等价于pop rip
rip
存放的是下一条指令的地址。pop rip
就相当于把rsp指向的东西
传入到rip
中,现在rsp
指向的是我们传进去的pop rdi;ret
的地址, 所以这样就能让rip
里存的是pop rdi;ret
的地址,所以执行完retn
后就会执行pop rdi;ret
。F8单步运行,
rip
如我们所愿变成了pop rdi;ret
的地址, 左侧窗口也变成了pop rdi
的位置,rsp
指向了$0
即/bin/sh
所在的地方。
接着单步运行的话, 会执行
pop rdi
指令,就能将rsp
现在所指的/bin/sh
传入到rdi
中,用rdi
来为system函数
进行传参。继续F8单步运行。
如我们所愿。左侧来到了
retn
的位置,现在来讲一下前面留下来的问题:“当时找的为什么是pop rdi;ret
而不只是pop rdi
”假设我们用的
pop rdi
,确实能够将/bin/sh
传入到rdi
中,但是我们就没办法执行system函数
了,因为如果用pop rdi
的话,执行完pop rdi
以后就没有指令能将rip
执行system函数
的地址了。为了更好地说明问题,再观察一下现在的栈及寄存器的情况。
rsp
指向了system函数
的地址,rip
中存的是retn
指令的地址,下一步就会执行retn
,执行完以后rip
里存的就是system函数
的地址了,然后就能执行system函数
了。继续F8单步运行。
- 如我们所愿,
rip
指向了system函数
的地址,rdi
里存的是/bin/sh
这样就能调用system函数
了,就能得到shell了。
0x04写在最后
- 我新接触pwn,还在慢慢爬坑,很用心地写了这篇文章来帮助同样在爬坑的人学习pwn,如果有什么地方有不足之处,希望大佬们能帮我指出。