本文章首发于先知社区
前言
前些天在网上看到有人在讨论题目,仔细一瞧,是我出的题。再问了问居然是工业互联网比赛的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出现后难度更是下降了一大截,希望文章中的一些技巧可以给大家带来一些帮助