CTF竞赛权威指南读书笔记
第4章,Linux安全机制
Linux基础
常用命令
shell是一个用户与Linux进行交互的接口程序。bash是当前Linux标准的默认shell,我们也可以选用其它的shell脚本语言,如zsh,fish等。
命令使用格式:
命令名称 [命令参数] [命令对象]
其中,命令参数有长和短两种模式,分别用"–“和”-"做前缀。如–help和-h
以下死记硬背内容:
ls——列出文件信息
cd——切换工作目录
pwd——显示当前工作目录
uname——打印系统信息
whoami——打印用户名
man——查询帮助信息
find——查找文件
echo——打印文本,参数-e可激活转义字符
cat——打印到标准输出
head/tail——打印文本的前/后几行
mv——移动或重命名文件
cp——复制文件
rm——删除文件
ps——查看进程状态
top——实时查看系统运行状态
kill——杀死进程
ifconfig——查看或设置网络设备
grep——匹配文本模式
nc——建立TCP/UDP并监听
netstat——查看网络,路由器,接口等信息。
ping——判断网络主机是否响应
su——切换到超级用户
diff——比较文本差异
cut——通过列提取文本
history——查看bash_history中的历史命令
exit——退出shell
chmod——变更文件或目录权限
流、管道、重定向
- 流是一个非常重要的概念,简单理解成一串连续的,可边读边操作的数据。标准流分为:标入,标出,标错文件描述符是内核为管理已打开文件所创建的索引,使用一个非负整数来指代被打开的文件。Linux中一切都可以被看作文件
- 管道是指一系列进程通过标准流连接在了一起,前一个进程的输出直接作为后一个进程的输入。管道符号为“|”如:“ps -aux|grep bash” 指令
- 重定向:>,>>,<,<<,这是比较显然的,重定向方向即为箭头方向。双重表追加,单个表覆盖,当单个符号时,有时会在前者附加一2,表示流为标准错误流。cmd < file1 > file2指令表示把1作为cmd的输入,再将cmd的输出重定向到2。
根目录结构
文件成树状排布。
LINIX系统中,有三种基本文件类型:
- 普通文件:文本文件和二进制文件
- 目录:包含一组链接的文本,且中每个链接都将一文件名映射到一个文件
- 特殊文件:包含块文件、符号链接、管道、套接字等
用户组及文件权限
Linux支持多用户操作,每个用户都有UID和GID,其中UID是对一个用户的单一身份标识,而GID则对应多个UID。一些程序可能需要UID/GID才可运行。ID可以通过id
指令进行查看。
UID为0的root用户类似于系统管理员(Administrator)拥有系统的完全访问权。
su指令可以让我们从普通成员转为系统管理员。
文件的访问权限可以通过ls -l [file]
来查看。第一个字符表类型,相关细节可以参考:细节。第2-4个字符是文件所有者权限,第5-7为组权限,剩下三位为其他人权限。r,w,x对应读,写,执行。没有当前权限则用_占位。
可以通过chmod指令变更文件权限。用法可参看:用法。
环境变量
环境变量这一概念与win中的基本一致。
Linux中的环境变量有两种划分方式:按生命周期分有永久与临时之分,按作用域分有系统变量与用户变量之分。用env
指令可以查看所有环境变量。
几个特殊的环境变量
- LD——PRELOAD环境变量可以定义程序运行时优先加载的动态链接库,这就允许预加载库中的函数与符号能够覆盖掉后加载的库中的函数和符号。我们可以通过修改改该环境变量来实现加载特定libc的操作。
- environ,libc中定义,指向环境变量表,该表就在栈上,所以通过泄露environ指针的地址,就可以获得栈地址。
procfs文件系统
这是一种由Linux内核提供的虚拟文件系统,为访问内核数据提供接口,只占用内存而不占用存储。每个正在运行的进程都对应/proc下的一个目录,目录名就是进程的PID。而每个线程也有其对应的TID。
auxv的每一项都是由一个unsigned long的id加上一个unsigned long的值构成,每个值具体的用途可以通过设置环境变量LD_SHOW_AUXV=1显示出来辅助向量存放在栈上,附带了传递给动态连接器的程序相关的特定信息。
syscall第一个值是系统调用号,后跟六个参数,最后两个分别是堆栈指针和指令计数器。
字节序
大端序和小端序。大端序直观,为arm所独有,小端序符合常识,为i386所采用。(小端序为低位存于低址的存放方式)
(函数参数)调用约定
内核接口:
- 32位:eax为sycall_number,ebx,ecx,edx,esi和ebp用于将6个参数传递给系统调用。返回值保存在eax中,所有其他寄存器保存在int 0x80中。
- 64位:系统调用通过syscall完成,除rcx,r11,rax外所有其他的寄存器都会被保存。系统调用的编号必须即存在rax中,不直接从堆栈上传递任何参数,且参数传递上限为6个。返回时rax保存了结果,只有MEMORY或者INTEGER类型的值才会被传给内核。
用户接口:
- 32位:cdecl约定(见上章)
- 64位:通过寄存器传参,纸样比通过栈具有更高的效率。如果参数的类型是MEMORY,则在栈上传递参数,如果类型是UNTEGER则属需使用rdi,rsu,rdx,rcx,r8和r9。若参数多于六个,剩下的参数在栈上传递。
核心转储
dump(转储)当程序运行的过程中出现异常或崩溃,程序就会将程序崩溃时的内存、寄存器状态、堆栈、指针、内存管理信息等记录下来,保存到一个文件中,叫做核心转储。在linux中,一般情况下,核心转储是关闭的。
ulimit -c(默认关闭)
ulimit -c unimted(临时开启)
cat /etc/security/limits.conf(将value 0修改为unlimited,永久开启)
#修改core_users_pid,使核心转储文件名变为core.[pid]
echo 1 > /proc/sys/kernel/core_uses_pid
(分析:echo,打印文本,将“1”作为输出,再用>进行重定向,将core_uses_pid改为1)
#还可以修改core_pattern,保存到/tmp目录,文件名为core-[filename]-[pid]-[time]
echo /tmp/core-%e-%p-%t > /proc/sys/kernel/core_parttern
系统调用
这是用户空间访问内核的唯一手段。
int 0x80中断与系统调用。大概是此中断能使整个系统进入或退出内核状态,借以进行处理。(是相当经典的调用方式)
现在则多用< syscall >< sysret >(64位) < sysenter >< sysexit >(32位)。注意,< sysenter >使用前,需要为其手动布置栈,因为< sysenter >被__kernel_vscall封装了sysenter调用的规范,是vDSO的一部分,而vDSO允许在用户层中执行内核代码。
直接调用系统调用的程序并不多见,更多的是通过高层接口来进行间接调用。printf函数即是如此。
printf()——>c库中的printf()——>c库中的write()——>系统调用中的write()。
Stack Canaries
在pwn checksec file
指令使用后,若出现:stack: canary found.则说明采用了这种方式来保护堆栈。
而Stack Canary一般分为三类:
T canary:低位赋为0x00.
R canary:全随机数,或关于时间的哈希.
X canary:全随机数,增加攻破难度。
作用:检测栈溢出错误。
开启与关闭操作:
编译时:gcc -fno-stack-protector file.c -o f.out
测试:python -c 'print("A"*20)'| ./f.out
(关闭)
编译时:gcc -fstack-protector file.c -o f.out
测试:python -c 'print("A"*20)'| ./f.out
(开启)
开启时出现“段溢出”提示。
关闭时发生崩溃。
体察生成过程
以64位程序为例,在程序加载过程中,首先初始化TLS(Thread Local Storage,线程局部存储),包括为其分配空间以及配置fs寄存器。(arch_prctl系统调用)
程序调用security_init()函数,生成Canary的值stack_chk_guard,放入fs:0x28。
当然,除security_init()函数以外,在__libc_start_main()函数中也可生成Canary。其中,__dl_random指向一个由内核提供的随机数。当然也可以选择由glibc自己生成。
接下来进入_dl_setup_stack_chk_guard()函数,并根据位数(32或64)以及字节序(大端小端)生成相应的Canary值。为使Canary具有字符截断的效果,其最低位设置为0x00。
当dl_random指针为NULL,Canary为定值(即不进行移位操作)
然后程序将生成的Canary交给THREAD_SET_STACK_GUARD宏进行处理,其中THREAD——SETMEM可以直接修改线程描述符的成员。执行完毕后,Canary被放到fs:0x28的位置,程序运行时即可取出使用。
在程序运行时,调用栈后,先从fs:0x28处取出Canaries,然后将其放入离栈入口很近的低址处(大部分情况下可尝试为8)。在栈调用后,判断低址处Canary的值是否被改变(与程序保存的副本进行异或运算),若不为零,则报警。
csapp:
hackit
攻击Canaries的主要目的是避免程序崩溃,那么就有两种思路:
第一种 将Canaries的值泄露出来,然后在栈溢出的时候覆盖上去,使其保持不变;
第二种 同时篡改TLS(fs寄存器)和栈上的Canaries,这样在检查的时候就能够通过。
例题之——NJCTF 2017: messager
第一步:pwn checksec [ filename ]指令,查看文件保护类型。
第二步:自建flag到本地(可以不用)
第三步:netstat -anp | grep messager指令打开网口
第四步:反汇编,读程。
第五步:分析漏洞:fork进程不会重新生成canary,故可以不断进入子进程进行爆破,得到后写payload,进攻发送flag的函数。完。
No-eXecute
表示不可执行,其原理是将数据所存在的内存页(如堆和栈)标识为不可执行,如果程序产生溢出转入执行shellcode时,CPU就会抛出异常。通常我们使用可执行空间保护作为一个统称,来描述这种防止传统代码注入攻击的技术——攻击者将恶意代码注入正在运行的程序中,然后利用内存损坏漏洞将控制流重定向到该代码。
NX的实现需要结合软件和硬件
一点常识:
payload:有效负载,本题为将shellcode写入ip寄存器,使得程序执行发送flag的操作。
payload组成:“padding1”+“canary”+“padding2”+“shellcode”,其中,shellcode必须为机器码。padding1和padding2为不固定的覆盖数,可以尝试得到确切长度。canary为本题特有。一般为shellcode的地址。
shellcode:即攻击指令,放入寄存器后,程序被黑客控制的机理就是因为有它。
pwntools使用:from pwn import *
函数:p64(contains)可以把contains转为机械码。