在x64架构下execve和sys_rt_sigreturn的系统调用就是59(0x3B)和15(0xF),可以直接用execve(“/bin/sh”,0,0)或者有SROP的方法伪造signal frame构造execve(“/bin/sh”,0,0)再利用sys_rt_sigreturn来getshell
生成Shellcode以及ASLR作用
ASLR主要影响stack、share library、heap几个部分。
当ASLR=0时,栈和堆的地址都不会改变。
当ASLR=1时,栈的地址变了,堆的地址不变。
当ASLR=2时,栈和堆的地址都变了。
生成shellcode
32位:
print(shellcraft.sh()) 生成的是shell的汇编代码
print(asm(shellcraft.sh())) 生成的是shell的机械码
64位:
context.arch=“amd64”
print(shellcraft.sh()) 生成的是shell的汇编代码
print(asm(shellcraft.sh())) 生成的是shell的机械码
gcc -fno-stack-protector -z execstack -no-pie -g -o ret2 ret2.c
刷题知识点总结1-1
python3进行编译此条代码会出现报错:
1 | payload = 'a' * 4 + p64(1853186401) |
报错信息:
1 | File "mex.py", line 4, in <module> |
报错翻译——字符类型不匹配不能进行拼接。
百度搜索相关资料,找到解决方法—添加后缀(**.decode(“iso-8859-1”)**)进行转码。
1 | payload = 'a'*4 + p64(1853186401).decode("iso-8859-1") |
1 | payload = 'a' * 23 + p64(0x401185).decode("iso-8859-1") + p64(0x401186).decode("iso-8859-1"); |
要使用额外地址的原因是栈地址需要对齐才能执行system。
(64位ubuntu18以上系统调用system函数时是需要栈对齐的。再具体一点就是64位下system函数有个movaps指令,这个指令要求内存地址必须16字节对齐)
解决方法:
1 | from pwn import * |
1 | from pwn import * |
p32、p64所做的是,将一个整形数据进行hex转换后,将这个进行转换成byte型,并进行小段输入。
(Hex)
1 | Hex就是16进制,本质上是将字节数组转化为16进制,然后用字符串的形式表现出来。 |
系统调用号
系统调用号(rax值)
在线查询链接:https://syscalls.w3challs.com/
分为32位和64位,链接中还有arm、mips等架构的系统调用号。
原链接没有arm64的,找到新的查询链接:https://syscall.sh/,此链接没有mips的系统调用
32位
1 | cat /usr/include/asm/unistd_32.h |
64位
1 | cat /usr/include/asm/unistd_64.h |
系统调用类型
系统调用(syscall)的类型
系统调用是操作系统提供的接口,允许用户程序请求内核执行特定操作。以下是一些常见的系统调用示例(以 Linux 系统为例):
文件操作
sys_open
: 打开文件。sys_read
: 读取文件内容。sys_write
: 写入数据到文件。sys_close
: 关闭文件描述符。sys_lseek
: 移动文件指针。
进程控制
sys_fork
: 创建子进程。sys_execve
: 执行新程序。sys_waitpid
: 等待子进程结束。sys_exit
: 退出进程。sys_getpid
: 获取当前进程ID。
内存管理
sys_mmap
: 映射文件或设备到内存。sys_munmap
: 解除映射内存区域。sys_brk
: 调整数据段末尾。
网络操作
sys_socket
: 创建网络套接字。sys_bind
: 绑定套接字到地址。sys_listen
: 监听连接请求。sys_accept
: 接受连接请求。sys_recv
: 接收数据。sys_send
: 发送数据。
时间与日期
sys_time
: 获取当前时间。sys_gettimeofday
: 获取当前时间和微秒。
目录操作
sys_opendir
: 打开目录。sys_readdir
: 读取目录内容。sys_closedir
: 关闭目录流。
权限管理
sys_chown
: 更改文件所有者和组。sys_chmod
: 更改文件权限。
信号处理
sys_kill
: 发送信号到进程。sys_signal
: 设置信号处理函数。
设备管理
sys_ioctl
: 执行设备控制操作。
示例代码
以下是一个简单的示例,展示如何使用系统调用打开文件并写入内容:
1 | #include <unistd.h> |
栈基础知识2
函数调用约定通常规定如下几方面内容:
\1) 函数参数的传递顺序和方式
最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序(从左至右还是从右至左)。某些调用约定允许使用寄存器传参以提高性能。
\2) 栈的维护方式
主调函数将参数压栈后调用 被调函数体,返回时需将 被压栈的参数 全部弹出,以便将栈恢复到调用前的状态。该清栈过程可由 主调函数 负责完成,也可由 被调函数 负责完成。
\3) 名字修饰(Name-mangling)策略
又称函数名修饰(Decorated Name)规则。编译器在链接时为区分不同函数,对函数名作不同修饰。
若函数之间的调用约定不匹配,可能会产生堆栈异常或链接错误等问题。因此,为了保证程序能正确执行,所有的函数调用均应遵守一致的调用约定。
Windows下可直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等标识确定函数的调用方式,如int __stdcall func()。Linux下可借用函数attribute 机制,如int attribute((stdcall)) func()。
c++函数的缺省值:
c++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,在调用函数的时候,如果不写相应位置的参数,则调用的参数就为缺省值。
不同编译器产生栈帧的方式不尽相同,主调函数不一定能正常完成清栈工作;而被调函数必然能自己完成正常清栈,因此,在跨(开发)平台调用中,通常使用stdcall调用约定(不少WinApi均采用该约定)。
此外,主调函数和被调函数所在模块采用相同的调用约定,但分别使用C++和C语法编译时,会出现链接错误(报告被调函数未定义)。这是因为两种语言的函数名字修饰规则不同,解决方式是使用extern “C”告知主调函数所在模块:被调函数是C语言编译的。采用C语言编译的库应考虑到使用该库的程序可能是C++程序(使用C++编译器),通常应这样声明头文件:
1 | 1 #ifdef _cplusplus |
这样C++编译器就会按照C语言修饰策略链接Func函数名,而不会出现找不到函数的链接错误。
x86函数参数传递方法:
x86处理器ABI规范中规定,所有传递给被调函数的参数都通过堆栈来完成,其压栈顺序是以函数参数从右到左的顺序。当向被调函数传递参数时,所有参数最后形成一个数组。由于采用从右到左的压栈顺序,数组中参数的顺序(下标0N-1)与函数参数声明顺序(Para1N)一致。因此,在函数中若知道第一个参数地址和各参数占用字节数,就可通过访问数组的方式去访问每个参数。
栈帧地址未压入参数时为4(%ebp)
x86函数返回值传递方法
函数返回值可通过寄存器传递。当被调用函数需要返回结果给调用函数时:
\1) 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值。
\2) 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节。
\3) 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。
\4) 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。
eg:函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。
具体步骤可能为:
a.主调函数将显式的实参逆序入栈;
1 | #"逆序入栈" 通常是指在将一系列元素入栈(push)到栈(stack)中时,这些元素按照它们原始顺序的逆序进入栈。也就是说,最后一个元素首先入栈,然后是倒数第二个元素,依此类推,直到第一个元素最后入栈。这样做通常是为了在后续的出栈(pop)操作中,元素能够以原始顺序出现。 |
b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);
c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。
因此,在被调函数中完成返回值的赋值工作。****
注意:函数如何传递结构体或联合体返回值依赖于具体实现。不同编译器、平台、调用约定甚至编译参数下可能采用不同的实现方法(VC6编译器对于不超过8字节的小结构体,会通过EAX+EDX寄存器返回)。而对于超过8字节的大结构体,主调函数在栈上分配用于接收返回值的临时结构体,并将地址通过栈传递给被调函数;被调函数根据返回值地址设置返回值(拷贝操作);调用返回后主调函数根据需要,再将返回值赋值给需要的临时变量(二次拷贝)。实际使用中为提高效率,通常将结构体指针作为实参传递给被调函数以接收返回值(这种方法允许你修改结构体的内容,但并不能直接返回一个全新的结构体。如果你需要返回一个全新的结构体,你可能需要动态分配内存,但这会带来额外的复杂性和责任)。
\5) 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的。
函数返回值通过寄存器传递,无需空间分配等操作,故返回值的代价很低。基于此原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。但这会带来类型安全隐患,如函数定义时返回值为浮点数,而函数未声明或声明时未指明返回值类型,则调用时默认从寄存器EAX(而不是浮点数寄存器)中获取返回值,导致错误!因此在C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。
GCC使用系统的标准约定来传递参数。在一些机器上,前几个参数通过寄存器传递;在另一些机器上,所有的参数都通过栈传递。原本可在所有机器上都使用寄存器来传递参数,而且此法还可能显著提高性能。但这样就与使用标准约定的代码完全不兼容。所以这种改变只在将GCC作为系统唯一的C编译器时才实用。当拥有一套完整的GNU 系统,能够用GCC来编译库时,可在特定机器上实现寄存器参数传递。
在一些机器上(特别是SPARC),一些类型的参数通过“隐匿引用”(invisible reference)来传递。这意味着值存储在内存中,将值的内存地址传给子程序。
栈迁移
每条指令的作用:
push eip+4;
- 这条指令将当前指令指针寄存器(
eip
)加上 4 的值压入栈中。这里的eip+4
实际上是下一条指令的地址(假设没有其他的分支跳转或中断),因为eip
指向当前正在执行的指令的地址。这样做通常是为了将返回地址(即函数调用指令的下一条指令地址)保存到栈中,以便函数调用完成后可以正确地返回。
- 这条指令将当前指令指针寄存器(
push ebp;
- 这条指令将当前基指针寄存器(
ebp
)的值压入栈中。ebp
通常用于指向当前栈帧的基址,因此将其值保存下来是为了在函数调用完成后能够恢复到调用函数之前的栈帧状态。
- 这条指令将当前基指针寄存器(
mov ebp, esp;
- 这条指令将栈指针寄存器(
esp
)的值复制到基指针寄存器(ebp
)。这意味着函数的栈帧基址现在被设置为当前栈顶的位置。之后,函数可以通过ebp
寄存器访问其局部变量和参数。
- 这条指令将栈指针寄存器(
这段代码的整体作用是设置一个新的栈帧,使得函数可以在其局部变量和参数上进行操作,同时保留调用函数前的栈帧状态,以便在函数返回时能够恢复原始状态。
主要通过两次leave ret指令得到目的,最主要是要得到ebp值也就是栈底的地址,来更精确地进行两次leave,第一次的leave是输入后的自带leave,第二次的leave是我们自己导入的leave,做完之后就按着输入顺序一一调取按顺序进行。之后执行之前相当于一串函数,执行ret gift
main
的操作,执行gitf时进行压栈,rsp下移到main,gift结束时执行main。
32位模板:
1 | payload1 = p32(write_plt_addr) + p32(main_addr) + p32(1) //#此前为需要执行的命令 |
64位模板(仅参考):
1 | payload=(p64(gift)+p64(main)).ljust(0x30,b'\x00')# 依次是需要执行的命令进行ret2libc |
rbp=gift 图误
1 | leave=mov esp,ebp; pop ebp; |
栈溢出基础知识-1
在汇编语言层面,
这组通用寄存器以%e(AT&T语法)或直接以e(Intel语法)开头来引用,
eg: mov $5, %eax或mov eax, 5表示将立即数5赋值给寄存器%eax。
在x86处理器中:
EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。
ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;
EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。
注意:
EIP是个特殊寄存器,不能像访问通用寄存器那样访问它,即找不到可用来寻址EIP并对其进行读写的操作码(OpCode)。EIP可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)。
FP:frame pointer 栈帧指针,每个进程的栈空间为一帧,FP指向 当前进程栈空间的 栈底。
32位Windows,一个进程栈的默认大小是1M。
每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。
栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
使用栈帧的一个好处:
使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。
栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;
ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
称EBP为帧基指针, ESP为栈顶指针,并在引用汇编代码时分别记为%ebp和%esp.
从图中可以看出,函数调用时入栈顺序为
$$
实参N1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1N
$$
其中,主调函数将参数按照调用约定依次入栈(图中为从右到左),然后将指令指针EIP入栈以保存主调函数的返回地址(下一条待执行指令的地址)。
进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。
此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值。
本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。
ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。如此递归便形成函数调用栈。
若需在函数中保存被调函数保存寄存器(如ESI、EDI),则编译器在保存EBP值时进行保存,或延迟保存直到局部变量空间被分配。在栈帧中并未为被调函数保存寄存器的空间指定标准的存储位置。
内存地址从栈底到栈顶递减,压栈就是把ESP指针逐渐往地低址移动的过程。而结构体tStrt中的成员变量memberX地址=tStrt首地址+(memberX偏移量),即越靠近tStrt首地址的成员变量其内存地址越小。因此,结构体成员变量的入栈顺序与其在结构体中声明的顺序相反。
1 | 1 //StackFrame.c |
输出结果:
函数调用以值传递时,传入的实参(locMain13)与被调函数内操作的形参(para13)两者存储地址不同,因此被调函数无法直接修改主调函数实参值(对形参的操作相当于修改实参的副本)。为达到修改目的,需要向被调函数传递实参变量的指针(即变量的地址)。
此外,”[locMain1,2,3] = [0, 0, 3]”是因为对四字节参数locMain2调用memset函数时,会从低地址向高地址连续清零8个字节,从而误将位于高地址locMain1清零。
memset()函数介绍
1 | void *memset(void *str, int c, size_t n) |
解释:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
作用:是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
头文件:C中#include<string.h>,C++中#include
局部变量并不总在栈中,有时出于性能(速度)考虑会存放在寄存器中。数组/结构体型的局部变量通常分配在栈内存中。
若要确保两个对象在内存上相邻且前后关系固定,可使用结构体或数组定义。
函数调用时的具体步骤如下:
\1) 主调函数(caller)将被调函数(callee)所要求的参数,根据相应的函数调用约定,保存在运行时栈中。该操作会改变程序的栈指针。
注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。
\2) 主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。
\3) 若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。
\4) 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。
\5) 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。
\6) 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。
\7) 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。
\8) 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。
\9) 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。
*步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。*
压栈(push):栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。*
出栈(pop):栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节。
可见,压栈操作将寄存器内容存入栈内存中(寄存器原内容不变),栈顶地址减小;出栈操作从栈内存中取回寄存器内容(栈内已存数据不会自动清零),栈顶地址增大。栈顶指针ESP总是指向栈中下一个可用数据。
调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。
离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针)。
返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。*