【渗透测试之二进制安全系列】格式化漏洞揭秘(一)

这篇具有很好参考价值的文章主要介绍了【渗透测试之二进制安全系列】格式化漏洞揭秘(一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

相信学习过C语言的童鞋儿们,都有接触过比较基础的输入输出函数(例如,scanf和printf等),那么对于%s、%d、%f、%c、%x等格式化符号应该并不会感到陌生。学习过汇编语言,并且有逆向工程基础的童鞋儿们,应该都对C语言翻译成汇编语言代码的大概格式会有所了解!

我们应该知道,函数参数和函数内部的局部变量的值内容都是存储在栈中的。不知道”栈“这个名词的童鞋儿们可以去学习下数据结构中的”栈结构“,这是非常重要的基础知识!必须要理解并且熟练掌握!”栈结构“的”先进后出“原则,大家一定要深入理解!

对于函数参数而言,在32位操作系统中,最后一个参数最先入栈,第一个参数最后入栈,第一个参数最先出栈,最后一个参数最后出栈,正是所谓的”先进而后出”!在栈中数据排列的顺序中,栈中第一个函数参数数据下面的栈空间中存储着函数的返回地址,栈中函数返回地址下面的栈空间中存储着EBP寄存器的值内容。

注意,在32位操作系统中,函数参数值大小一般情况下为4个字节(即每次PUSH入栈的字节长度为4个字节)(但double、long long类型是个例外,遇到 double、long long 类型的函数参数,每个 double、long long 类型的函数参数会以PUSH两次的方式(高32位先入栈,低32位后入栈)将8字节(64位)长度数据入栈)。无论是字符型(char)还是整型(int),亦或者是地址类型(*)的函数参数,都会以4字节的数据长度大小入栈(PUSH <数据内容值>)!

数组本质上,是内存地址!结构体本质上,是内存地址!函数本质上,还是内存地址!

当遇到数组、结构体、函数类型的函数参数传递时,传递的也是内存地址,也就是(*)地址型数据!那么传递结构体类型的指针变量的值到函数中(指针变量作为函数参数)和传递结构体类型的普通变量到函数中(普通变量作为函数参数)会存在什么区别吗?

这里,我们要弄清楚,结构体变量的本质是什么!结构体类型的指针变量的本质是什么!

那么,结构体变量的本质是什么呢?是内存地址!内存地址!内存地址!重要的事情说三遍!

结构体类型的指针变量更像一个盘子,而盘子上摆放着结构体变量在内存区域中的起始地址(也可简称为“首地址”)

结构体类型的普通变量和结构体类型的指针变量作为函数参数进行传递时,传递的内容是相同的!

结构体类型的指针变量中存放的是结构体类型的普通变量在内存区域中的起始地址!

结构体类型的普通变量,直接代表着结构体类型的普通变量在内存区域中的起始地址!

根据值传递原则,由于结构体类型的普通变量和结构体类型的指针变量都指向了结构体类型的普通变量在内存区域中的起始地址,所以在使用结构体类型的普通变量和结构体类型的指针变量进行函数参数传递时,传递的值是相同的!此类原则,在传递数组型的普通变量和指向数组的指针变量时,同样的适用!对于初学者,应搞清楚数组、结构体、指针变量等概念的内容本质!这很重要!

那么,在64位操作系统中,函数参数又是在栈中如何进行存储的呢?

在64位操作系统中,函数参数的存储方式对比32位操作系统而言,是有明显变化的!

在64位操作系统中,部分函数参数的值内容被存储在寄存器中部分函数参数的值内容被存储在栈结构空间中!

64位操作系统中,不同编译器使用不同寄存器传递函数参数

MSVC编译器中,会使用RCX、RDX、R8、R9寄存器传递前四个函数参数其它函数参数仍然会利用栈空间结构以“先进后出”的方式进行函数参数传递!

GCC编译器中,会使用RDI、RSI、RDX、RCX、R8、R9寄存器传递前六个函数参数其它函数参数仍然会利用栈空间结构以“先进后出”的方式进行函数参数传递!

当然,总会有例外情况存在!对于浮点型的数据(float、double)不同编译器有时也会采用XMM系列寄存器来进行函数参数传递XMM系列寄存器128位长度的,与其它的寄存器不同,XMM系列的寄存器只能使用特定的SSE指令集对寄存器中的数据进行操作!

我们来看两段代码,这两段代码分别是C语言源码和反汇编代码。

先看C语言源码:

#include <stdio.h>

void show_1(char string[] ){
    printf("%s\n",string);
}

void show_2(char * string ){
    printf("%s\n",string);
}

int main() {
    char string[17] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','\0'};
    char * string_point = string;
    show_1(string);
    show_1(string_point);
    show_2(string_point);
    return 0;
}

接下来,再看反汇编代码:

; Attributes: bp-based frame

; void __cdecl show_1(char *string)
public show_1
show_1 proc near

string= qword ptr -8

; __unwind { // 55829E03C000
push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+string], rdi
mov     rax, [rbp+string]
mov     rdi, rax        ; s
call    _puts
nop
leave
retn
; } // starts at 55829E03D139
show_1 endp

; Attributes: bp-based frame

; void __cdecl show_2(char *string)
public show_2
show_2 proc near

string= qword ptr -8

; __unwind { // 55829E03C000
push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+string], rdi
mov     rax, [rbp+string]
mov     rdi, rax        ; s
call    _puts
nop
leave
retn
; } // starts at 55829E03D154
show_2 endp

; Attributes: bp-based frame

; int __fastcall main(int argc, const char **argv, const char **envp)
public main
main proc near

string= byte ptr -20h
string_point= qword ptr -8

; __unwind { // 55829E03C000
push    rbp
mov     rbp, rsp
sub     rsp, 20h
mov     rax, 3736353433323130h
mov     rdx, 6665646362613938h
mov     qword ptr [rbp+string], rax
mov     qword ptr [rbp+string+8], rdx
mov     [rbp+string+10h], 0
lea     rax, [rbp+string]
mov     rdi, rax        ; string
call    show_1
lea     rax, [rbp+string]
mov     [rbp+string_point], rax
mov     rax, [rbp+string_point]
mov     rdi, rax        ; string
call    show_1
mov     rax, [rbp+string_point]
mov     rdi, rax        ; string
call    show_2
mov     eax, 0
leave
retn
; } // starts at 55829E03D16F
main endp

发现了什么吗?注意直接将数组做为函数参数和将数组指针作为函数参数之间存在什么不同呢?

注意观察:

lea     rax, [rbp+string]
mov     rdi, rax        ; string
call    show_1

mov     rax, [rbp+string_point]
mov     rdi, rax        ; string
call    show_1

mov     rax, [rbp+string_point]
mov     rdi, rax        ; string
call    show_2

通过观察上面的代码,我们会发现!最终传递到RDI寄存器GCC编译器下,在64位系统环境使用RDI寄存器传递函数第一个参数的值)中的内容,都是string这个字符数组在内存中的起始地址(首地址)!也就是说,直接使用数组名和直接使用保存了数组首地址的指针变量来作为函数参数,其达到传参效果是几乎相同的!数组名和指向数组的指针变量都指向一个相同的内存地址!

话题说回本文正题,对于格式化漏洞,我们还要提一下函数的汇编语言实现方式!

我们知道 printf 函数,可以只传递一个参数(例如,纯字符串数据),也可以传递多个参数(例如,第一个函数参数为包含格式化符号的字符串数据,其它函数参数,则按照第一个函数参数中的格式化符号的类型与排列顺序进行相应的函数参数传递)。这些都是比较常规的 printf 函数用法。

但是,如果我们 printf 函数传递非常规内容呢?

例如,我们使用这样的方式(printf("%s");)来调用 printf 函数,会发生什么呢?!

前面,我们介绍了函数汇编语言实现方式,我们了解到,在32位系统中,将使用栈结构空间来进行“先进后出方式函数参数传递!我们使用汇编语言中的PUSH指令来实现函数参数入栈!那么,大家有没有思考过,printf 函数的参数来源于哪里?又存储在哪里呢?答案是:printf 函数参数存储在栈结构空间中!大家再思考一下,格式化符号“%s”的本质是什么?如何利用格式化符号”%s“在命令行环境中显示出一个字符串?如何界定一个字符串的开始与结束呢?大家是否还记得 ’\0' 这个字符?‘\0' 的ASCII码是 0,对应的十六进制表示为 0x00 !大家还记得,操作系统检测一个字符串结束,是以 ’\0' 字符字符串结束标志吗?!

大家思考一下,如果我们仅仅向 printf 函数传递包含格式化符号的字符串(例如,包含 格式化符号”%s“ 的字符串),而并不传递格式化符号对应的其它参数内容给 printf 函数,会发生什么?!

答案是:会产生格式化漏洞

格式化漏洞的起因,是因为 printf 函数并未对传递的格式化符号对应具体参数值进行匹配性安全检查!导致,即使我们不向 printf 函数传递格式化符号对应的相应参数值,printf 函数的代码也会被照常执行,并打印出会让普通程序员感到有些费解,而却令专业黑客非常感兴趣的内容!那就是栈结构空间的内容,从低位地址向高位地址的内容开始输出栈结构空间的所有内容(遇到 ”\0“ 字符后停止)!这就是格式化漏洞危害

如果一些程序员在使用 printf 函数打印相应内容时,并没有对传递给 printf 函数参数进行安全检查,那么就会导致格式化漏洞的产生!大家要注意,栈空间中是存储着一些函数返回地址的!一些函数参数如果是函数指针类型的数据,那么一些函数指针对应的内存地址也会被暴露

由于篇幅内容过多,这篇《【渗透测试之二进制安全系列】格式化漏洞揭秘》的文章已经进行了分章节处理!对于格式化漏洞的内容而言,涉及到的知识点内容很多!在复杂的编程环境中,即使是函数参数传递这块的内容,也会存在很多的变化!例如,传递的函数参数中是否存在浮点型数据,会影响到汇编语言对于寄存器的调用!而浮点型函数参数的传递顺序,也会影响着对于寄存器的调用!

更多内容,请阅读下一篇!文章来源地址https://www.toymoban.com/news/detail-758866.html

到了这里,关于【渗透测试之二进制安全系列】格式化漏洞揭秘(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 你了解Redis 的二进制安全吗

    最近面试的时候被问到Redis 的二进制安全相关八股文面试题。Redis二进制安全内容比较多,以下是简单的总结大致的过程,需要深入学习的建议跳过 Redis是基于C语言进行开发的,而C语言中的字符串是二进制不安全的,所以Redis就没有直接使用C语言的字符串,而是自己编写了一

    2024年02月04日
    浏览(25)
  • CMake教程系列-02-使用cmake代码生成二进制

    参考:Cmake安装以及升级(Ubuntu) Win10安装文件: 创建的目录以及代码如下: CMakeLists.txt sample.cpp 目录结构如下:注意多创建一个build目录存放中间文件和最终二进制文件 点击“Configure 我的的vs是2019,在Configure中选择了 点击“Configure”右边的“Generate”。 点击“Generate”右边

    2024年02月04日
    浏览(40)
  • 【ARM 嵌入式 编译系列 10.4 -- 生成二进制文件】

    在嵌入的工作中,经常会使用到二进制文件,那么我们如何自己生成一个二进制文件呢?接下来介绍如何将一个只包含将32位数据的文件转化为二进制文件,原文件如下(数据一共 64bytes): 我们使用 gcc 对齐先进行编译然后再进行反汇编: 具体命令如下: 通过上面命令会生成

    2024年02月02日
    浏览(35)
  • 【二进制安全】堆漏洞:Double Free原理

    参考:https://www.anquanke.com/post/id/241598 次要参考:https://xz.aliyun.com/t/6342 malloc_chunk 的源码如下: 释放的chunk 会以单向链表的形式回收到fastbin 里面。 fastbin 是 LIFO 的数据结构,使用单向链表实现。 示例代码: 需要使用glibc 2.27编译。 Linux下更换glibc版本的方法:https://blog.csdn.

    2024年02月14日
    浏览(29)
  • 安全研究 # 二进制代码相似性检测综述

    本文参考: [1]方磊,武泽慧,魏强.二进制代码相似性检测技术综述[J].计算机科学,2021,48(05):1-8. (信息工程大学数学工程与先进计算国家重点实验室, 国家重点研发课题,北大核心) 代码相似性检测常用于 代码预测 、 知识产权保护 和 漏洞搜索 等领域,可分为 源代码相似性检测

    2024年02月02日
    浏览(27)
  • 安全研究 # 课题:二进制成分分析(Binary SCA)

    本文参考多篇文章写作而成,出处在文末注明(本文在课题开展过程中长期保持更新)。 SCA(Software Composition Analysis)软件成分分析,通俗的理解就是通过分析软件包含的一些信息和特征来实现对该软件的识别、管理、追踪的技术。通常用来对软件源码、二进制软件包等进行静

    2024年02月16日
    浏览(21)
  • 二进制安全虚拟机Protostar靶场 安装,基础知识讲解,破解STACK ZERO

    pwn是ctf比赛的方向之一,也是门槛最高的,学pwn前需要很多知识,这里建议先去在某宝上买一本汇编语言第四版,看完之后学一下python和c语言,python推荐看油管FreeCodeCamp的教程,c语言也是 pwn题目大部分是破解在远程服务器上运行的二进制文件,利用二进制文件中的漏洞来获

    2024年02月09日
    浏览(27)
  • 二进制安全虚拟机Protostar靶场(2)基础知识讲解,栈溢出覆盖变量 Stack One,Stack Two

    Protostar靶场的安装和一些二进制安全的基础介绍在前文已经介绍过了,这里是文章链接 当系统向缓冲区写入的数据多于它可以容纳的数据时,就会发生缓冲区溢出或缓冲区溢出,用更简单的话说就是在程序运行时,系统会为程序在内存里生成一个固定空间,如果超过了这个空

    2024年02月09日
    浏览(24)
  • 【十进制 转 二进制】【二进制 转 十进制】10进制 VS 2进制【清华大学考研机试题】

    原题链接 本题我们先需要知道 十进制 如何转 二进制 二进制 如何转 十进制 十进制 如何转 二进制: 十进制转成二进制 例如 173 转成 二进制 就把173 短除法 除到0 然后 得到的余数, 从下往上写 二进制 转成 十进制 利用如图方法,把二进制 转成 十进制 本题是高精度,如何

    2023年04月26日
    浏览(32)
  • 将数据转二进制流文件,用PostMan发送二进制流请求

    一、将byte数组转二进制流文件,并保存到本地 byte [] oneshotBytes=new byte[]{78,-29,51,-125,86,-105,56,82,-94,-115,-22,-105,0,-45,-48,-114,27,13,38,45,-24,-15,-13,46,88,-90,-66,-29,52,-23,40,-2,116,2,-115,17,36,15,-84,88,-72,22,-86,41,-90,-19,-58,19,99,-4,-63,29,51,-69,117,-120,121,3,-103,-75,44,64,-58,-34,73,-22,110,-90,92,-35,-18,-128,16,-

    2024年02月15日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包