Bugku-Pwn4

0x00前言

  • 新接触Pwn不久,以这道题为例,记录一下自己的学习经历。

  • 这道题在网上能搜到WP,比着WP能很容易做出来,但是对没啥pwn基础的我来说想要弄懂却不容易。

  • 写这篇文章希望能帮助到想搞懂这道题目但是却不能被现有的wp满足的pwner们。

0x01理论分析

  • 检查了一番,没开启啥保护。

  • 根据main函数能看出来是考察的栈溢出, memsets只开辟了0x10大小的空间,但是read却读入了0x30个字节, 会造成溢出。

  • 这是一张在别的地方拿来的图(32位), 0x28个字节的栈空间下面是EBP返回地址

  • 64位程序类似, 只不过EBP变成RBP 然后它们的大小都变成了0x8个字节,那些Hello能写到一行,World能写到一行了。

  • 按照一般思路, 需要覆盖掉EBP, 然后 再将返回地址覆盖成我们想让程序跳转到的地址,一般是调到能拿到shell的地方比如System("/bin/sh")

  • 那么搜索关键字符串/bin/sh

  • 并没有找到,但是却有$0
  • 在Linux中:
1
2
3
4
5
$# 是传给脚本的参数个数 
$0 是脚本本身的名字
$1是传递给该shell脚本的第一个参数
$2是传递给该shell脚本的第二个参数
$@ 是传给脚本的所有参数的列表

$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位的函数调用使用寄存器传参,分别用rdirsirdxrcxr8r9来传递参数(参数个数小于7的时候)。

  • 我们的参数只有一个,所以是用rdi来传参的。

小难点

  • 以下内容可能会有些迷,别急,后续会有详细的调试以及讲解。

  • 我们需要将参数pop到rdi中,因此我们需要调用pop rdi;ret所以我们去找这条命令的地址。

  • 关于为什么是pop rdi;ret 而不只是pop rdi,后续也会有讲解。

  • 可以利用ROPgadget工具进行查找。

  • 这样我们得到了pop rdi;ret的地址0x00000000004007d3

0x02拿Flag

  • 先给出脚本,运行脚本前需要安装pwntools:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#coding:utf-8
from pwn import *

c = remote('114.116.54.89', 10004)

pop_rdi_ret = 0x00000000004007d3
bin_sh = 0x000000000060111f
system = 0x000000000040075A

payload = ''
payload += 'A'*0x10 #这0x10个是用来覆盖那0x10字节的栈空间的。
payload += 'A'*0x8 #这0x8是覆盖RBP的,因为是64位程序,RBP大小为0x8
payload += p64(pop_rdi_ret) #64位程序用p64,这三行为什么是这个顺序,待会会有讲解。
payload += p64(bin_sh)
payload += p64(system)

c.recvuntil('Come on,try to pwn me')

c.sendline(payload)

c.interactive()
  • 运行即可拿到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点

  • 0x000000000040075Asystem函数的地址

  • 单步来到这,是一个leave指令,这个指令等价于mov rsp,rbppop rbp

  • 执行前,栈和寄存器的状态是这样的,注意观察RSP,RBP

  • 栈窗口中深色的那一行即为RSP目前指向的位置。

  • 执行后是这样的。

  • 这说明了上述第2点和第3点。

补充

  • 我在分析的时候产生了这样的疑惑。

  • leave相当于mov rsp,rbppop 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,如果有什么地方有不足之处,希望大佬们能帮我指出。