180827 逆向-网鼎杯(3-1)

这两周比赛接着比赛打到头爆。。。。
网鼎杯的题目质量感觉都还行,也练习到了不少东西

SimpleSMC


这里的0x400aa6函数点过去一看就可以发现它是乱的字节无法执行,根据题目SMC(Self-Modifying Code)可以猜到这个函数就是被修改的目标了

查看它的交叉引用可以发现有两个函数有调用
sub_400c48
关键代码如下

 for ( i = 0; *((_BYTE *)sub_400AA6 + i) != 0xC3u; ++i )
  {
    v1 = *((unsigned __int8 *)sub_400AA6 + i);
    *((_BYTE *)sub_400AA6 + i) = v1 ^ *((_BYTE *)sub_41E1B0 + i);
  }

是一个函数字节码异或的固定调用,直接写个IDC脚本解就行
查看这个函数的交叉引用是在init_array中,所以是在调用之前调用的

sub_400bac

signed __int64 __fastcall sub_400BAC(char *key)
{
  int i; // [rsp+Ch] [rbp-Ch]

  for ( i = 0; *((_BYTE *)sub_400AA6 + i) != 0xC3u; ++i )
    *((_BYTE *)sub_400AA6 + i) ^= key[i % 7];
  return 1LL;
}

根据key进行逐字节异或,这里的key从调用方可以看出是Input的一部分,所以要猜key

根据密码学常识,单表替换即key仅有一个字节的时候通常是根据词频统计来解决的,在机器码中一般00较多
而本题是类似于维吉尼亚的多表替换,循环长度为7,如果当函数字节足够长的话也是可以用上述方式解决的–将整个密文分成7段,每段内独立进行词频统计。尝试了一下,本题由于密文不够大,因此统计规律应用的不是很顺畅,仅能解出密钥的最后一个字节e

既然词频统计不好使的话,就得从一些固定字节来思考了
这个题目里key不是input的开头,因此没有固定的内容
但对于一个被加密的函数而言,如果非手写汇编的话,通常会以push rbp;mov rbp, rsp开头,通过这几个字节进行异或可以得到一个全可见字符的串

于是根据这个key也可以写出一个解密脚本:

#a = [0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec]

addr = 0x400aa6
i = 0
while(Byte(addr+i)!=0xc3):
    PatchByte(addr+i, Byte(addr+i)^Byte(0x41e1b0+i))
    i += 1
#for i in range(7):
#    print(chr(Byte(addr+i)^a[i])),

key = "F1@gChe"
i = 0
while(Byte(addr+i)!=0xc3):
    PatchByte(addr+i, Byte(addr+i)^ord(key[i%7]))
    i += 1

要注意的一点是,这两个异或顺序是不可变的–因为它们判断结束的标志是加密前的0xC3,而先异或0x41e1b0时是不会产生0xc3的中间字节的,但是如果先异或key,会产生0xC3的中间字节,导致第二次异或时被截断
当然如果将解密循环改为固定长度就可以规避这个BUG

解密得到

_BOOL8 __fastcall sub_400AA6(__int64 a1)
{
  _BOOL8 result; // rax
  __int64 v2; // rdx
  __int64 v3; // rcx
  unsigned __int64 v4; // rt1
  __int64 v5; // [rsp+10h] [rbp-30h]
  __int64 v6; // [rsp+18h] [rbp-28h]
  __int64 v7; // [rsp+20h] [rbp-20h]
  __int64 v8; // [rsp+28h] [rbp-18h]
  char v9; // [rsp+30h] [rbp-10h]
  unsigned __int64 v10; // [rsp+38h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  sub_4009AE(a1, 32LL, 64LL);
  v9 = 0;
  v5 = 0x3851081D0B070A66LL;
  v6 = 0x281A0A3038145C1FLL;
  v7 = 0x1F012224240C5939LL;
  v8 = 0xA1505083A1D731ELL;
  result = (unsigned int)sub_400360((__int64)&v5, a1) == 0;
  v4 = __readfsqword(0x28u);
  v3 = v4 ^ v10;
  if ( v4 != v10 )
    sub_443260(&v5, a1, v2, v3);
  return result;
}

其中a1是input,sub_4009AE很明显对它进行处理,然后sub_400360进行校验

那么我们首先看一下check方法

emmmm有点乱,我觉得不太对劲,退回去看一眼

这是plt段的跳转,也就是说这是一个静态链接的库函数

再根据它的另一个函数来想,很明显就是strcmp啦

那么只需要专心看sub_4009AE是怎么变换的了
点进去看注意这里有花指令,按D将它们调成数据后,将脏字节E8改成90(NOP)即可

然后即可F5了

这里是一个63x5的迭代
内层计算同样有花指令,同样清除以后即可反编译

__int64 __fastcall calc_in(char *a1, int a2)
{
  int i; // [rsp+1Ch] [rbp-4h]

  if ( !a2 )
    return 0LL;
  for ( i = 0; i < a2; ++i )
    a1[a2 + i] ^= a1[i];
  return calc_in(a1, a2 >> 1);
}

外层计算由于不影响参数,因此只是重复计算
另一方面,由于内层计算全部都是异或,因此偶数次计算会使得数组还原。我觉得是出题人的失误23333不过影响也不大就是了

内层计算的逆运算比较简单,逆序异或即可

r = bytes.fromhex("660a070b1d0851381f5c1438300a1a2839590c242422011f1e731d3a0805150a")
r = list(r)
# print(r)
# print(len(r))
def foo(s, n):
    print(s)
    if(n==32):
        return
    for i in range(n):
        s[n+i] ^= s[i]
    foo(s, n<<1)
foo(r,1)
print("".join([chr(i) for i in r]))

准备飞了,我们明天再见。。。

你可能感兴趣的:(CTF)