CTF竞赛权威指南(PWN)
一、 基础知识篇
1.1 linux基础
内核接口
x86-32 系统调用约定:Linux 系统调用使用寄存器传递参数。eax
为 syscall_number,ebx
、ecx
、edx
、esi
、ebp
用于将 6 个参数传递给系统调用。返回值保存在 eax
中。所有其他寄存器(包括 EFLAGS)都保留在 int 0x80
中。
x86-64 系统调用约定:内核接口使用的寄存器有:rdi
、rsi
、rdx
、r10
、r8
、r9
。系统调用通过 syscall
指令完成。除了 rcx
、r11
和 rax
,其他的寄存器都被保留。系统调用的编号必须在寄存器 rax
中传递。系统调用的参数限制为 6 个,不直接从堆栈上传递任何参数。返回时,rax
中包含了系统调用的结果。而且只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。
用户接口
x86-32 函数调用约定:参数通过栈进行传递。最后一个参数第一个被放入栈中,直到所有的参数都放置完毕,然后执行 call 指令。这也是 Linux 上 C 语言函数的方式。
x86-64 函数调用约定:x86-64 下通过寄存器传递参数,这样做比通过栈有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY,则在栈上传递参数。如果类型是 INTEGER,则顺序使用 rdi
、rsi
、rdx
、rcx
、r8
和 r9
。所以如果有多于 6 个的 INTEGER 参数,则后面的参数在栈上传递。
状态码
状态码表明资源的请求结果状态,由三位十进制数组成,第一位代表基本的类别:
- 1xx,提供信息
- 2xx,请求成功提交
- 3xx,客户端重定向其他资源
- 4xx,请求包含错误
- 5xx,服务端执行遇到错误
常见的状态码及短语如下所示:
状态码 | 短语 | 描述 |
---|---|---|
100 | Continue | 服务端已收到请求并要求客户端继续发送主体 |
200 | Ok | 已成功提交,且响应主体中包含请求结果 |
201 | Created | PUT 请求方法的返回状态,请求成功提交 |
301 | Moved Permanently | 请求永久重定向 |
302 | Found | 暂时重定向 |
304 | Not Modified | 指示浏览器使用缓存中的资源副本 |
400 | Bad Request | 客户端提交请求无效 |
401 | Unauthorized | 服务端要求身份验证 |
403 | Forbidden | 禁止访问被请求资源 |
404 | Not Found | 所请求的资源不存在 |
405 | Method Not Allowed | 请求方法不支持 |
413 | Request Entity Too Large | 请求主体过长 |
414 | Request URI Too Long | 请求URL过长 |
500 | Internal Server Error | 服务器执行请求时遇到错误 |
503 | Service Unavailable | Web 服务器正常,但请求无法被响应 |
401 状态支持的 HTTP 身份认证:
- Basic,以 Base64 编码的方式发送证书
- NTLM,一种质询-响应机制
- Digest,一种质询-响应机制,随同证书一起使用一个随机的 MD5 校验和
JavaScript 打印数据
在浏览器中调试代码时,经常用到的手段是打印变量。
函数 | 作用 |
---|---|
window.alert() | 弹出警告框 |
document.write() | 写入HTML文档 |
console.log() | 写入浏览器控制台 |
文件后缀解析
由于 Nginx 对 CGI 的使用更加广泛,所以 PHP 在 CGI 的一些解析特性放到 Nginx 这里来讲解,PHP 具有对文件路径进行修正的特性,使用如下配置参数:
cgi.fix_pathinfo = 1
当使用如下的 URL 来访问一个存在的 1.jpg 资源时,Nginx 认为这是一个 PHP 资源,于是会将该资源交给 PHP 来处理,而 PHP 此时会发现 1.php 不存在,通过修正路径,PHP 会将存在的 1.jpg 作为 PHP 来执行。
http://xxx/xxx/1.jpg/1.php
相似的绕过方式还有以下几种方式:
http://xxx/xxx/1.jpg%00.php
http://xxx/xxx/1.jpg \0.php
但是,新版本的 PHP 引入了新的配置项 “security.limit_extensions” 来限制可执行的文件后缀,以此来弥补 CGI 文件后缀解析的不足。
IIS解析特征
- IIS 短文件名
为了兼容 16 位 MS-DOS 程序, Windows 会为文件名较长的文件生成对应的短文件名,如下所示:
利用这种文件机制,我们可以在 IIS 和 .net 环境下进行短文件名爆破。
- IIS 6.0 解析特性
IIS 6.0 解析文件时会忽略分号后的字符串,因此 1.asp;2.jpg
将会被解析为 1.asp
。
- IIS 也存在类似于 Nginx 的 CGI 解析特性
文件拓展名
URL 中使用的文件扩展名也能够揭示相关的服务平台和编程语言,如:
asp
:Microsoft Active Server Pagesaspx
:Microsoft ASP.NETjsp
:Java Server Pagesphp
:PHP
会话令牌
许多服务会默认生成会话令牌,通过读取 cookie 中的会话令牌可以判断所使用的技术。如:
JSESSIONID
:JAVAASPSESSIONID
:IISASP.NET_SessionId
:ASP.NETPHPSESSID
:PHP
1.2 C/C++基础
从源代码到可执行文件4步骤:
预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)
预编译
gcc -E hello.c -o hello.i
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
......
extern int printf (const char *__restrict __format, ...);
......
main() {
printf("hello, world\n");
}
预编译过程主要处理源代码中以 “#” 开始的预编译指令:
- 将所有的 “#define” 删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
- 处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,该过程递归执行。
- 删除所有注释。
- 添加行号和文件名标号。
- 保留所有的 #pragma 编译器指令。
编译
gcc -S hello.c -o hello.s
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。
汇编
$ gcc -c hello.s -o hello.o
或者
$gcc -c hello.c -o hello.o
$ objdump -sd hello.o
汇编器奖汇编代码转变成机器可执行指令。
gcc技巧
通常在编译后只会生成一个可执行文件,而中间过程生成的 .i
、.s
、.o
文件都不会被保存。我们可以使用参数 -save-temps
永久保存这些临时的中间文件。
$ gcc -save-temps hello.c
$ ls
a.out hello.c hello.i hello.o hello.s
这里要注意的是,gcc 默认使用动态链接,所以这里生成的 a.out 实际上是共享目标文件。
$ file a.out
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=533aa4ca46d513b1276d14657ec41298cafd98b1, not stripped
使用参数 --verbose
可以输出 gcc 详细的工作流程。
gcc hello.c -static --verbose
东西很多,我们主要关注下面几条信息:
$ /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/cc1 -quiet -v hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -o /tmp/ccj1jUMo.s
as -v --64 -o /tmp/ccAmXrfa.o /tmp/ccj1jUMo.s
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/collect2 -plugin /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/lto-wrapper -plugin-opt=-fresolution=/tmp/cc1l5oJV.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crt1.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crti.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtbeginT.o -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../.. /tmp/ccAmXrfa.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtend.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crtn.o
三条指令分别是 cc1
、as
和 collect2
,cc1 是 gcc 的编译器,将 .c
文件编译为 .s
文件,as 是汇编器命令,将 .s
文件汇编成 .o
文件,collect2 是链接器命令,它是对命令 ld 的封装。静态链接时,gcc 将 C 语言运行时库的 5 个重要目标文件 crt1.o
、crti.o
、crtbeginT.o
、crtend.o
、crtn.o
和 -lgcc
、-lgcc_eh
、-lc
表示的 3 个静态库链接到可执行文件中。
C语言标准库
常用的标准库文件头:
- 标准输入输出(stdio.h)
- 字符操作(ctype.h)
- 字符串操作(string.h)
- 数学函数(math.h)
- 实用程序库(stdlib.h)
- 时间/日期(time.h)
- 断言(assert.h)
- 各种类型上的常数(limits.h & float.h)
- 变长参数(stdarg.h)
- 非局部跳转(setjmp.h)
整数表示
int 可表示正数或负数
unsigned int 只能表示为0或正数
signed
或者 unsigned
取决于整数类型是否可以携带标志 +/-
:
Signed
- int
- signed int
- long
Unsigned
- unit
- unsigned int
- unsigned long
格式化输出函数
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
fprintf()
按照格式字符串的内容将输出写入流中。三个参数为流、格式字符串和变参列表。printf()
等同于fprintf()
,但是它假定输出流为stdout
。sprintf()
等同于fprintf()
,但是输出不是写入流而是写入数组。在写入的字符串末尾必须添加一个空字符。snprintf()
等同于sprintf()
,但是它指定了可写入字符的最大值size
。当size
大于零时,输出字符超过第size-1
的部分会被舍弃而不会写入数组中,在写入数组的字符串末尾会添加一个空字符。dprintf()
等同于fprintf()
,但是它输出不是流而是一个文件描述符fd
。vfprintf()
、vprintf()
、vsprintf()
、vsnprintf()
、vdprintf()
分别与上面的函数对应,只是它们将变参列表换成了va_list
类型的参数。
格式转换符
一个转换规则有可选部分和必需部分组成:
%[ 参数 ][ 标志 ][ 宽度 ][ .精度 ][ 长度 ] 转换指示符
1.3 X86汇编基础
数据移动指令
mov 移动
mov
指令将数据从它的第一个参数 ( 即寄存器中的内容, 内存单元中的内容, 或者一个常数值 ) 复制到它的第二个参数 ( 即寄存器或者内存单元 ).
语法:
mov <reg, <reg
mov <reg, <mem
mov <mem, <reg
mov <con, <reg
mov <con, <mem
例子:
mov %ebx, %eax #将 EBX 中的值复制到 EAX 中
mov $5, var(,1) #将数字 5 存到字节型内存单元 " var "
push 入栈
push
指令将它的参数移动到硬件支持的栈内存顶端. 特别地, push
首先将 ESP 中的值减少 4, 然后将它的参数移动到一个 32 位的地址单元 ( %esp ). ESP ( 栈指针 ) 会随着不断入栈从而持续递减, 即栈内存是从高地址单元到低地址单元增长.
语法:
push <wreg32
push <mem
push <con32
例子:
push %eax #将EAX送入栈
push var(,1) #将var对应的4字节大小的数据送入栈中
pop 出栈
pop
指令从硬件支持的栈内存顶端移除 4 字节的数据, 并把这个数据放到该指令指定的参数中 ( 即寄存器或者内存单元 ). 其首先将内存中 ( %esp ) 的 4 字节数据放到指定的寄存器或者内存单元中, 然后让 ESP + 4.
语法:
pop <reg32
pop <mem
例子:
pop %edi #将栈顶的元素移除,并放入到寄存器EDI中
pop (%ebx) #将栈顶的元素移除,并放入从EBX开始的4字节大小的内存单元中
lea 加载有效地址
lea
指令将其第一个参数指定的内存单元 放入到 第二个参数指定的寄存器中. 注意, 该指令不加载内存单元中的内容, 只是计算有效地址并将其放入寄存器. 这对于获得指向存储器区域的指针或者执行简单的算术运算非常有用.
- 汇编语言中 lea 指令和 mov 指令的区别 ?
这里的代码是 16 位 MASM 汇编的格式, 和我们现在用的 AT&T 汇编有一些细微区别, 不过不影响我们的理解.
MOV
指令的功能是传送数据, 例如 MOV AX,[1000H]
, 作用是将 1000H 作为偏移地址, 寻址找到内存单元, 将该内存单元中的数据送至 AX;
LEA
指令的功能是取偏移地址, 例如 LEA AX,[1000H]
, 作用是将源操作数 [1000H] 的偏移地址 1000H 送至 AX.理解时, 可直接将[ ]去掉, 等同于 MOV AX,1000H
.
再如: LEA BX,[AX]
, 等同于 MOV BX,AX
; LEA BX,TABLE
等同于 MOV BX,OFFSET TABLE
. 但有时不能直接使用 MOV
代替:
比如: LEA AX,[SI+6]
不能直接替换成: MOV AX,SI+6
; 但可替换为:
MOV AX,SI
ADD AX,6
两步完成.
语法:
lea <mem, <reg32
例子:
lea (%ebx,%esi,8),%edi #EBX+8*ESI的值被移入到了EDI
lea val(,1), %eax #val的值被移入到了EAX
逻辑运算指令
add
整数相加
add
指令将两个参数相加, 然后将结果存放到第二个参数中. 注意, 参数可以是寄存器,但参数中最多只有一个内存单元. 这话有点绕, 我们直接看语法 :
语法:
add <reg, <reg
add <mem, <reg
add <reg, <mem
add <con, <reg
add <con, <mem
例子:
add $10, %eax #EAX中的值被设置为EAX+10
addb $10, (%eax) #往 EAX 中的值所代表的内存单元地址加上1个字节的数字10
sub
整数相减
sub
指令将第二个参数的值与第一个相减, 就是后面那个减去前面那个, 然后把结果存储到第二个参数. 和 add
一样, 两个参数都可以是寄存器, 但两个参数中最多只能有一个是内存单元.
语法:
sub <reg, <reg
sub <mem, <reg
sub <con, <reg
sub <con, <mem
例子:
sub %ah, %al #AL被设置成AL-AH
sub $216, %eax #将EAX中的值减去216
inc, dec
自增, 自减
inc
指令让它的参数加 1, dec
指令则是让它的参数减去 1.
语法:
inc <reg
inc <mem
dec <reg
dec <mem
例子:
dec %eax #将EAX中的值减去1
incl var(,1) #将var所代表的32位整数加上1
imul
整数相乘
imul
指令有两种基本格式 : 第一种是 2 个参数的 ( 看下面语法开始两条 )# 第二种格式是 3 个参数的 ( 看下面语法最后两条 ).
2 个参数的这种格式, 先是将两个参数相乘, 然后把结果存到第二个参数中. 运算结果 ( 即第二个参数 ) 必须是一个寄存器.
3 个参数的这种格式, 先是将它的第 1 个参数和第 2 个参数相乘, 然后把结果存到第 3 个参数中, 当然, 第 3 个参数必须是一个寄存器. 此外, 第 1 个参数必须是一个常数.
语法:
imul <reg32, <reg32
imul <mem, <reg32
imul <con, <reg32, <reg32
imul <con, <mem, <reg32
例子:
imul (%ebx), %eax #将EAX中的32位整数,与EBX中的内容所指的内存单元,相乘,然后把结果保存到EAX中
imul $25, %edi, %esi #ESI被设置为EDI * 25
idiv
整数相除
idiv
只有一个操作数, 此操作数为除数, 而被除数则为 EDX : EAX 中的内容 (一个64位的整数), 除法结果 ( 商 ) 存在 EAX 中, 而所得的余数存在 EDX 中.
语法:
idiv <reg32
idiv <mem
例子:
idiv %ebx #用EDX:EAX的值除以EBX的值。商存放在EAX中,余数存放在EDX中
idivw (%ebx) #将EDX:EAX的值除以存储在EBX所对应内存单元的32位值。商存放在EAX中,余数存放在EDX中
and, or, xor
按位逻辑 与, 或, 异或 运算
这些指令分别对它们的参数进行相应的逻辑运算, 运算结果存到第一个参数中.
语法:
and <reg, <reg
and <mem, <reg
and <reg, <mem
and <con, <reg
and <con, <mem
or <reg, <reg
or <mem, <reg
or <reg, <mem
or <con, <reg
or <con, <mem
xor <reg, <reg
xor <mem, <reg
xor <reg, <mem
xor <con, <reg
xor <con, <mem
例子:
and $0x0F, %eax #只留下 EAX 中最后 4 位数字 (二进制位)
xor %edx, %edx #将 EDX 的值全部设置成 0
not
逻辑位运算 非
对参数进行逻辑非运算, 即翻转参数中所有位的值.
语法:
not <reg
not <mem
例子:
not %eax #将EAX的所有值翻转
neg
取负指令
取参数的二进制补码负数. 直接看例子也许会更好懂.
语法:
neg <reg
neg <mem
例子:
neg $eax #EAX >> -EAX
shl, shr
按位左移或者右移
这两个指令对第一个参数进行位运算, 移动的位数由第二个参数决定, 移动过后的空位拿 0 补上.被移的参数最多可以被移 31 位. 第二个参数可以是 8 位常数或者寄存器 CL. 在任意情况下, 大于 31 的移位都默认是与 32 取模.
语法:
shl <con8, <reg
shl <con8, <mem
shl %cl, <reg
shl %cl, <mem
shr <con8, <reg
shr <con8, <mem
shr %cl, <reg
shr %cl, <mem
例子:
shl $1, %eax #将EAX的值乘以2(如果最高有效位是0的话)
shr %cl, %ebx #将EBX的值除以2n,其中n为CL中的值,运算最终结果存到EBX中
流程控制指令
x86 处理器有一个指令指针寄存器 ( EIP ), 该寄存器为 32 位寄存器, 它用来在内存中指示我们输入汇编指令的位置.
我们使用符号 <label 来当作程序中的标签. 通过输入标签名称后跟冒号, 可以将标签插入 x86 汇编代码文本中的任何位置. 例如 :
mov 8(%ebp), %esi
begin:
xor %ecx, %ecx
mov (%esi), %eax
jmp
跳转指令
将程序跳转到参数指定的内存地址, 然后执行该内存地址的指令.
语法:
jmp <label
例子:
jmp begin #跳转到大了"begin"这个标签的地方
jcondition
有条件的跳转
这些指令是条件跳转指令, 它们基于一组条件代码的状态, 这些条件代码的状态存放在称为机器状态字 ( machine status word ) 的特殊寄存器中. 机器状态字的内容包括关于最后执行的算术运算的信息. 例如, 这个字的一个位表示最后的结果是否为 0. 另一个位表示最后结果是否为负数. 基于这些条件代码, 可以执行许多条件跳转. 例如, 如果最后一次算术运算结果为 0, 则 jz
指令就是跳转到指定参数标签. 否则, 程序就按照流程进入下一条指令.
语法:
je <label #相等的时候跳转
jne <label #当不想等的时候跳转
jz <label #当最后结果为0的时候跳转
jg <label #当大于的时候跳转
jge <label #当大于等于的时候跳转
jl <label #当小于的时候跳转
jle <label #当小于等于的时候跳转
例子:
cmp %ebx, %eax
jle done #如果EAX的值小于等于EBX的值,就跳转到“done”标签,否则就继续执行下一条指令
cmp
比较指令
比较两个参数的值, 适当地设置机器状态字中的条件代码. 此指令与sub指令类似, 但是cmp不用将计算结果保存在操作数中.
语法:
cmp <reg, <reg
cmp <mem, <reg
cmp <reg, <mem
cmp <con, <reg
例子:
cmpb $10, (%ebx)
jeq loop
#如果EBX的值等于整数常量10,则跳转到标签“loop”的位置
call, ret
子程序调用与返回
这两个指令实现子程序的调用和返回. call
指令首先将当前代码位置推到内存中硬件支持的栈内存上 ( 请看 push
指令 ), 然后无条件跳转到标签参数指定的代码位置. 与简单的 jmp
指令不同, call
指令保存了子程序完成时返回的位置. 就是 call
指令结束后, 返回到调用之前的地址.
ret
指令实现子程序的返回. 该指令首先从栈中取出代码 ( 类似于 pop
指令 ). 然后它无条件跳转到检索到的代码位置.
语法:
call <label
ret
1.4 X64汇编基础
导语
x86-64 (也被称为 x64 或者 AMD64) 是 64 位版本的 x86/IA32 指令集. 以下是我们关于 CS107 相关功能的概述.
寄存器Registers
下图列出了常用的寄存器 ( 16个通用寄存器加上 2 个特殊用途寄存器 ).
每个寄存器都是 64 bit 宽, 它们的低 32, 16, 8 位都可以看成相应的 32, 16, 8 位寄存器, 并且都有其特殊名称.
%rsp 栈指针
%rax 函数返回值
寻址模式 Addressing modes
正由于它的 CISC 特性, X86-64 支持各种寻址模式. 寻址模式是计算要读或写的内存地址的表达式. 这些表达式用作mov
指令和访问内存的其它指令的来源和去路. 下面的代码演示了如何在每个可用的寻址模式中将 立即数 1 写入各种内存位置 :
movl $1, 0x604892 #直接写入, 内存地址是一个常数
movl $1, (%rax) #间接写入, 内存地址存在寄存器 %rax 中
movl $1, -24(%rbp) #使用偏移量的间接写入
#公式 : (address = base %rbp + displacement -24)
movl $1, 8(%rsp, %rdi, 4) #间接写入, 用到了偏移量和按比例放大的索引 ( scaled-index )
#公式 : (address = base %rsp + displ 8 + index %rdi * scale 4)
movl $1, (%rax, %rcx, 8) #特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设偏移量 ( displacement ) 为 0
movl $1, 0x8(, %rdx, 4) #特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设基数 ( base ) 为 0
movl $1, 0x4(%rax, %rcx) #特殊情况, 用到了按比例放大的索引 ( scaled-index ), 假设比例 ( scale ) 为0