0x00 Environment Setup
mac下的虚拟机:Virtual Box
镜像:ubuntu-18.04.4-desktop-amd64.iso
安装过程略
写(膜改)了一个命令行脚本vbox_ubuntu.sh
:
|
|
然后在~/.zshrc
的末尾加上
|
|
source ~/.zshrc
使其生效
-
ubuntu
: 以非图形化界面启动ubuntu虚拟机 -
ubuntu off
:关闭ubuntu虚拟机 -
ubuntu pause
:暂停(pause)ubuntu虚拟机 -
ubuntu resume
:从paused状态恢复到running -
ubuntu status
:查看ubuntu虚拟机当前的状态(running, powered off, paused) -
ubuntu ip
:查看ubuntu的ip -
ubuntu ssh
:ssh连入ubuntu虚拟机哦,关于ssh这边,可能需要去找到ubuntu虚拟机的ip才能连进去,每次都挺麻烦的。
所以我干脆就直接把ubuntu虚拟机的22端口(用于ssh服务)直接转发到了host machine的2222端口上:
ubuntu → Settings → Network → Port Forwading
然后就只需要
ssh -p 2222 soreatu@127.0.0.1
即可连入ubuntu虚拟机
python肯定是用python3的,网上随便搜搜教程,装了一波python3.8
接下来就是装pwntools:https://docs.pwntools.com/en/stable/install.html
|
|
gdb的gef:https://gef.readthedocs.io/en/master/
环境应该差不多了
Binary Exploitation / Memory Corruption by LiveOverflow
先从liveoverlow的二进制专辑学起
专辑链接:https://www.youtube.com/playlist?list=PLhixgUqwRTjxglIswKp9mpkfPNfHkzyeN
0x01 Introduction to Linux Operating System
在vmware里安装ubuntu
介绍了一些linux命令:
0x02 Writing a simple program in C
Vim
- : syntax on
- : set number
0x03 Writing a simple Program in Python
Vim -O matrix.c matrix.py 同时在vim中打开两个文件
光标位置:ctrl + w + ⬅️/➡️
O/shift + O:在这一行下面/上面 添加一行
在Vim中执行shell命令
-
: !ls
-
: !python
shebang: #!/usr/bin/python3
0x04 How a CPU works and Introduction to Assembler
0x05 Reversing and Cracking first simple program
一个简单的crackme,手动画了一下control graph
0x06 Simple Tools and Techniques for Reversing a binary
各种可以用来分析elf文件的工具
- file
- strings
- hexdump -C
- objdump -d -x
- strace
- ltrace
- Hopper (mac)
- radara2
And remember that no tool is better than the other, it makes sense to master them all. Except radara, some say radara is the best, but nobody ever masters radara.
0x07 Uncrackable Programs? Key validation with Algorithm and creating a Keygen - Part 1/2
liveoverflow秀了一波他熟练的radara2以及他nb的反汇编分析能力
0x08 Uncrackable Program? Finding a Parser Differential in loading ELF - Part 2/2
如何使得elf文件能够正常运行,但是gdb、radara2识别不出来?
在本视频中,liveoverflow展示了一个可能的fuzz方法:随机修改elf文件中的1byte
0x09 Syscalls, Kernel vs. User Mode and Linux Kernel Source Code
syscalls, user-mode, kernel-mode
Ring 0, Ring 3
The system call is the fundamental interface between an application and the linux kernel.
System calls are generally not invoked directly, but rather via wrapper functions in glibc.
Often the glibc wrapper function is quite thin, doing little work other than copying arguments to the right registers before invoking the system call.
例如C语言中的printf,就是write这个syscall函数的wrapper
CPU启动后,会做一些初始化操作,然后从Ring 0进入到Ring 3(用户模式)。在Ring 3想要回到Ring 0,只能通过syscall,但是syscall只会执行一段已经在kernel中预定义好的代码片段,并不能让我们随意操作。syscall结束后,又会回到Ring 3
然后就是a random dive into the kernel code,基本跟不上liveoverflow的节奏
0x0A The deal with numbers: hexadecimal, binary and decimals
1个hex character可以用来表示4bits,那么1byte=8bits就可以很清晰地用2个hex character来表示。
0x0B Smashing the Stack for Fun and Profit - setuid, ssh and exploit.education
-
http://www.phrack.org/ 二进制安全相关的paper
-
http://www.phrack.org/issues/49/14.html#article 1996年(可能是最早的?)一篇介绍buffer overflow的paper
-
-
exploit-exercises 二进制入门习题?
然后讲了一下setuid,某些命令(程序)会以root身份去运行(rwsr-xr-x),例如sudo、ping,我们可以尝试利用这些程序中的某些漏洞来以root身份执行命令
在权限rwsr-xr-x中,s表示设定了SUID位,unprivileged user能够以privilege user (例如root) 的身份来执行这个程序。
SUID (Set User ID) is a type of permission which is given to a file and allows users to execute the file with the permissions of its owner. There are plenty of reasons why a Linux binary can have this type of permission set. For example the ping utility require root privileges in order to open a network socket but it needs to be executed by standard users as well to verify connectivity with other hosts.
escalate privileges:提权
扩展学习:
- https://pentestlab.blog/2017/09/25/suid-executables/
- https://micrictor.github.io/Exploiting-Setuid-Programs/
Paper Study
2021.06.08更新:上周日去听了深信服来我校做的一个技术沙龙分享会,其中有一位主讲人非常流畅地讲了一遍“二进制漏洞攻防演变史”,感觉可以顺着这条发展线去找paper研究一番~
Smashing The Stack For Fun And Profit (1996)
Introduction
This paper attempts to explain what buffer overflows are, and how their exploits work.
Basic knowledge of assembly is required. An understanding of virtual memory concepts, and experience with gdb are very helpful but not necessary. We also assume we are working with an Intel x86 CPU, and that the operating system is Linux.
buffer
: a contiguous block of computer memory that holds multiple instances of the same data type.
Process Memory Organization
先来了解一下一个process在memory中的构造。
Process由三个部分(regions)组成:
-
Text
:program code (instructions) + read-only data(感觉是字面量?) r– -
Data
:initialized and uninitialized data (static variables) 全局变量 + heap?大小会随着
brk(2)
system call 而变化 -
Stack
What Is A Stack?
一个先进后出 (LIFO) 的抽象数据结构 (abstract data type)
PUSH
: adds an element at the top of the stackPOP
: reduces the stack size by one by removing the last element at the top of the stack.
Why Do We Use A Stack?
高级语言中会有function (procedure)。函数调用会跳转 (call xxxx) 到一片代码区域,执行完了后又会跳转 (ret) 回来继续执行下一条指令,这就需要stack来帮助实现。
stack同时也可以用来
- 存储函数中的局部变量 (local variables)
- 函数传参
- 放置函数的返回值
The Stack Region
A stack is a contiguous block of memory containing data.
有一个专门的(e)sp
寄存器用来指向栈顶 (the top of the stack),栈底在一块固定的地址上。栈空间会在run time动态变化。
这里先假定为32位
stack frame
(栈帧):在函数调用 (call) 时开辟,在函数返回 (ret) 时销毁。
一个frame包括了这个函数的:
-
参数
-
局部变量(临时变量、本地变量)
-
一些可以用来恢复上一级frame的数据 (
push ebp
,即上一级frame的ebp
) -
返回地址(例如下面指令中的
0x565555b7
)1 2
0x565555b2 call xxx → 0x565555b7 add eax, 1 ; not exactly this instruction
栈是往低地址生长的(当然也有一些implementaion是往高地址长的)
esp
指向的是 last address on the stack(也有一些是指向下一个空闲的、还未使用的地址)
除了这个指向栈顶的SP
,其实还有frame pointer FP/LB
(Intel CPU中的ebp
),用于指向frame中某一个固定的位置(一般来说ebp
指向返回地址的上一层)。
因为栈顶指针esp
是会动态变化的,所以想以esp
为基地址,来reference到frame里的一个局部变量或者函数参数,是比较麻烦的(偏移量会变);但是如果我们用ebp
来作为基地址的话,偏移量就是固定的了,这就是为什么需要有ebp
的原因吧。此时,函数参数的偏移量就是正的,而局部变量的偏移量就是负的。
在procedure被called了之后,首先要做的就是把上一级frame的ebp
给保存下来(这样函数返回的时候就可以完整地恢复上一级frame);然后把当前esp
所指的地址赋值给ebp
,从而得到当前frame的基地址;再减少esp
来为当前frame的局部变量预留存储空间。这些准备环节被叫做procedure prolog。
当proceduce exit时,即函数返回时,需要对当前frame进行一些清理工作。首先将esp
指向返回地址 (ebp + 0x4
),然后再将ebp
指向上一级frame的基地址 (ebp = *ebp
),最后ret
再根据此时栈顶的返回地址 (*esp
) 来跳转回去。这些收尾环节叫做procedure epilog。
Intel CPU有ENTER
和LEAVE
这两个指令来干这两件事。
接下来,我们通过一个简单的例子来看看stack到底长什么样子。
|
|
用gcc对其进行编译:gcc -g -m32 -fno-stack-protector -o example1 example1.c
-g
表示保留程序的调试信息;
-m32
表示将其编译成32bit的程序(不然在我的虚拟机上就默认是64bit的了)
-fno-stack-protector
表示关闭栈保护(canary),不然反汇编后会有一些额外的与canary相关的指令
然后用gdb对其main
函数进行反汇编:
|
|
- 首先会将3个函数参数 (3, 2, 1) 压入栈中(第一个参数最后被压进)
- 然后再去
call function
,call
指令会将当前eip
所指向下一条指令的地址(add esp, 0x10
这条指令的地址0x56555518
)压入栈中(我们将其称为the return addressRET
),并跳转至function
函数的指令片段
然后再来反汇编一下function
函数:
|
|
其中
call 0x... <__x86.get_pc_thunk.ax>; add eax, 0x...
这两条指令是与position-independent相关的指令,先不用管它。
在function
的开头可以看到
|
|
这三条指令就是用来开辟一个新的frame (procedure prologue)。在函数function
中分别定义了5bytes、10bytes的局部变量buffer1
, buffer2
,所以sub esp, 0x10
为此开辟了一段0x10bytes的空间。
应该还会有一些对齐 (alignment) 之类的,可能编译器有一些优化吧,所以不像paper里那样是
subl $20, %esp
在function
的结尾可以看到
|
|
这两条指令就是用来销毁这个frame (procedure epilogue)。leave
将esp
指向返回地址 (ebp + 0x4
)、并将ebp
指向上一级frame的栈基地址 (*ebp
);ret
将取出 (pop
) 此时栈顶的值 (*esp
) 作为返回地址,并调转至该返回地址处。
动态调试一下,来看看栈到底是怎么变化的:
首先是push 0x3
传入的第三个参数:
然后push 0x2; push 0x1
再传入第二个参数、第一个参数:
![Screen Shot 2020-09-09 at 11.40.46 AM](/Users/Soreat_u/Library/Application Support/typora-user-images/Screen Shot 2020-09-09 at 11.40.46 AM.png)
然后call 0x... <function>
进行调用:
ok,此时已经进入到了function
的领地了,接着push ebp
:
再mov ebp, esp
,确定下当前frame的基地址:
然后sub esp, 0x10
,为当前frame的局部变量开辟空间:
这样,function
函数的frame就已经完成了初始化,接下来就是funtion
函数里的各种操作。
function
函数返回时,先leave
:
再ret
返回到调用者的frame:
哦,对了,还需要把这3个参数给清理掉,所以call 0x... <function>
的下一条指令就是add esp, 0xc
:
![Screen Shot 2020-09-09 at 3.00.53 PM](/Users/Soreat_u/Library/Application Support/typora-user-images/Screen Shot 2020-09-09 at 3.00.53 PM.png)
ok,完全恢复到最初的状态
在调用function
时的栈布局:
Buffer Overflows
A buffer overflow is the result of stuffing more data into a buffer than it can handle.
例如下面这个实例代码:
|
|
当我们call function时,其栈布局如下:
function
函数中的局部变量buffer
只有16bytes,而传入的str
却能有256bytes,strcpy
语句将str
中的所有东西都全部拷贝至buffer
中 (strcpy
是空字符\x00
截断,不会有边界检查bound check),显然可能会出现溢出,而溢出的那些东西就会把栈下面的东西给覆盖掉。
So a buffer overflow allows us to change the return address of a function. In this way we can change the flow of execution of the program.
利用缓冲区溢出漏洞可以控制程序的执行流
ok,我们来修改一下最开始的那个example1:
|
|
该代码将buffer1后12bytes处的那32-bit (1-word) 增加了8,即返回地址+8,将会导致x = 1;
这条语句跳过,直接去执行printf("%d\n", x);
,这样变量x
打印出来就是0,而非1!
草,本地编译出来的不太对劲,没对齐,栈上还有一些其他的东西。。不本地测试了,就单纯地看paper了。
Shell Code
ok,既然我们已经可以通过buffer overflow来控制程序的流程了,那么我们应该去执行一些杀伤力比较大的指令。我们可以来起一个shell,有了shell,想干嘛就干嘛。
可以在栈上写一段shellcode,然后将ret地址改为shellcode的开头。
想要起一个shell,也就是执行:
|
|
将其编译,即可得到它的汇编指令(进而机器码)。
![image-20200911192931394](/Users/Soreat_u/Library/Application Support/typora-user-images/image-20200911192931394.png)
但是,有一个问题:如何确定返回地址?shellcode是在栈上,但是不知道它的具体地址,无法给ret指定到底是跳转到哪里,以及"/bin/sh"
字符串的绝对地址。
后面这个问题可以通过call
和jump
指令的相对跳转来解决。
我们只需要在ret前面布上我们的shellcode,然后ret到前面的位置,即可定位到我们的shellcode;还可以利用call
指令会将其下一条指令的地址压到栈顶,得到"/bin/sh"
字符串的绝对地址。所以我们可以这样布栈:
JJ是
jump
指令,SSSSS是shellcode,CC
是call
指令,sss是字符串。
测试程序:
但是,我们会发现shellcode里面会有一些空字符"\x00"
,buffer overflow通常的情况是strcpy等危险函数,会以"\x00"
作为字符串结尾的判断,所以我们的shellcode中不能有空字符"\x00"
。这也很简单,可以通过xor eax, eax; mov ebx, eax
这种操作来实现。
修改完后的shellcode:
Writing an Exploit (or how to mung the stack)
很多情况,我们都需要确定一下我们shellcode的地址,这样才能让ret精准跳转到我们的shellcode。
在本节中,作者给出了2种方法:
-
根据stack最初的基地址,爆破shellcode在stack上与基地址的偏移
-
在
shellcode
前加上很多nop
指令,ret到nop
中,然后滑到shellcode
Small Buffer Overflow
还有些情况,我们可以溢出的字节数很少,这样我们的shellcode就很受限制。
我们可以通过将shellcode放到这个程序的一个环境变量里,程序加载时,环境变量是会在stack的top处:
我们可以ret到envp处,进而执行shellcode。
Finding Buffer Overflows
总的来说,buffer overflow出现的根本原因在于C语言没有内置的边界检查,例如:
strcat, strcpy, sprintf, vsprintf
等函数无bound check,且以空字符null作为终止字符gets
以换行符\n
作为终止字符scanf
以non-white-space字符作为终止字符
还有某些函数会以是否读到一个换行字符(或者其他的delimiter)的while来获取用户输入,getc, fgetc, getchar
,这些while loop也非常容易用来buffer overflow。
To conclude, grep(1) is your friend.
最后paper的附录给出了当时(1996年)各种架构下的shellcode以及一个Generic Buffer Overflow Program。
StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks (1998)
Here is Notes .
内功修炼
COMPILER, ASSEMBLER, LINKER AND LOADER: A BRIEF STORY
源代码=>可执行文件=>运行镜像:编辑edit,编译compile,汇编assemble,链接link,装载load
链接、装载与库
预处理:处理#
指令,删除注释,添加标识
编译:词法分析、语法分析、语义分析、中间语言生成、目标代码生成与优化
汇编:将汇编代码翻译成机器指令assembly
链接:组合目标文件object files,重定位relocation,生成可执行文件executable
装载:将硬盘secondary memory中的可执行文件,加载到内存primary memory中,用于CPU执行
静态链接vs动态链接
section vs segment
内存memory中程序的布局
Runtime linker
-
Load-time dynamic linking:加载到内存后,一次性全部解析完所有的external symbols
-
Run-time dynamic linking:加载到内存后,先不解析,等遇到了再解析这一个
got && plt
main函数前后发生了什么?
Linux X86 程序启动 – main函数是如何被执行?
C语言运行库,C语言标准库
系统调用
系统调用:用户层面与内核层面的界限
Windows系统完全基于DLL机制
系统调用有明确的定义、向后兼容性
Linux: int 0x80 / sysenter
Windows: Windows API
运行库兼容Windows、Linux等操作系统
系统调用的方式不一样,运行库提供统一的接口,针对不同的操作系统分别实现(封装系统调用)接口函数
helloworld
helloworld程序,如何从源代码一直到屏幕上显示?
编辑源代码helloworld.c
|
|
编译,链接生成可执行文件:gcc helloworld.c -o helloworld
执行可执行文件:./helloworld
命令gcc helloworld.c -o helloworld
在shell中得到执行,shell进程会fork出一个新进程gcc,新进程完成编译工作
gcc是一个工具集,包括了预处理器、编译器、汇编器和链接器等等
gcc具体工作(编译原理):对源代码进行 预处理,编译生成目标程序(词法分析、语法分析、语义分析及中间代码生成、代码优化、目标代码生成),然后跟用到的一些库文件进行链接,最终得到可执行文件helloworld
,并保存在磁盘中
命令./helloworld
会 从创建一个新进程,操作系统为其分配一个独立的虚拟地址空间,读取文件头,建立虚拟空间与可执行文件的映射关系,将eip指向程序的入口点(_start或动态链接器),CPU一行一行执行命令。此时,内存中还没东西,引发缺页中断,从磁盘中按页调取数据到内存。