栈溢出--解题二三事

1、在python中如 \x62,表示一个占一个字节的十六进制数,采用utf-8编码。

2、大多数程序是小端顺序,就是说输入一个数据,如果是分开输入时(比如要一个字节,一个字节的按字节分开进行输入十六进制的数,而他们本身合起来是一个具体的数,那么此时输入进去,就需要把小的值放在前面先写,大的值放后面);而大端顺序则是大的值写前面,与平常一样的顺序写

因为小端顺序,就是需要从低地址往高地址填充数据,所以先输入的是会放在低地址的数,后输入的才是高地址的数

在Linux中LSB文件为小端顺序,MSB为大端顺序,

3、IDA

(1)F5进行反汇编

(2)ida 中按下F12+SHIFT,可以查看string windows,查看一些关键字符串信息

(3)Alt+b可以进行搜索字符串

(4)在反汇编窗口中,点击函数按下X,可以查看交叉引用

(5)TAB键可以切换汇编与伪代码

(6)**;** 键可以为当前指令添加全文交叉引用的注释

(7)n可以定义或修改名称,通常用来标注函数名

(8)g可以跳转到任意地址

(9)Esc可以返回到跳转前的位置

(10)D可以分别按字节、字、双字显示数据

(11)A可以按 ASCII 显示数据

(12)/为伪C代码添加注释

ida的伪代码含义:

sub_ 指令和子函数起点

locret_ 返回指令

loc_ 指令

off_ 数据,包含偏移量

seg_ 数据,包含段地址值

asc_ 数据,ASCII字符串

byte_ 数据,字节(或字节数组)

word_ 数据,16位数据(或字数组)

dword_ 数据,32位数据(或双字数组)

qword_ 数据,64位数据(或4字数组)

flt_ 浮点数据,32位(或浮点数组)

dbl_ 浮点数,64位(或双精度数组)

tbyte_ 浮点数,80位(或扩展精度浮点数)

stru_ 结构体(或结构体数组)

algn_ 对齐指示

unk_ 未处理字节

4、利用字符串程序输入溢出数据造成栈溢出的时候不要包含\x00(也就是所谓的空字符’\0’),否则向程序传入溢出数据时会造成截断

5、遇到不确定的地址时而最后地址的末端是我们需要到的shell的起始地址,可以填入’\x90’,对应的机器码指令是NOP(No Operation),让CPU什么都不做,跳转到下一条命令,但是这个应用的前提是该处代码具有可执行性

6、在一起定义的两个全局变量,在内存的中位置是相邻的。如果一个全局变量被破坏了,不妨先查查其前后相关变量的访问代码,看看是否存在越界访问的可能。

7、有一个有意思的东西:在有的pwn题目中,我可以很正常的使用recvuntil()或者sendlineafter()去接收字符串,然而有的却不行;有的题目可以不用接收字符串直接输入payload(一开始认为是因为程序是在自己运行,刚好溢出点又在第一次输入的地方,所以可以),然而却碰到有些却要我接收信息才可以进行payload的输入

回复 :最好是进行交互,之前可能是因为环境有些问题

8、遇到一个对齐的问题:关于使用system函数进行跳转出现报错的原因

回复:system 获得shell,是要求有栈对齐的

9、劫持栈帧到 .bss 段上时,要注意一下是否可能会将 .got.plt 表覆盖,导致程序无法正常执行

10、承接第4点吧,我们避免输入\x00导致程序截断,同时也要注意程序是否自身有着\x00,导致了截断

11、遇到strcat、strcpy、strcmp、strlen等函数的阻碍时,由于这些函数遇到’\0’将会停止,那么只要将其中的某个变量的第一个字符设为’\0’即可快速简单的绕过这些函数

12、当用puts函数泄露got表的地址,使用u64解包时,由于u64解包的条件是需要字符串的长度为8个字节,而地址的长度顶多6个字节那么长(高地址填充0,但是我们并不能接收到),此时就需要使用这样的形式进行接收:u64(p.recv(6).ljust(8,’\0’)) (’\0’写成’\x00’也行,毕竟在内存中存储情况是一样的还有**u64(p.recvuntil(‘\x7f’).ljust(8,’\x00’))**)

13、一个大多数人应该都有错误的认知:认为溢出只要到如gets函数就能实现溢出了,所以下面的代码就不用看,这是绝对错误的!首先,我们通过栈溢出改变的只是返回地址的内容,想要到达返回地址,整个的函数执行流必须进行完,才会发生退栈,从而利用返回地址,所以下方的代码也要处理

14、执行strcmp、strncmp、strcasecmp的时候,rdx 会被设置为将要被比较的字符串的长度

15、system的参数不一定是要/bin/sh才可以,只有sh也是可以的。而且sh可以从fflush()这个函数的名称中获取,直接查找ida或者ROPgadget –binary 文件名 –string ‘sh’查询(不一定都能成功)

有时候 system 参数会出现截断问题,写成;sh\x00||sh&&sh

16、当远程 docker 过滤空格和 cat 时,将无法使用cat flag获取到flag,此时我们可以用more<flag或者base64<flag获取flag,其中的<可以代替空格的功能,第一个more<flag 与cat一样都是输出文件内容,而base64是把flag的内容转换成base64编码

1
tacMATHJAX-SSR-09flag

<、<>可以代替空格

17、64位的文件有时候不一定会存在rbp的调用,不存在leave指令,这时候我们只需要去关注rsp的位置即可,因为ret指令代表着pop rip,把栈顶的内容弹出给rip,所以锁定rsp的位置,就可以劫持rip

18、有时候在我们输入时,无法通过recvuntil去卡到输入的点,可以使用sleep()函数,让线程休眠,让对面的程序跑到我们要到点,再进行输入,方可利用成功(个人猜测)

19、64位下的linux的系统调用号(使用syscall调用,调用号是保存到rax中,函数传参依次存入RDI,RSI,RDX,RCX,R8,R9)

0(0x0) sys_read

1(0x1) sys_write

2(0x2)sys_open

3(0x3)sys_close

9(0x9)sys_mmap

15(0xF) sys_rt_sigreturn

37(0x25)sys_alarm

59(0x3B) sys_execve

60(0x3C)sys_exit

62(0x3E)sys_kill

32位下linux系统调用号(使用int 80h,调用号保存在eax中,执行完函数的返回值也保存在eax里面),函数其他参数依次存入ebx,ecx,edx中

1(0x1)sys_exit

3(0x3)sys_read

4(0x4)sys_write

5(0x5)sys_open(文件名,0,0)

11(0xB) sys_execve

20、使用execve获取shell时,其参数要设置为execve("/bin/sh",0,0)

21、劫持rip时,如果觉得在ida中的栈查看覆盖偏移量是对的,却没能获取shell时,去看看汇编代码,可能这时候出题人故意设置了返回代码不是平常的 leave ret这两个,被修改为其他的代码了,导致退栈时候rsp指向地方的不是在rbp的下面,这时候我们要去gdb里面下断点在返回的指令前,查看rbp具体指向的地方

22、gdb 文件名 core可以调试程序中断而生成的核心转储文件,进行深入查看中断原因

23、当题目已给出libc时,可以直接用libc显示出函数在其内的偏移地址,命令格式如下:(也可以直接调用libc文件,libc = ELF(“./文件名”))

1
objdump -T ./libc-2.23.so | grep system #显示system的libc地址

24、静态链接题目多半考虑 ret2syscall,或者看有没有 mprotect 函数,修改段的执行权限,注入shellcode

25、libc 中 environ 存储了栈上环境变量的特性,可以利用这个特效来泄露出栈上的地址,脚本写为 libc.sym[“environ”]

26、scanf 读取过长的输入时会使用 malloc_hook 分配 chunk,也就是如果能修改__malloc_hook为one_gadget的话,就能getshell

27、got表劫持:常见劫持put(buf)、free(buf)、atoi(buf)、atol(buf)、strlen(buf)

28、当scanf的参数不是%s时候,是u,d等数字,输入数字是使用str()即可,不用p64()。

因为输入数字是就是使用数字输入,如果使用了p64()将会变成二进制数,例如字符’1’实际上是二进制的0x31

29、使用execv(“/bin/sh”, 0)getshell需要两个参数,比execve少一个

另外:2.29之后的libc直接调用system可能会出问题,但是劫持__free_hook为system没问题

30、碰到静态编译的题目时,有两种方法:

  • 使用ROPgadget内置的功能,可以自动生成rop链,调用系统函数getshell,命令:

    ROPgadget --binary 文件名 --ropchain

  • 使用mprotect函数修改.bss段执行权限,把shellcode写入.bss段中执行而getshell

31、格式化字符串漏洞:当要往某个地址上写入地址时,格式为:目标addr + ‘%’ + str(addr - 已写的长度) + “c%a(a是偏移量)$hn” 这边一般使用hn写两个字节,或者hhn写一个字节,这样不容易造成timeout,当然前提是格式化漏洞给的长度够长

已写长度:printf打印出来的字符长度,地址数值大的写后面(就能直接减去前面已经写的偏移量)

64位,因为地址必定含有’\x00’,会产生截断,所以往目标地址写入值,要把地址放在最后面,不然打印的字符被截断,n就没办法发生作用

32、对于劫持rip的理解:首先我们传递给rip的是某个地址,其次这个地址指向的是一段有可执行权限的地址,最后这个地址上存在着指令(机器码)rop就是还需要ret来衔接进行不断地更换rip的值

33、linux中,程序在加载的时候,会依次调用init.array数组中的每一个函数指针;在结束的时候,依次调用fini.array中的每一个函数指针。一般用在当程序出现格式化字符串漏洞,但是至少需要写两次才能完成攻击。

  • 可以考虑改写fini.array中的函数指针为main函数地址,可以再执行一次main函数。(或者在格式化字符串漏洞后面存在有其他函数的调用,也可以考虑修改这个函数的got表)
  • 一般来说,这个数组的长度为1,也就是说只能写一个地址

获取fini.array地址有两种方式:1、fini_array = elf.sym["__init_array_end"] 2、使用readelf -s 文件名命令去查找__init_array_end的地址

34、如果把0x80000000赋值给a,而a = -a,那么a还是等于0x80000000

35、用libc寻找字符串/bin/sh地址,写为binsh = libc_base + next(libc.search(b’/bin/sh’))

36、scanf(buf,buf),如果是以这样的形式出现输入,因为第一个buf是格式化字符串,scanf使用了我们的输入作为格式化字符串,因此会存在格式化字符串漏洞,计算偏移需要通过gdb调试(必须在相应的版本下调试)

%n$s(n是偏移,s是输入的形式以字符串,所以我们输入的地方是以偏移点作为指针输入的)

37、当程序存在close(1)以及close(2):代表关闭了linux里面的标准输出(1)和标准错误(2),所以即使已经getshell了,我们是看不到输出的,所以这时候输入exec 1>&0就可以让标准输出的文件描述符重定向为0,而0没被关闭(0,1,2是默认开启的),才能看到输出

38、glibc版本仅限于libc-2.26及以下:当flag保存在栈上、bss等地方时,可以通过修改栈上main函数的argv[0](文件路径),改为flag的地址,然后stack_chk_fail会打印环境变量的第一个值,即可打印出flag,高版本增加了限制,不会再打印出文件路径

39、当遇到一些题目需要在shellcode前面加上’\x00’才能绕过时,因为’\x00’是会影响到shellcode解析的,所以要让’\x00’凑成有意义的指令却又不能影响到shellcode(底下的机器码在32位和64位中都是一样的)

1
2
3
4
5
6
7
00 40 00                 add    BYTE PTR [rax+0x0],  al
00 41 00 add BYTE PTR [rcx+0x0], al
00 42 00 add BYTE PTR [rdx+0x0], al
00 43 00 add BYTE PTR [rbx+0x0], al
00 45 00 add BYTE PTR [rbp+0x0], al
00 46 00 add BYTE PTR [rsi+0x0], al
00 47 00 add BYTE PTR [rdi+0x0], al

40、栈题的调试一直都不知道怎么才能准确的停止在某个地方,现在终于知道了

img

在上面的gdb.attach(p)就是运行脚本到此位置时打开gdb调试,然后在gdb框中输入c,就会运行到pause()处,然后我们就可以进行调试。这样就能准确的停在你想要的位置

41、mprotect修改内存权限的第一个参数必须是起始地址,修改的长度必须是页的倍数

42、int snprintf(char dest_str, size_t size, const char format,…);**

将可变个参数(…)按照format格式化成字符串,然后将其复制到str中。返回值为欲写入的字符串长度

这个函数有个漏洞,因为返回值是欲写入的字符串长度,也就是说,被复制的字符串很长,又能被加载(比如%s)那返回值就可能很大,如果以这个作为某种输入的值就有产生溢出

43、系统是通过 cs 段寄存器来判断程序是64位还是32位,cs == 0x23代表32位模式,cs == 0x33代表64位模式,而cs寄存器可以通过retfq汇编指令来修改

retfq有两步操作:ret以及set cs,执行retfq会跳转到 rsp 同时将cs设置为[rsp+0x8],我们只需要事先在ret位置写入32位的shellcode就可以执行了,但是这里有一点需要注意的是,retfq跳转过去的时候程序已经切换成了32位模式,所以地址解析也是以32位的规则来的,所以如原先的rsp = 0x7ffe530d01b8会被解析成esp = 0x530d01b8

44、在大多数libc中 read 与 write 的 symbols 只有一个字节不同,所以在一类没有泄露函数的栈题目中,可以考虑覆盖read的got表的最后一字节,变成write从而泄露libc

45、如果got表可改,有种思路是可以修改某个函数,让他指向syscall,在有的函数这种偏移仅仅只需要更改最后一字节即可。

46、在Linux下,有一种线程局部存储(Thread Local Storage)机制,简称TLS。它主要存储着一个线程的一些全局变量,其中包括uintptr_t stack_guard,我们熟知的canary来源于此,因此可以修改uintptr_t stack_guard,来绕过canary。这种方法可以避免有时候打印函数使用printf、puts,而出现’\x00’字节截断无法泄露canary,但却需要有次任意写才能进行修改。

输入tls, 可以找到其地址。

image-20220901214930484

47、Linux 下存在像gettimeofday(获取当前时间)的系统调用,但又只进行读取数据这种较为安全的操作。因此将其直接映射到用户空间上,像这种 Linux 内核在用户空间映射一个包含一些变量及一些系统调用的实现的内存页被称为vsyscall。而vsyscall可以简单看作是一个ret

其中一个的固定地址为0xFFFFFFFFFF600000,不受 ASLR 影响,可以稳定调用。

48、非预期:有些可以进入到远程环境的内核题目,可以利用 gcc 命令编译 flag 文件,然后会因为报错而输出 flag 文件内容。

查看评论