by cdm258
前言 前些天在网上看到有人在讨论题目,仔细一瞧,是我出的题。再问了问居然是工业互联网比赛的0解题,于是我决定写一篇文章来聊一聊这道题,顺便聊一聊riscv逆向。
题目设计思路 考点 riscv和简单vm。难度当时我定为了中等,但没想到最后没有人解出来。
关于riscv RISC-V是一个基于精简指令集计算(RISC)原则的开源指令集架构(ISA),它是由加州大学伯克利分校的研究团队在2010年开发的。RISC-V的”V”代表第五代RISC,意味着它是继前四代RISC处理器之后的新一代架构。
在逆向中我们主要关注的一个特点是他的架构简单:RISC-V的基础指令集仅有40多条,加上扩展指令也仅有几十条,规范文档简洁,易于理解和实现。
题目流程 首先输入flag后,进入一段魔改tea加密,然后加密结果进行比对。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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" ); 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。
1 2 3 4 5 6 7 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部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall encipher (__int64 result, unsigned int *a2, __int64 a3) { int i; unsigned int v4; unsigned int v5; unsigned int v6; 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 int __cdecl main (int argc, const char **argv, const char **envp) { size_t v4; __int64 v5; __int64 v6; __int64 v7; __int64 v8; __int64 v9; __int64 v10; _QWORD v12[6 ]; int v13; unsigned __int64 i; size_t v15; size_t v16; size_t v17; _QWORD *v18; size_t v19; int *v20; __int64 v21; _QWORD v22[2 ]; char v23[40 ]; char v24[120 ]; v13 = 52 ; v22[0 ] = 0x9ABCDEF012345678 LL; v22[1 ] = 0x9A323EF09AB111F0 LL; 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部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #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就交给读者自己探索啦。
彩蛋 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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反编译,同时由于代码比较简单,也可以直接从汇编入手。
1 2 3 4 5 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出现后难度更是下降了一大截,希望文章中的一些技巧可以给大家带来一些帮助