MY_ARM
用ida打开我们就可以发现输入,跟踪数据,就可以找到对比函数和加密函数,里面有密钥和密文。加密函数就是一个原生的tea加密,去解密,发现解密的是错误的,于是我们进行动调寻找,被修改的密文和密钥。用qume虚拟机运行程序
再次查看就可以找到被修改的密文和密钥,直接tea解密就行,值得注意的是在这个tea解密时,v1,v2的数据类型应该是int确保要有符号
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
| #include <stdio.h> #include <stdint.h>
int decrypt(uint32_t* v, uint32_t* k) { int sum = 0x9E3779B9 * 32; int v0 = v[0], v1 = v[1], i; uint32_t delta = 0x9E3779B9; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
for (i = 0; i < 32; i++) { v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } v[0] = v0; v[1] = v1; return 0; }
int main() { uint32_t v[] = { 0xA0F8CB44, 0xF82F83CF, 0xA55E48C2, 0x7A26E00A, 0xF1E354C9, 0x687D9915, 0xF88816E8, 0x90878E86, 0x3AB06298, 0xCBCFE78B, 0x578F0F50, 0xC39E3C65, 0xBBE92B84, 0x128A2CA2, 0xDB8F03F5, 0x8482F8E2 }; uint32_t k[4] = { 0x11223344, 0x55667788, 0x9900AABB, 0xCCDDEEFF }; for (int i = 0; i < 16; i += 2) { decrypt(v+i, k); } for (int i = 0; i < 64; i++) { printf("%c", *((unsigned char*)v + i)); } return 0; }
|
ohn_flutter!!!
用blutter进行解包,在”asm\ohn_flutter”目录下,把这个文件夹在vscode里面打开,我们可以看到汇编形式的源代码,可以尝试用关键的字符串搜索,找到程序的主逻辑处。然后我们可以读汇编中调用的函数,和一些关键参数,来查看加密函数的名字,和地址。然后在ida中分析。搜索字符串,发现主逻辑在一个_bulid的函数里面,用地址在ida中找到这个函数,发现是一坨,也不好找到逻辑。于是回到汇编中继续往下看,这个时候我们看到了一个”key”,但是查找后也没有什么收获。继续往下看我们看到一个函数里面调用了许多加密函数,用ida分析,这就是加密的主函数,一下就看到一个AES加密。这里面有些函数出现了很多次,感觉是程序自带的函数,把这些函数排除后,我们一个个进去看,在”ohn_flutter_doi_::jumppp_2fe3c8()”深入挖掘,我们还可以看到在”ohn_flutter_drink_drink::_encryptUint32List_2fe5ec()”函数里面有XXTEA加密的特征,在下图的上面还有一个”v18 = v14 / v16 + 6;”

往后面看,只有一个base64加密,再后面就没有其他加密方法了。接下来的步骤就是找出密文和密钥,以及iv。
在这里我们需要一个真机来进行ARM调试,用frida进行hook或者IDA调试。这里我们用frida进行hook。
首先我们来hookXXtea的密钥,根据XXtea的加密逻辑,我们可以知道密钥的偏移量里面有一个”&3”,我们找到这里。

但是偏移量有点大,所以我们直接在寄存器里找密钥,在汇编里找到和+16有关x29的寄存器,很显然就是X12。

然后我们把地址复制下来,就可以写hook脚本读取寄存器数值了,脚本如下。
用类似的方法可以拿到AES加密的密钥和iv。
最后我们要找密文。密文其实在java层里,但是最后有一个check函数,在这个函数里面一定会把密文传入比较,所以我们同样可以用上面的方法获取寄存器的值来获取密文,只不过我们要猜一下是哪个寄存器。
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
| const ShowNullField = false; const MaxDepth = 5; var libapp = null;
Interceptor.att
function onLibappLoaded() { const fn_addr = 0x2FE7F0; Interceptor.attach(libapp.add(fn_addr), { onEnter: function () { var r1 = this.context.x12; console.log(r1) console.log(hexdump(ptr(r1), { length: 100, ansi: true })) } }); }
function tryLoadLibapp() { libapp = Module.findBaseAddress('libapp.so'); if (libapp === null) setTimeout(tryLoadLibapp, 500); else onLibappLoaded(); } tryLoadLibapp();
|
jun…junkcode?
打开后去除一个jz,jnz的花指令。我们可以看到main函数的加密流程,就是一个对表的异或和加减,写出解密脚本发现答案并不正确,结合题目来看我们再次去仔细的看看main前面的其他函数,同时我们也可以看看import,里面导入了许多WindowsAPI的函数。在开头,我们可以看到一个sub_4017A9(“%43s”, byte_408A40);函数,这个函数里面藏了一些东西,
1 2 3 4 5 6 7 8 9 10 11 12 13
| BOOL sub_4017A9(const char *a1, __int64 a2, ...) { _UNKNOWN *retaddr;
scanf(a1, a2); *((_QWORD *)qword_408A20[0] + 1) = &retaddr; CreateProcessA(ApplicationName, 0LL, 0LL, 0LL, 0, 0, 0LL, 0LL, &StartupInfo, (LPPROCESS_INFORMATION)&hObject); WaitForSingleObject(hObject, 0xFFFFFFFF); UnmapViewOfFile(qword_408A20[0]); CloseHandle(qword_408A70); CloseHandle(hObject); return CloseHandle(*(&hObject + 1)); }
|
这里涉及到了文件映射的概念,UnmapViewOfFile(qword_408A20[0]); 这个函数取消了一个文件映射,我们查看qword_408A20[0]这个文件的交叉引用,我们可以找到另一个函数sub_401550()。
Windows文件映射:创建并打开一个文件,把这个文件映射到内存中,通过读写这个文件以达到让不同进程共享内存的目的。
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
| DWORD sub_401550() { DWORD *v0; DWORD result; __int64 Buffer; HANDLE hProcess;
qword_408A70[0] = OpenFileMappingA(0xF001Fu, 0, "jun...junkcode?"); if ( qword_408A70[0] ) { qword_408A20[0] = MapViewOfFile(qword_408A70[0], 0xF001Fu, 0, 0, 0x18uLL); if ( DebugActiveProcess(*(_DWORD *)qword_408A20[0]) ) { hProcess = OpenProcess(0x1F0FFFu, 0, *(_DWORD *)qword_408A20[0]); Buffer = *((_QWORD *)qword_408A20[0] + 2); WriteProcessMemory(hProcess, *((LPVOID *)qword_408A20[0] + 1), &Buffer, 8uLL, 0LL); DebugActiveProcessStop(*(_DWORD *)qword_408A20[0]); } UnmapViewOfFile(qword_408A20[0]); CloseHandle(qword_408A70[0]); CloseHandle(hProcess); exit(0); } GetModuleFileNameA(0LL, ApplicationName, 0x104u); qword_408A70[0] = CreateFileMappingA((HANDLE)0xFFFFFFFFFFFFFFFFLL, 0LL, 4u, 0, 0x18u, "jun...junkcode?"); qword_408A20[0] = MapViewOfFile(qword_408A70[0], 0xF001Fu, 0, 0, 0x18uLL); *((_QWORD *)qword_408A20[0] + 2) = 4201097LL; v0 = (DWORD *)qword_408A20[0]; result = GetCurrentProcessId(); *v0 = result; return result; }
|
这个函数整个逻辑的大概解释,就是父进程执行打开失败的内容,子进程执行打开成功的内容。
那么这个函数的逻辑就是父进程把自己的返回地址和另一个返回地址映射到内存中,然后被子进程替换,从而使父进程的返回地址改变到4201097。
加密逻辑已经清楚,但是我们还是不知道总程序的逻辑。接下来我们分析一下这个函数的交叉引用来寻找总逻辑。
总逻辑
- 对sub_401550()引用我们可以找到一个数组__int64 qword_4032A0[];

在这个数组中记录了sub_401550()的地址,继续寻找数组的引用,继续往下找我们可以找到sub_401B50()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __int64 sub_401B50() { void (**v0)(void); __int64 *v1; unsigned int i;
for ( i = 0; qword_4032A0[i + 1]; ++i ) ; if ( i ) { v0 = (void (**)(void))&qword_4032A0[i]; v1 = &qword_4032A0[i - (unsigned __int64)(i - 1) - 1]; do (*v0--)(); while ( v0 != (void (**)(void))v1 ); } return sub_401510(loc_401B10); }
|
这个函数把qword_4032A0[]数组的指针依次保存到v0执行,包括我们的sub_401550()函数。我们继续往上跟,最终会跟到main函数里面

这下就真相大白了,在进入main函数时程序也就是父进程,先执行了sub_401550(),因为没有创建映射对象所以会走打开失败的分支,然后创建映射对象,并映射内存,把上述的数据写入内存。接下来执行到输入的函数sub_4017A9(),创建子进程(创建的子进程与父程序是相同文件)并等待子进程的结束。这期间子进程因为有了父进程的创建映射对象,子进程会走成功打开的分支,修改父进程的返回地址,从而改变程序的执行逻辑,我们找到最终执行的位置,发现真正的加密方法。
那么接下来就是对最后的加密进行分析了,我们来到0x401A89处,也就是父进程返回被修改后到的函数。但是我们发现一个问题,这里是一大堆字节码,我们c键恢复成代码后勉强可以看到汇编,不能反编译。
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
| .text:0000000000401A90 loc_401A90: .text:0000000000401A90 movsx eax, bl .text:0000000000401A93 .text:0000000000401A93 loc_401A93: .text:0000000000401A93 mov edx, 41 .text:0000000000401A98 sub edx, eax .text:0000000000401A9A mov eax, edx .text:0000000000401A9C lea rdx, byte_408A40 .text:0000000000401AA3 cdqe .text:0000000000401AA5 movzx r8d, byte ptr [rdx+rax] .text:0000000000401AAA movsx eax, bl .text:0000000000401AAD lea ecx, [rax+rax] .text:0000000000401AB0 mov edx, 818089009 .text:0000000000401AB5 mov eax, ecx .text:0000000000401AB7 imul edx .text:0000000000401AB9 sar edx, 3 .text:0000000000401ABC mov eax, ecx .text:0000000000401ABE sar eax, 31 .text:0000000000401AC1 sub edx, eax .text:0000000000401AC3 mov eax, edx .text:0000000000401AC5 imul eax, 42 .text:0000000000401AC8 sub ecx, eax .text:0000000000401ACA mov eax, ecx .text:0000000000401ACC lea rdx, byte_408A40 .text:0000000000401AD3 cdqe .text:0000000000401AD5 movzx edx, byte ptr [rdx+rax] .text:0000000000401AD9 movsx eax, bl .text:0000000000401ADC mov ecx, 41 .text:0000000000401AE1 sub ecx, eax .text:0000000000401AE3 mov eax, ecx .text:0000000000401AE5 mov ecx, r8d .text:0000000000401AE8 xor ecx, edx .text:0000000000401AEA lea rdx, byte_408A40 .text:0000000000401AF1 cdqe .text:0000000000401AF3 mov [rdx+rax], cl .text:0000000000401AF6 mov eax, ebx .text:0000000000401AF8 add eax, 1 .text:0000000000401AFB mov ebx, eax .text:0000000000401AFD cmp bl, 29h .text:0000000000401B00 jle short loc_401A90 .text:0000000000401B02 jmp loc_4019F9
|
其实这里可以借助动调分析。但是这个题是有反调试的,在上面的sub_401550()函数中,要子进程调试父进程,如果在这之前父进程被其他进程调试了,子进程就无法附加调试,那自然无法执行正确的逻辑。我们已经知道子程序的逻辑就是修改父进程的返回地址,我们可以手动修改。在retn处下断点,然后修改EIP的值为0x401A89;这样就可以直接跳到加密部分分析了。
这里我们经过短暂(雾)的分析汇编盯针出主要的加密逻辑。
1 2 3
| for (int i = 0; i < 42; i++) { input[41 - i] ^= input[(2* i)%42]; }
|
写出解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h>
int main() { unsigned char mm[42] = { 0x34, 0x6C, 0x60, 0x33, 0x15, 0x3B, 0x74, 0x38, 0x5E, 0x6A, 0x53, 0x05, 0x31, 0x1C, 0x43, 0x35, 0x53, 0x58, 0x4A, 0x12, 0x39, 0x3B, 0x35, 0x5E, 0x3A, 0x21, 0x08, 0x1B, 0x44, 0x00, 0x7C, 0x26, 0x6E, 0x5D, 0x54, 0x0C, 0x01, 0x07, 0x00, 0x1F, 0x52, 0x1B };
for (int i = 41; i>=0; i--) { mm[41-i] ^= mm[(2 * i) % 42]; printf("%c", mm[41-i]); } }
|