附件链接

https://adworld.xctf.org.cn/media/file/task/54836ebebad142679...

这一题没有什么花活,但是和以往的c程序不同,c++大量使用模板、类对象等,反汇编之后会非常丑陋。要努力去克服这种干扰,并且熟悉string这种常见类的成员函数

分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  __int64 v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // rax
  __int64 v10; // rax
  __int64 v11; // rax
  __int64 v12; // rax
  __int64 v13; // rax
  __int64 v14; // rax
  __int64 v15; // rax
  __int64 v16; // rax
  char v18[32]; // [rsp+10h] [rbp-130h] BYREF
  char v19[32]; // [rsp+30h] [rbp-110h] BYREF
  char v20[32]; // [rsp+50h] [rbp-F0h] BYREF
  char v21[32]; // [rsp+70h] [rbp-D0h] BYREF
  char v22[32]; // [rsp+90h] [rbp-B0h] BYREF
  char v23[120]; // [rsp+B0h] [rbp-90h] BYREF
  unsigned __int64 v24; // [rsp+128h] [rbp-18h]

  v24 = __readfsqword(0x28u);    //注意这里只是读取canary值,并不是真正的输入
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v18, argv, envp);
  std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, v18);        //这里从键盘读入 v18
  v3 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------------------------");
  std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Quote from people's champ");
  std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
  v5 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------------------------");
  std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
  v6 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "*My goal was never to be the loudest or the craziest. It was to be the most entertaining.");
  std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
  v7 = std::operator<<<std::char_traits<char>>(&std::cout, "*Wrestling was like stand-up comedy for me.");
  std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
  v8 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "*I like to use the hard times in the past to motivate me today.");
  std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
  v9 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------------------------");
  std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
  HighTemplar::HighTemplar(v23, v18);    //这里用v23和v18作为参数来构造一个类对象。注意v23在最开始是分配过内存的,什么数据都没有记载,可能是buffer
  v10 = std::operator<<<std::char_traits<char>>(&std::cout, "Checking....");
  std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v19, v18);
  func1(v20, v19);
  func2(v21, v20);
  func3(v21, 0LL);    //这三个函数的功能未知,但似乎与我们的结果无关
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v21);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v20);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v19);
  HighTemplar::calculate((HighTemplar *)v23);    //calculator接受v23参数,经过分析可以知道是将input进行数据加密(当然是后话了)
  if ( !(unsigned int)HighTemplar::getSerial((HighTemplar *)v23) )    //只要我们进入这个if内部就能够得到flag,getSerial的作用是比较判断
  {
    v11 = std::operator<<<std::char_traits<char>>(&std::cout, "/////////////////////////////////");
    std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
    v12 = std::operator<<<std::char_traits<char>>(&std::cout, "Do not be angry. Happy Hacking :)");
    std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
    v13 = std::operator<<<std::char_traits<char>>(&std::cout, "/////////////////////////////////");
    std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
    HighTemplar::getFlag[abi:cxx11](v22, v23);
    v14 = std::operator<<<std::char_traits<char>>(&std::cout, "flag{");
    v15 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v14, v22);
    v16 = std::operator<<<std::char_traits<char>>(v15, "}");
    std::ostream::operator<<(v16, &std::endl<char,std::char_traits<char>>);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v22);
  }
  HighTemplar::~HighTemplar((HighTemplar *)v23);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v18);
  return 0;
}

c++反汇编后的程序并不优美,我们要根据上下文猜测一些语句的作用。我们发现,一些用到>>和<<的语句,虽然前面很长,但通过看这个语句的尾部,如(&std::cin, v18)也能大致猜出其功能

另一个困难是充斥着大量的basic_string,我们在后续用到的时候再查阅其功能即可

下面看一看几个重要函数:

  • HighTemplar构造函数:
unsigned __int64 __fastcall HighTemplar::HighTemplar(HighTemplar *a1, __int64 a2)
{
  char v3; // [rsp+17h] [rbp-19h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  DarkTemplar::DarkTemplar(a1);
  *(_QWORD *)a1 = &off_401EA0;
  *((_DWORD *)a1 + 3) = 0;
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string((char *)a1 + 16, a2);    //注意仔细阅读这一句的代码,理清结构
 //a1是第一个参数,也就是我们上述猜测的buffer,将它作为基地址,在+16处填充a2的数据 std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string((char *)a1 + 48, a2);    //在+48这里也填充数据
  std::allocator<char>::allocator(&v3);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
    (char *)a1 + 80,
    "327a6c4304ad5938eaf0efb6cc3e53dc",    //在+80这里填充固定的一段数据
    &v3);
  std::allocator<char>::~allocator(&v3);
  return __readfsqword(0x28u) ^ v4;
}

basic_string构造函数可能有多种重载,这里我们用到的是copy形式
注意“类”是编程语言层级的抽象,在汇编层级并不存在,因此在反汇编时并不能复原所有信息,需要我们通过上下文进行类结构的猜测复原

  • calculate
bool __fastcall HighTemplar::calculate(HighTemplar *this)
{
  __int64 v1; // rax
  _BYTE *v2; // rbx
  bool result; // al
  _BYTE *v4; // rbx
  int i; // [rsp+18h] [rbp-18h]
  int j; // [rsp+1Ch] [rbp-14h]

  if ( std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length((char *)this + 16) != 32 )    //从这里我们可以知道,读入的数据必须是32个字符
  {
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Too short or too long");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
    exit(-1);
  }
  for ( i = 0;
        i <= (unsigned __int64)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length((char *)this + 16);
        ++i )
  {
    v2 = (_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                    (char *)this + 16,
                    i);        //注意熟悉operator[]的代码,熟悉其结构
    *v2 = (*(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                       (char *)this + 16,
                       i) ^ 0x50)
        + 23;
  }                //第一轮,执行的轮数等于数据的长度,也就是32(但好像在临界处是<=号?是33轮?)
  for ( j = 0; ; ++j )
  {
    result = j <= (unsigned __int64)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length((char *)this + 16);
    if ( !result )
      break;
    v4 = (_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                    (char *)this + 16,
                    j);
    *v4 = (*(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                       (char *)this + 16,
                       j) ^ 0x13)
        + 11;
  }        //第二轮加密
  return result;
}

我们发现calculate函数就是将input的每个字节进行^0x50 -> +23 -> ^0x13 -> +11

  • getSerial
__int64 __fastcall HighTemplar::getSerial(HighTemplar *this)
{
  char v1; // bl
  __int64 v2; // rax
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  unsigned int i; // [rsp+1Ch] [rbp-14h]

  for ( i = 0;
        (int)i < (unsigned __int64)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length((char *)this + 16);
        ++i )
  {
    v1 = *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                     (char *)this + 80,
                     (int)i);
    if ( v1 != *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](
                           (char *)this + 16,
                           (int)i) )
    {
      v4 = std::operator<<<std::char_traits<char>>(&std::cout, "You did not pass ");
      v5 = std::ostream::operator<<(v4, i);
      std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
      *((_DWORD *)this + 3) = 1;
      return *((unsigned int *)this + 3);
    }
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Pass ");
    v3 = std::ostream::operator<<(v2, i);
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  }
  return *((unsigned int *)this + 3);
}

这个就简单了,将+16处和+80处进行逐字节比较

解密

编写解密程序:

#include<stdio.h>
int main(void){
    char dst[]="327a6c4304ad5938eaf0efb6cc3e53dc";
    char obj[64];
    unsigned char b;
    for (int i=0;i<32;i++){
      b=((dst[i]-11)^0x13-23)^0x50;
        printf("%c",b);
    }
    return 0;
}

但结果输出是一团乱码,我甚至试过用pwntools工具将其喂给crazy,但是仍然显示不通过
后来才发现是异或运算符优先级的问题!!!
只需要该一行:

b=(((dst[i]-11)^0x13)-23)^0x50;

输出为
tMx~qdstOs~crvtwb~aOba}qddtbrtcd
再输入到crazy中得到flag:
flag{tMx~qdstOs~crvtwb~aOba}qddtbrtcd}

疑惑

在HighTemplar对象中,+16处开始填充了32字节的input,+48处开始填充了32字节的input,+80处填充32字节的data,这三部分难道不应该是无空隙相连的吗?为什么在实际运行时lenth还是返回32?


StupidMagpie
6 声望0 粉丝