其他分享
首页 > 其他分享> > 《CTF权威指南(pwn)》学习笔记

《CTF权威指南(pwn)》学习笔记

作者:互联网

第一章

知名安全会议

RSA、Black Hat、DEFCON、ISC(中国互联网安全大会) 学术顶会 CCS(A): ACM Conference on Computer and Communications Security NDSS(B): Network and Distributed System Security Symposium Oakland S&P(A): IEEE Symposium on Security & Privacy USENIX(A): USENIX Security Symposium

学习路线

计算机基础

中文:网易云课堂的大学计算机专业课程体系
英文:Harvard CS50 Introduction to Computer Science
CMU 18-447 Introduction to Computer Architecture
MIT 6.828 Operating System Engineering
Stanford CS143 Compilers

逆向工程

掌握各平台的静态反汇编(IDA、Radare2)和动态调试(GDB、x64dbg)工具,熟练阅读反汇编代码,理解x86、ARM和MIPS二进制程序,注意程序结构组成和编译运行的细节。
推荐书籍:
Secure Coding in C and C++,2nd Edition
The Intel 64 and IA-32 Architectures Software Developer's Manual
ARM Cortex-A Series Programmer's Guide
See MIPS Run.2nd Edition
Reverse Engineering for Beginners
《程序员的自我修养--链接、装载与库》
《加密与解密,第4版》

漏洞学习

从CTF切入是一个很好的思路。学习常见的漏洞(溢出、UAF、double-free等)的原理、Linux漏洞缓解机制(stack canaries、NX、ASLR等)以及针对这些机制的漏洞利用方法(stack smashing、shellcoding、ROP等),此阶段还可以读wp来学习。在掌握这些基本知识之后就可以,尝试分析真实环境中的漏洞,或者分析一些恶意样本了。
推荐资料:
RPI CSCI-4968 Modern Binary Exploitation
Hacking: The Art of Exploitation,2nd Edition
The Shellcode r' s Handbook,2nd Edition
Practical Malware Analysis
《漏洞战争:软件漏洞分析精要》

有了实践的基础后,可以学习分析一些程序分析理论,比如数据流分析(工具如Soot)、值集分析(BAP)、可满足性理论(Z3)、动态二进制插桩(DynamoRio、Pin)、符号执行(KLEE、angr)、模糊测试(Peach、AFL)等。这些技术对于程序分析和漏洞挖掘自动化非常重要,是学术界和工业界都在研究的热点。感兴趣的还可以关注下自动化网络攻防的CGC竞赛。推荐资料如下:
UT Dallas CS-6V81 System Security and Binary Code Analysis
AU static Program Analysis Lecture notes

如果是走学术路线,阅读论文必不可少,一开始可以读综述类的文章,对某个领域的研究情况有全面的了解,然后跟随综述去找相应的论文。比较推荐会议论文,因为通常可以在作者个人主页上找到幻灯片,甚至会议录像视频,对学习理解论文很有帮助。如果直接读论文则感觉会有些困难,这里推荐上交大"蜚语"安全小组的论文笔记。坚持读、多思考,相信量变终会产生质变。

为了持续学习和提升,还需要收集和订阅一些安全资讯(FreeBuf、SecWiki、安全客)、漏洞披露(exploit-db、CVE)、技术论坛(看雪论坛、吾爱破解、先知社区)和大牛的技术博客,这一步可以通过RSS Feed来完成。随着社会媒体的发展,很多安全团队和个人都转战到了Twitter微博、微信公众号等新媒体上,请果断关注他们(操作技巧:从某个安全研究者开始,遍历其关注列表,然后递归,即可获得大量相关资源),通常可以获得最新的研究成果、漏洞、PoC、会议演讲等信息甚至资源链接等。
目前二进制方向很难就业,如果不是特别有兴趣和死磕一辈子的决心,建议还是选择web安全、安全管理等方向。

对从业者的建议

1.关于个人成长

1)确立方向,结合工作,找出短板
该领域主要专家们的工作是否都了解过?
相关协议、文件格式是否都熟悉?
相关技术和主要工具是否看过、用过?
2)阅读只是学习过程的起点,不能止于阅读
工具的每个参数每个菜单都要看、要试
学习网络协议要实际抓包分析,学习文件格式要读代码实现.学习老漏洞一定要调试,搞懂每一个字节的意义, 之后要完全自己重写一个Exploit
细节、细节、细节,刨根问底

2.建立学习参考目标

(1)短期参考比自己优秀的同龄人。阅读他们的文章和工作成果,从细节中观察他们的学习方式和工作方式。
(2)中期参考你的方向上的业内专家。了解他们的成长轨迹,跟踪他们关注的内容。
(3)长期参考业内老牌企业和先锋企业。把握行业发展、技术趋势,为未来做积累。

3.推荐的学习方式

(1)以工具为线索
一个比较省事的学习目录: Kali Linux
学习思路,以Metasploi为例: 遍历每个子目录,除了Exploi里面还有什么?每个工具怎么用?原理是什么?涉及哪些知识?能否改进优化?能否发展、组合出新的功能?
(2)以专家为线索
你的技术方向上有哪些专家?他们的邮箱、主页、社交网络账号是什么?他们在该方向上有哪些作品,发表过哪些演讲?跟踪关注,一个一个地学。

4.如何提高效率

做好预研,收集相关前人成果,避免无谓的重复劳动
在可行性判断阶段,能找到工具就不写代码,能用脚本语言写就不要用编译语言,把完美主义放在最终实现阶段
做好笔记并定期整理,遗忘会让所有的投入都白白浪费
多和同事交流,别人说一个工具的名字可能让你节约数小时
处理好学习、工作和生活
无论怎么提高效率,要成为专家,都需要大量的时间投入

第二章

2.1.1 编译原理

5个步骤:
词法分析
语法分析
语义分析
中间代码生成和优化
代码生成和优化

2.1.2 GCC编译过程

GCC命令加"-save-temps"和"--verbose"编译选项,前者用于将编译过程中生成的中间文件保存下来,后者查看GCC编译的详细流程。
主要包含四个阶段:预处理、编译、汇编和链接

2.1.3 预处理阶段

处理源代码中"#"开头的预处理指令,将其转换后直接插入程序文本,得到另一个C程序,通常以".i"作为文件扩展名。"-E"选项可以单独执行预处理。
自行了解一些常见的预处理规则。

2.1.4 编译阶段

完成分析及优化,生成汇编代码。"-S"选项单独执行。例:"gcc -S hello.c -o hello.s"
GCC默认是使用AT&T格式的汇编,可以加选项"-masm=intel"使其指定为 我们熟悉的intel格式。"-fno-asynchronous-unwind-tables" 则用于生成没有cfi宏的汇编指令,以提高可读性。

2.1.5 汇编阶段

将汇编指令翻译成机器指令。"-c"单独执行,例:"gcc -c hello.s -o hello.o"。

2.1.6 链接阶段

可分为静态链接和动态链接,默认是动态链接,"-static"选项可指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配(Address and Storage Allocation)、符号绑定(Symbol Binding)和重定位(Relocation)等操作。
链接操作由链接器(Id.so)完成,结果就得到了hello文件,这是一个静态链接的可执行文件(Executable File),包含了大量的库文件。

2.2ELF文件格式

ELF(Executable and Linkable Format),即“可执行可链接格式”,是COFF(Common file format)格式的变种。Linux相关定义在"/usr/include/elf.h"文件里。

2.2.1 ELF文件的类型

ELF文件主要分三种类型,可执行文件(.exec)、可重定位文件(.rel)和共享目标文件(.dyn),此外,还有核心转储文件(Core Dump file)作为进程意外终止时进程地址空间的转储。

2.2.2 ELF文件的结构

ELF文件统称为Obiect file.这与我们通常理解的".o"文件不同。本笔记中提到目标文件时,指各种类型的ELF文件。对于".o"文件,则表示为可重定位文件,此类文件包含了代码和数据,可以被用于链接成可执行文件或者共享目标文件。
从链接视角看,可以将目标文件用节(Section)来划分;另一种是运行视角,通过段(Segment)来进行划分。本节先学习链接视角。通常目标文件会包含代码(.text)、数据(.data)和BSS(.bss)三个节。其中代码节用于保存机器指令,数据节保存已初始化的全局变量和局部静态变量,BSS节保存未初始化的全局变量和局部静态变量。目标文件还包含一个文件头。
ELF文件头位于目标文件最开始位置,包含ELF文件类型、版本/ABI版本、目标机器、程序入口、段表和节表的位置和长度等。

64位ELF文件
ELF 文件解析 3-段 - 知乎 (zhihu.com)
<<elf-64-gen.pdf>>

Linux安全机制

Linux基础

procfs虚拟文件系统。通过procfs查看系统硬件及正在运行进程的信息,可以通过修改其中某些内容来改变内核的运行状态。
每个正在运行的进程都对应/proc下的一个目录,目录名就算进程的PID。

比较重要的文件(cat命令为例):

auxv                       传递给进程的解释器信息
cmdline                     启动进程的命令行
cwd -> /home/firmy 当前工作目录
environ                      进程的环境变量
exe -> /bin/cat          最初的可执行文件
fd                               进程打开的文件  
fdinfo                         每个打开文件的信息
maps                          内存映射信息
mem                          内存空间
root -> /                    进程的根目录
stack                          内核调用栈
status                         进程的基本信 
syscall                        正在执行的系统调用
task                           进程包含的所有线程
/proc/PID/mem 由open、read和seek等系统调用使用,无法由用户直接读取,但其内容可以通过/proc/PID/maps 查看,进程的布局通过内存映射来实现,包括可执行文件、共享库、栈、堆等。
看栈(cat /proc/xx/stack)需要在编译内核时启用CONFIG_STACKTRACE选项
auxv (AUXiliary Vector)的每一项都是由一个unsigned long 的ID加上一个unsigned long的值构成,每个值具体的用途可以通过设置环境变量 LD_SHOW_AUXV=1显示出来。辅助向量存放在栈上,附带了传递给动态链接器的程序相关的特定信息。
task 每个线程的信息分别放在一个由线程号(TID)命名的目录中.
syscall 第一个值是系统调用号,后面跟着六个参数,最后两个值分别是堆栈指针和指令计数器。

调用约定

(1)内核接口
x86-32系统调用约定:Linux系统调用使用使用寄存器传递参数。eax为syscall_nnumber,ebx、ecx、edx、esi和ebp用于将6个参数传递给系统调用,返回值保存在eax中。其他寄存器都保留在int 0x80中。
x86-64系统调用约定:内核接口使用的寄存器有rdi、rsi、rdx、r10、r8和r9。通过指令syscall完成。系统调用的编号必须在寄存器rax中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax包含了结果,且只有integer或者memory类型的值才会被传递给内核。
(2)用户接口
x86-32函数调用约定:参数通过栈进行传递。最后一个参数第一个被放入栈中,直到所有的参数都放置完毕,然后执行call指令。这也是Linux上C语言默认的方式。
x86-64函数调用约定:x86-64下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是MEMORY,则在栈上传递参数。
如果类型是INTEGER,则顺序使用rdi、rsi、rdx、rcx、r8和r9。所以如果有多于6个的INTEGER参数,则后面的参数在栈上传递。

核心转储

当程序运行的过程中出现异常终止或崩溃,系统就会将程序崩溃时的内存、寄存器状态、堆栈指针、内存管理信息等记录下来,保存在一个文件中,叫作核心转储(Core Dump)

信号 动作 解释
SIGQUIT Core 通过键盘退出时
SIGILL Core 遇到不合法的指令时
SIGABRT Core 从abort中产生的信号
SIGSEGV Core 无效的内存访问
SIGTRAP Core trace/breakpoint陷阱

系统调用

在Linux中,系统调用是一些内核空间函数,是用户空间访问内核的唯一手段。这些函数与CPU架构有关,86提供了358个系统调用,86-64提供了322个系统调用。32位和64位有区别。
先看看32位的:

.data
msg:
    .ascii "hello 32-bit!\n"
    len = . - msg
    
.text
    .global_start
    
_start:
    movl $len,%edx
    movl Smsg,%ecx
    movl $1,%ebx
    movl $4,%eax
    int $0x80
    
    movl $0,%ebx
    mov1 $1,%eax
    int $0x80

程序将调用号保存在eax中,参数传递的顺序依次是ebx,ecx,edx,,esi和edi。通过int $0x80来执行系统调用,返回值存放在eax。
(可以被编译成64位程序)
软中断 int 0x80 早期2.6及更早版本的内核都使用这种机制进行系统调用,但因其性能较差,在往后的内核中被快速系统调用替代,如32位系统使用sysenter指令,64位syscall指令。

栈保护机制 stack canaries

canary的值是栈上的一个随机数,程序启动时随机生成并保存在比函数返回地址更低的位置。由于栈溢出是从高地址进行覆盖,因此攻击者想要控制函数的返回指针,就一定要先覆盖canary,程序之需要在函数返回前检查canary是否被篡改,就可以达到保护栈的目的。

简介

通常可分为3类:terminator、random和random XOR,具体实现有StackGuard、StackShield、ProPoliced等。
关于StackGuard的论文:StackGuard:Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks.

Terminator canaries:由于许多栈溢出是字符串操作不当所产生的,而这些字符串以"\x00"截断,基于这一点,terminator canaries 将低位设置为"\x00",既可以防止被泄露,也可以防止被伪造。截断字符还包括CR(0x0d)、LF(0x0a)和EOF(0xff)。

Random canaries:为防止canaries被攻击者猜到,random canaries通常在程序初始化时随机生成,并保存在一个相对安全的地方。随机数通常由/dev/urandom生成,有时也使用当前时间的哈希。

Random XOR canaries: 与random canaries 类似,但多一个XOR操作,这样无论canaries被篡改还是与之XOR的控制数据被篡改,都会发生错误,这就增加了攻击难度。

GCC包含多个canaries有关参数,可以用帮助命令查看。
-fstack-protector 对alloca系列函数和内部缓冲区大于8字节的函数启用保护
-fno-stack-protector 禁用保护
可以自己敲个实例试试,看看反汇编代码分析分析。

gef> disassemble main
0x00000000004005b6<+0>: push rbp
0x00000000004005b7<+1>: mov rbp,rsp
0x00000000004005ba<+4>: sub rsp,0x20
0x00000000004005be<+8>: mov rax,QWORD PTR fs:0x28
0x00000000004005c7<+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000004005cb<+21>: xor eax,eax
0x00000000004005cd<+23>: 1ea rax,[rbp-0x20]
0x00000000004005d1<+27>: mov rsi,rax
0x00000000004005d4<+30>: mov edi,0x400684
0x00000000004005d9<+35>: mov eax,0x0
0x00000000004005de<+40>: ca11 0x4004a0 <_isoc99_scanf@plt>
0x00000000004005e3<+45>: nop
0x00000000004005e4<+46>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004005e8<+50>: xor rax,QWORD PTR fs 0x28
0x00000000004005f1<+59>: je 0x4005f8 <main+66>
0x00000000004005f3<+61>: ca11 0x400480 <stack_chk_fail@plt>
0x00000000004005f8<+66>: 1eave
0x00000000004005f9<+67>: ret

在Linux中,fs寄存器被用于存放线程局部存储(Thread Local Storage, TLS),TLS主要是为了避免多个线程同时访存同一全局变量或静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。TLS为每一个使用该全局变量的线程提供了一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会影响其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。从全局变量的角度看,就像被复制了很多副本,每一个副本都被一个线程独立占有。在glibc的实现中,TLS结构体tcbhead_t的定义如下所示,偏移0x28正是stack_guard。

typedef struct{
void *tcb; /* Pointer to the TCB.Not necessarily the  
           thread descriptor used by libpthread.*/
dtv_t *dtv;
void *self; /*Pointer to the thread descriptor.*/
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
......
} tcbhead_t;

从TLS取出canary后,程序就将其插入rbp-0x8的位置暂时保存。在函数返回前,又从栈上将其取出,并于TLS中的canary进行异或比较,从而确定两个值是否相等。如果不相等就说明发生了栈溢出,然后转到__stack_chk_fail()函数中,程序终止并抛出错误;否则程序正常退出。
如果是32位程序,那么canary就变成了gs寄存器偏移0x14的地方。

typedef struct{
void *tcb; /*Pointer to the TCB.Not necessarily the
            thread descriptor used by libpthread.*/
dtv_t *dtv;
void *self; /*Pointer to the thread descriptor.*/
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
......
} tcbhead_t;
gef > disassemble main
......
0x0804849c<+17>:mov eax,gs:0x14
0x080484a2<+23>:mov DWORD PTR [ebp-0xc],eax
......
0x080484bc<+49>:mov eax,DWORD PTR [ebp-0xc]
0x080484bf<+52>:xor eax,DWORD PTR gs:0x14
0x080484c6<+59>:je 0x80484cd<main+66>
0x080484c8<+61>:ca11 0x8048350 <__stack_chk_fail@plt>
......

checksec.sh对Canary的检测也是根据是否存在__stack_chk_fail__intel_security_cookie来进行判断。

至此,我发现我的笔记几乎都是从书上摘抄下来,只有很少自己的理解。我不愿再抄书,后面只概括主要内容,加上自己的理解。

实现

以64位程序为例,程序加载时,glibc中的ld.so首先初始化TLS,为其分配空间,通过arch_prctl系统调用设置fs寄存器指向TLS。
然后程序调用security_init()函数,生成Canary的值stack_chk_guard,并放入fs:0x28。

static void security_init (void)
{
    /* Set up the stack checker's canary.  */
    uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
    THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
    __stack_chk_guard = stack_chk_guard;
#endif

    /* Set up the pointer guard as well, if necessary.  */
    uintptr_t pointer_chk_guard
    = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);
#ifdef THREAD_SET_POINTER_GUARD
    THREAD_SET_POINTER_GUARD (pointer_chk_guard);
#endif
    __pointer_chk_guard_local = pointer_chk_guard;

    /* We do not need the _dl_random value anymore.  The less
       information we leave behind, the better, so clear the
       variable.  */
    _dl_random = NULL;
}

glibc/elf/rtld.c
security_init()函数生成canary值。

STATIC int LIBC_START_MAIN {
......
    /* Set up the stack checker's canary.  */
    uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
    THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
    __stack_chk_guard = stack_chk_guard;
# endif

glibc/csu/libc-start.c
__libc_start_main()函数生成canary值

/* Random data provided by the kernel.  */
void *_dl_random;

glibc/elf/ld-support.c
_dl_random指向一个由内核提供的随机数。

标签:文件,调用,chk,guard,笔记,eax,CTF,pwn,stack
来源: https://www.cnblogs.com/dreamc-top/p/pwn_notes.html