pwn学习笔记
知识点
栈
栈是一种数据结构,遵循后进先出的原则(Last in First Out),主要有压栈(push)与出栈(pop)两种操作
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。
在栈中,esp保存栈帧的栈顶地址,ebp保存栈帧的栈底地址
程序的栈是从进程地址空间的高地址向低地址增长的
栈溢出
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
栈溢出的前提是:程序向栈上写入数据;数据的长度不受控制
最简单的栈溢出就是通过溢出,覆盖程序的返回地址,将返回地址覆盖为system(‘/bin/sh’)的地址
ret2系列
通过栈溢出覆盖ebp从而改写返回值,让程序返回到后门函数从而getshell.
栈迁移
栈迁移相当于控制程序执行的控制流,通过修改ebp的值和执行两次leave;ret指令来修改eip。一般在栈溢出长度不够写下完整payload时使用。
使用的条件是能够泄露ebp且溢出长度至少能够覆盖ebp。
原理:
payload:overflow + target_addr + leave_ret_addr
leave和ret指令的本质如下:
1 | leave: mov esp,ebp; |
修改完ebp的值为目标地址后,运行到函数结尾第一次执行leave指令,实现将ebp弹到目标地址,然后执行第一次ret指令,由于我们将返回地址修改为了leave_ret的地址,所以第一次ret指令会返回到leave_ret处执行第二次,而第二次leave指令,mov esp,ebp后esp也被弹到了目标地址,而pop ebp后esp指向了下一个内存单元。最后ret指令使esp的内容进入了eip。所以只要让第二次leave后的esp指向我们的后门函数即可成功执行后门函数。
格式化字符串漏洞
实现原理:
利用printf函数的格式化字符串%n实现对内存的改写。
1 | printf('xxxx%n',p) //将%n之前的字符串的长度赋值给指针p |
利用方法:
出现printf(输入)的情况下,首先通过格式化字符串“%x”来找到输入在栈上的位置,然后输入需要改变的内存的地址和%x$n来改变需要更改的内存的值(x是输入在栈上的位置)。
做题记录
ret2text
[buu]bjdctf_2020_babystack
使用scanf输入的长度,且后续read时调用长度时用的是无符号整型,那么read处就可以栈溢出。
发现有写好的后门函数来getshell,直接栈溢出让main函数返回到这个函数的地址即可。
payload:
1 | sh.sendline(b'-2') |
然后就成功getshell了。
[buu]get_started_3dsctf_2016
主函数中看到gets函数,可以栈溢出。
函数列表中看到了getflag函数,同时这个程序是静态编译的,所以考虑ret2text。
需要两个参数都满足if中的条件即可得到flag.
payload如下:
1 | func = 0x80489A0 |
函数地址与参数之间需要一个返回地址,它可以是0,但是不能不填,否则程序会将参数当作返回地址从而导致getshell失败。此处的地址是动调后得到的main函数运行时的实际返回地址,目的是让程序正常退出。如果填别的会失败。
溢出的部分没有+4覆盖ebp是因为main函数没有ebp。
ret2libc
[buu]ciscn_2019_c_1
gets函数可以栈溢出。本题没有能直接调用的system函数,也没有可写可执行的段,故考虑使用ret2libc的方式getshell.
利用puts泄露libc地址,然后跟libc中的地址计算得出基址,然后在用基址和libc中system的地址调用system函数,从而getshell。
回到这题,它对输入进行了加密,但是由于使用的是while的结构,在最开始会先ret一次,这个时候就会直接执行我们的payload中想让他执行的内容,所以不用反向加密payload.
payload:
1 | file = ELF('./elf') |
[GeekChallenge2024]买黑马喽了么
开启了NX保护和随机硬件地址,同时text段中没有可以直接利用的函数,故考虑ret2libc。Write函数处可以栈溢出。由于开启了随机硬件地址,所以还需要获取这个附件本身的基址。
view函数中else分支处对balance取了地址,同时这里printf的格式化字符串是str1,在上方可以修改,将%x改为%p即可让这个分支输出运行时的balance地址,减去静态地址即可得到基址。
修改str1需要让balance的值大于它的初始值,但是操作中并没有能让他增加的部分。注意到判断时balance的类型是无符号整型,可以在shop函数中吧balance的值减少到负数来让这个无符号整型大于256.
可以得到第一部分exp:
1 | for i in range(8): |
获得程序基址。
然后就可以利用基址调用程序的puts函数来泄露libc,然后调用libc中的system函数来getshell.
完整exp:
1 | from pwn import * |
格式化字符串漏洞
[buu]jarvisoj_fm
x的值是3,把它变成4就可以getshell。
第十行的printf处存在格式化字符串漏洞,可以利用。
构造如下payload得到buf在栈上存储的位置。
1 | sh.sendline(b'aaaa 1:%x 2:%x 3:%x 4:%x 5:%x 6:%x 7:%x 8:%x 9:%x 10:%x 1:%x 2:%x 3:%x 4:%x 5:%x 6:%x') |
得到如下回显:
1 | b'aaaa 1:ff892aec 2:50 3:0 4:f7ffb000 5:f7ffb918 6:ff892af0 7:ff892be4 8:0 9:ff892b84 10:50 1:61616161 2:3a312020 3:32207825 4:2078253a 5:3!\n' |
可以看到我们的输入在第11个内存空间上。
构造如下payload:
1 | x_addr = 0x804A02C |
原理就是格式化字符串%n可以改写内存的值,%11$n所对应的参数是往后11个内存空间,即我们的输入,也就是x的地址;而x的地址是一个32位的十六进制字符串,转换为unsigned char的话长度是4,从而实现将x的值改成4.
就能成功getshell了。
完整exp:
1 | from pwn import * |
[buu] [第五空间2019 决赛]PWN5
程序读取了一个随机数,之后将输入与随机数进行了对比,相同直接getshell.
对栈进行了检查,如果栈不平衡程序会死掉。第21行存在格式化字符串漏洞。所以考虑利用格式化字符串改变随机数的值。
首先寻找输入在栈上的位置:
1 | sh.sendline(b'aaaa 1:%x 2:%x 3:%x 4:%x 5:%x 6:%x 7:%x 8:%x 9:%x 10:%x 1:%x 2:%x 3:%x 4:%x 5:%x 6:%x') |
发现在第十个内存空间上。
随机数一共32位,所以需要改写4个地址的值。
构造payload如下:
1 | payload = p32(0x804C044) + p32(0x804C045) + p32(0x804C046) + p32(0x804C047) |
这里直接将随机数每一位的值改写成4个十六进制字符串的值,即0x10,最后rand的值被改为了0x10101010.
完整exp如下:
1 | from pwn import * |
栈迁移
[buu]ciscn_2019_es_2
read函数处可以溢出,但是溢出的长度不够写下函数返回地址和参数,故考虑栈迁移。
这里能看到疑似后门函数,但是只是echo flag,而且这个字符串存储的rodata段并没有写的权限,所以这个函数无法利用。
考虑栈迁移具体用法,可以迁移到输入的开头,这样可以在输入的溢出前的部分写下system函数的地址,返回地址,参数,shell的地址和‘/bin/sh’字符串(程序中没有现成的/bin/sh)。
第一步要先泄露ebp。此处输出函数使用的是printf,所以只要把我们的输入和ebp之间的空间填满,printf函数就会把我们的输入跟ebp的内容一起输出。
1 | overflow1 = b'1'*0x28 |
即可得到ebp的内容(不是ebp的地址)
ebp存放的是一个地址,这个地址到我们输入开头的地址的相对位置是一定的,所以只要动调,计算得到这个偏移值就可以得到我们输入的地址了。
得到输入地址后将栈迁移到这个地方就可以getshell了。
1 | system = 0x8048400 |
完整exp:
1 | system = 0x8048400 |