技术 · 2024年10月8日

浅析riscv逆向-由2024工业互联网一道零解题plctlove开始

前言

前些天在网上看到有人在讨论题目,仔细一瞧,是我出的题。再问了问居然是工业互联网比赛的0解题,于是我决定写一篇文章来聊一聊这道题,顺便聊一聊riscv逆向。

题目设计思路

考点

riscv和简单vm。难度当时我定为了中等,但没想到最后没有人解出来。

关于riscv

RISC-V是一个基于精简指令集计算(RISC)原则的开源指令集架构(ISA),它是由加州大学伯克利分校的研究团队在2010年开发的。RISC-V的”V”代表第五代RISC,意味着它是继前四代RISC处理器之后的新一代架构。

在逆向中我们主要关注的一个特点是他的架构简单:RISC-V的基础指令集仅有40多条,加上扩展指令也仅有几十条,规范文档简洁,易于理解和实现。

题目流程

首先输入flag后,进入一段魔改tea加密,然后加密结果进行比对。

代码如下

void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9;
for (i=0; i < num_rounds; i++) {
v0 += (((v1 << 5) ^ (v1 >> 4)) + v1) ^ (sum + key[(sum & 3)]);
sum += delta;
v1 += (((v0 << 5) ^ (v0 >> 4)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
}
...
    encipher(num_rounds, plaintext_int, key);
  encipher(num_rounds,user_input_int, key);
  for (size_t i = 0; i < num_words; ++i) {
        if (plaintext_int[i] != user_input_int[i]) {
            break;
        }
        printf("舔狗真的是爱吗\n");//假flag提示
        printf("VHJ1ZSBsb3ZlIGRvZXNuJ3QgaGFyYXNzOyBpdCByZXNwZWN0cw==\n");
        return 0;
    }

通过分析我们可以发现这其实是一段密文自身与自身对比,所以这一段可以直接跳过。

随后是一段vm加密,使用opcode来进行加密,最后再次和假flag进行对比。

也就是说,假flag其实密文,将这段密文进行解密即可得到flag。

然后题目还埋下了一个小小的坑,在你输入正确的flag之后,会出现,”and!!! Please replace i(in the flag) with the following picture(NUMBER)这个坑。需要你将flag中的i换成1。

printf("你已寻得真爱,Never_stop_exploiting_and_love!");
printf("and!!! Please replace i(in the flag) with the following picture(NUMBER)\n");
printf("*\n");
printf("*\n");
printf("*\n");
printf("*\n");
printf("*\n");

题目难点解析

难点一是riscv逆向其实很少见,二是题目中采取了一些虚假的加密方式,混淆了选手们的视听,在时间有限的情况下,导致了这道中等难度的题变成了一道0解题。

解题思路

逻辑分析

在一开始我们对输入进行分析时,我们会很容易发现这个魔改的tea加密,但先不要着急,纵观全局之后我们就可以发现一件很诡异的事情,这个加密程序在主程序调用了两次。

tea部分

__int64 __fastcall encipher(__int64 result, unsigned int *a2, __int64 a3)
{
int i; // [sp+2Ch] [-24h]
unsigned int v4; // [sp+30h] [-20h]
unsigned int v5; // [sp+34h] [-1Ch]
unsigned int v6; // [sp+38h] [-18h]v4 = *a2;
v5 = a2[1];
v6 = 0;
for ( i = 0; i < (unsigned __int64)(int)result; ++i )
{
v4 += (*(_DWORD *)(4LL * (v6 & 3) + a3) + v6) ^ (((v5 >> 4) ^ (32 * v5)) + v5);
v6 -= 1640531527;
v5 += (*(_DWORD *)(4 * ((v6 >> 11) & 3LL) + a3) + v6) ^ (((v4 >> 4) ^ (32 * v4)) + v4);
}
*a2 = v4;
a2[1] = v5;
return result;
}

主体逻辑分析

于是我们通过分析主程序摸清主题思路,找到vm程序进行求解,最后得到flag。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  size_t v4; // a0
  __int64 v5; // a1
  __int64 v6; // a2
  __int64 v7; // a3
  __int64 v8; // a4
  __int64 v9; // a6
  __int64 v10; // a7
  _QWORD v12[6]; // [sp+0h] [-190h] BYREF
  int v13; // [sp+34h] [-15Ch]
  unsigned __int64 i; // [sp+38h] [-158h]
  size_t v15; // [sp+40h] [-150h]
  size_t v16; // [sp+48h] [-148h]
  size_t v17; // [sp+50h] [-140h]
  _QWORD *v18; // [sp+58h] [-138h]
  size_t v19; // [sp+60h] [-130h]
  int *v20; // [sp+68h] [-128h]
  __int64 v21; // [sp+70h] [-120h]
  _QWORD v22[2]; // [sp+78h] [-118h] BYREF
  char v23[40]; // [sp+88h] [-108h] BYREF
  char v24[120]; // [sp+B0h] [-E0h] BYREF

  v13 = 52;
  v22[0] = 0x9ABCDEF012345678LL;
  v22[1] = 0x9A323EF09AB111F0LL;
  strcpy(v23, "flag{Love_is_not_one_sided_Love}");
  printf(byte_1E18);
  fgets(v24, 100, stdin);
  v24[strcspn(v24, "\n")] = 0;
  v15 = strlen(v24);
  if ( (v15 & 3) != 0 )
  {
    puts(byte_1E38);
    return -1;
  }
  else
  {
    v16 = v15 >> 2;
    v17 = (v15 >> 2) - 1;
    v12[4] = v15 >> 2;
    v12[5] = 0LL;
    v12[2] = v15 >> 2;
    v12[3] = 0LL;
    v18 = v12;
    v19 = v17;
    v12[0] = v15 >> 2;
    v12[1] = 0LL;
    v20 = (int *)v12;
    memcpy(v12, v23, v15);
    v4 = strlen(v24);
    memcpy(v20, v24, v4);
    encipher(v13, v18, v22);
    encipher(v13, v20, v22);
    sub_114514((__int64)v24, v5, v6, v7, v8, (__int64)v24, v9, v10, v12[0]);
    v21 = 0LL;
    if ( v16 && *((_DWORD *)v18 + v21) == (__int64)v20[v21] )
    {
      puts(byte_1E60);
      puts("VHJ1ZSBsb3ZlIGRvZXNuJ3QgaGFyYXNzOyBpdCByZXNwZWN0cw==");
      return 0;
    }
    else
    {
      puts(byte_1EB0);
      for ( i = 0LL; i <= 0x1F; ++i )
      {
        if ( (unsigned __int8)v24[i - 40] != (unsigned __int64)(unsigned __int8)v24[i] )
        {
          printf(byte_1EF8, (unsigned __int8)v24[i - 40] - (unsigned __int8)v24[i]);
          puts(byte_1F10);
          return 0;
        }
        if ( i == 31 )
        {
          printf(byte_1F90);
          puts(aAnd);
          puts("*");
          puts("*");
          puts("*");
          puts("*");
          puts("*");
        }
      }
      return 0;
    }
  }
}

vm部分

while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( !*((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) )
{
++*((_DWORD *)&a9 - 1807);
v9 = (_BYTE *)(*((int *)&a9 - 1808) + *((_QWORD *)&a9 - 905));
++*v9;
}
if ( *((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) != 1LL )
break;
++*((_DWORD *)&a9 - 1807);
v10 = (_BYTE *)(*((int *)&a9 - 1808) + *((_QWORD *)&a9 - 905));
--*v10;
}
if ( *((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) != 2LL )
break;
++*((_DWORD *)&a9 - 1807);
++*((_DWORD *)&a9 - 1808);
}
if ( *((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) == 3LL )
break;
if ( *((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) == 4LL )
{
*(_BYTE *)(*((int *)&a9 - 1808) + *((_QWORD *)&a9 - 905)) = *(_BYTE *)(*((int *)&a9 - 1808)1LL*((_QWORD *)&a9 - 905))*(_BYTE *)(*((int *)&a9 - 1808)*((_QWORD *)&a9 - 905))70;
++*((_DWORD *)&a9 - 1807);
}
else if ( *((_DWORD *)&a9 + *((int *)&a9 - 1807) - 1806) == 5LL )
{
*(_BYTE *)(*((int *)&a9 - 1808) + *((_QWORD *)&a9 - 905)) = *(_BYTE *)(*((int *)&a9 - 1808)*((_QWORD *)&a9 - 905))*(_BYTE *)(*((int *)&a9 - 1808)1LL*((_QWORD *)&a9 - 905))70;
++*((_DWORD *)&a9 - 1807);
}

直接根据opcode逆

动态调试提取opcode,随后直接进行逆向

exp

#l里面是opcode 太长故不放出 
l[0]+=32
l[1]+=32
l[2]+=-39+l[3]
l[3]+=155-l[4]
l[4]+=0
l[5]+=-42
l[6]+=2
l[7]+=23
l[8]+=111-l[9]
l[9]+=75-l[10]
l[10]+=10
l[11]+=145-l[12]
l[12]+=0
l[13]+=160-l[14]
l[14]+=109-l[15]
l[15]+=131-l[16]
l[16]+=-19
l[17]+=-74+l[18]
l[18]+=26
l[19]+=6
l[20]+=-61+l[21]
l[21]+=67
l[22]+=120-l[23]
l[23]+=-1
l[24]+=6
l[25]+=87-l[26]
l[26]+=112-l[27]
l[27]+=-42
l[28]+=124-l[29]
l[29]+=4
l[30]+=-14
l[31]+=0

flag=[ord(x) for x in "flag{Love_is_not_one_sided_Love}"]
opcode=''.join([str(i) for i in opcode]).split("2")
for i in list(range(len(opcode)))[::-1]:
    x=opcode[i]
    d=x.count("0")-x.count("1") 
    if x.count("4")==1:
        d+=flag[i+1]-70
    elif x.count("5")==1:
        d-=flag[i+1]-70
    flag[i]-=d
print("".join(map(chr,flag)))

最后由于是0解题,flag就交给读者自己探索啦。

彩蛋

      puts(byte_1EB0);
      for ( i = 0LL; i <= 0x1F; ++i )
      {
        if ( (unsigned __int8)v24[i - 40] != (unsigned __int64)(unsigned __int8)v24[i] )
        {
          printf(byte_1EF8, (unsigned __int8)v24[i - 40] - (unsigned __int8)v24[i]);
          puts(byte_1F10);
          return 0;
        }
        if ( i == 31 )
        {
          printf(byte_1F90);
          puts(aAnd);
          puts("*");
          puts("*");
          puts("*");
          puts("*");
          puts("*");
        }

需要我们将flag中的i替换成1

riscv的其他题目与技巧

XYCTF-TCPL

这道题是一道misc题我们直接运行就可以拿到flag,如何运行riscv,建议使用qemu配置然后进行运行。当然我们也可以使用ghidra进行分析最后写脚本解出。

FLAG{PLCT_An4_r0SCv_x0huann0}

[MoeCTF 2022]EzRisc-V

一道较为基础的riscv题目 这里建议采用ghidra反编译,同时由于代码比较简单,也可以直接从汇编入手。

key = [84, 86, 92, 90, 77, 95, 66, 75, 8, 74, 90, 20, 79, 102, 8, 74, 102, 74, 86, 9, 9, 86, 86, 102, 8, 87, 77, 92, 75, 92, 74, 77, 8, 87, 0, 24, 24, 24, 68]

for i in range(len(key)):
    flag = key[i] ^ 0x39
    print(chr(flag), end='')

moectf{r1sc-v_1s_so00oo_1nterest1n9!!!}

总结

其实riscv的逆向并没有多难 在ida9.0出现后难度更是下降了一大截,希望文章中的一些技巧可以给大家带来一些帮助

本文章首发于先知社区

苏ICP备2024067700号 | 苏公网安备32098202000238号