这道题当时没做,这两天参照@Nu1L战队的writeup调了一下,感觉挺有收获,遂做一下笔记。
首先看一下题目的交互
$ ./pwn400
RSA example
What do you want to do?
1. new cipher
2. encrypt
3. decrypt
4. comment
5. exit
看来本题实现了一个基于RSA的加密工具,顺便看一下开启了哪些保护
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
用IDA打开,发现是C++写的,之前对C++的pwn题有一种莫名的恐惧感,但仔细分析发现和C写的道理是差不多的。好吧,看一看主程序
case 1:
cipher = operator new(0x258uLL);
init_cipher(cipher);
cipher1 = cipher;
keyChain = get_keyChain(cipher);
if ( (unsigned __int8)isKeyChainInit(keyChain) )
{
pubicKey = getP(keyChain);
v25 = sub_402286(keyChain);
pass1(keyChain, (__int64)&opt, v9);
}
这一段主要是初始化一个cipher类
case 2:
if ( cipher1 )
{
...
if ( length <= 0x40 )
{
init_textHead(cipher1, length);
...
readstr((__int64)&buf, length);
if ( (unsigned __int8)isKeyChainInit(keyChain) )
//有密钥加密
(*(void (__fastcall **)(__int64, char *, __int64))(*(_QWORD *)cipher1 + 16LL))(cipher1, &buf, pubicKey);
else
//无密钥加密(本质上是一样的)
(*(void (__fastcall **)(__int64, char *))(*(_QWORD *)cipher1 + 24LL))(cipher1, &buf);
}
...
}
这是进行加密操作
case 3:
if ( cipher1 )
{
...
if ( length <= 0x100 )
{
init_textHead(cipher1, length >> 2);
...
readstr((__int64)&buf, 2 * length);
if ( (unsigned __int8)isKeyChainInit(keyChain) )
//有密钥解密
(*(void (__fastcall **)(__int64, char *, __int64))(*(_QWORD *)cipher1 + 32LL))(cipher1, &buf, v25);
else
//无密钥解密(但本质上也是一样的)
(*(void (__fastcall **)(__int64, char *))(*(_QWORD *)cipher1 + 40LL))(cipher1, &buf);
if ( cipher1 )
//解密后释放cipher类,但是这里并没有将cipher指针置空,导致UAF漏洞
(*(void (__fastcall **)(__int64))(*(_QWORD *)cipher1 + 8LL))(cipher1);
}
...
}
这一部分进行解密操作
case 4:
printf("comment about my implement of RSA", &opt, a4);
v27 = operator new[](0x80uLL);
readstr(v27, 0x80u);
这里申请一块内存,并写入128字节的数据。结合上一步的解密操作,我们可以控制cipher类在堆里面的内存空间,当然也可以覆盖vtable。到了这里我们还缺少一个条件:一块地址已知的可控内存来存储虚函数表。但是,分析完所有的代码,我们并没有发现可以操作bss段的方法,所以我们只能想办法泄露栈地址或堆地址。这里我们就得关注一下加密函数了
int __fastcall encrypt_with_key(__int64 cipher1, __int64 plainText, __int64 pubicKey)
{
...
for ( j = 0; 4 * length > j; ++j )
{
if ( *(_BYTE *)v13 <= 0x9Fu )
{
if ( *(_BYTE *)v13 <= 0x9Fu )
{
v4 = cipher1;
//这里往cipher内存块写入了一个字节的数据
*(_BYTE *)(cipher1 + 2 * j + 72) = (*(_BYTE *)v13 >> 4) + 48;
}
}
...
}
return printCipherText(cipher1, plainText, v4);
}
这里,j 的上边界是4×length,那么表达式cipher1 + 2×j + 72所能表示的最大地址为cipher1 + 8×length + 72,length最大值为0x40,所以cipher1 + 8×length + 72最大值应为cipher+0x248
不难注意到,在初始化对象的时候,有这样一段代码
__int64 __fastcall init_cipher(__int64 cipher)
{
__int64 keyChain; // rbx@1
__int64 result; // rax@1
*(_QWORD *)cipher = functions;
keyChain = operator new(0x14uLL);
init_keyChain(keyChain);
result = cipher;
//这里的keyChain是一个堆地址
*(_QWORD *)(cipher + 0x248) = keyChain;
return result;
}
因此,只要我们输入长度为0x40的明文字符串,加密结束后输出的密文末尾就会跟着keyChain(堆)地址。至此,我们已经可以控制eip了,那么下一步就是泄露libc地址。注意到程序进行加解密的时候都会往栈上读入一段数据,那么只需要合理构造一下Rop链在栈上的位置,就可以达到调用任意函数的目的了。