LOADING

加载过慢请开启缓存 浏览器默认开启

CtfNewStar Week5 Writeup

MY_ARM

用ida打开我们就可以发现输入,跟踪数据,就可以找到对比函数和加密函数,里面有密钥和密文。加密函数就是一个原生的tea加密,去解密,发现解密的是错误的,于是我们进行动调寻找,被修改的密文和密钥。用qume虚拟机运行程序

qemu-arm -g 23946 文件

再次查看就可以找到被修改的密文和密钥,直接tea解密就行,值得注意的是在这个tea解密时,v1,v2的数据类型应该是int确保要有符号

#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;”
Snipaste_2024-12-02_19-55-00
往后面看,只有一个base64加密,再后面就没有其他加密方法了。接下来的步骤就是找出密文和密钥,以及iv。

在这里我们需要一个真机来进行ARM调试,用frida进行hook或者IDA调试。这里我们用frida进行hook。

首先我们来hookXXtea的密钥,根据XXtea的加密逻辑,我们可以知道密钥的偏移量里面有一个”&3”,我们找到这里。
Snipaste_2024-12-02_20-25-20
但是偏移量有点大,所以我们直接在寄存器里找密钥,在汇编里找到和+16有关x29的寄存器,很显然就是X12。
Snipaste_2024-12-02_20-28-10
然后我们把地址复制下来,就可以写hook脚本读取寄存器数值了,脚本如下。
用类似的方法可以拿到AES加密的密钥和iv。
最后我们要找密文。密文其实在java层里,但是最后有一个check函数,在这个函数里面一定会把密文传入比较,所以我们同样可以用上面的方法获取寄存器的值来获取密文,只不过我们要猜一下是哪个寄存器。

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;  //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);函数,这个函数里面藏了一些东西,

BOOL sub_4017A9(const char *a1, __int64 a2, ...)
{
  _UNKNOWN *retaddr;  //一个地址指针,存储了main函数call的返回地址,即call的下一个地址

  scanf(a1, a2);  //获取输入
  *((_QWORD *)qword_408A20[0] + 1) = &retaddr;  //记录返回地址到变量中
  CreateProcessA(ApplicationName, 0LL, 0LL, 0LL, 0, 0, 0LL, 0LL, &StartupInfo, (LPPROCESS_INFORMATION)&hObject); //以ApplicationName创建一个子进程,把句柄保存到hObject
  WaitForSingleObject(hObject, 0xFFFFFFFF); //等待句柄记的结束,0xFFFFFFFF表示等待时间为无限,
  UnmapViewOfFile(qword_408A20[0]); //取消文件映射
  CloseHandle(qword_408A70);  //关闭句柄
  CloseHandle(hObject);//关闭hObject句柄
  return CloseHandle(*(&hObject + 1)); //关闭句柄
}

这里涉及到了文件映射的概念,UnmapViewOfFile(qword_408A20[0]); 这个函数取消了一个文件映射,我们查看qword_408A20[0]这个文件的交叉引用,我们可以找到另一个函数sub_401550()。

Windows文件映射:创建并打开一个文件,把这个文件映射到内存中,通过读写这个文件以达到让不同进程共享内存的目的。

DWORD sub_401550()
{
  DWORD *v0; // rbx
  DWORD result; // eax
  __int64 Buffer; // [rsp+30h] [rbp-50h] BYREF
  HANDLE hProcess; // [rsp+38h] [rbp-48h]

  qword_408A70[0] = OpenFileMappingA(0xF001Fu, 0, "jun...junkcode?");  //打开一个名为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);//写内存,把buffer的值写入*((LPVOID *)qword_408A20[0] + 1)中
      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;//把4201097LL;赋值给*((_QWORD *)qword_408A20[0] + 2),上文中是buffer。
  v0 = (DWORD *)qword_408A20[0];//保存指针到v0
  result = GetCurrentProcessId();//获取进程ID
  *v0 = result;//把进程ID赋值给v0指针,即是为qword_408A20[0]。
  return result;
}

这个函数整个逻辑的大概解释,就是父进程执行打开失败的内容,子进程执行打开成功的内容。
那么这个函数的逻辑就是父进程把自己的返回地址和另一个返回地址映射到内存中,然后被子进程替换,从而使父进程的返回地址改变到4201097。
加密逻辑已经清楚,但是我们还是不知道总程序的逻辑。接下来我们分析一下这个函数的交叉引用来寻找总逻辑。

总逻辑

  • 对sub_401550()引用我们可以找到一个数组__int64 qword_4032A0[];

    在这个数组中记录了sub_401550()的地址,继续寻找数组的引用,继续往下找我们可以找到sub_401B50()
__int64 sub_401B50()
{
  void (**v0)(void); // rbx
  __int64 *v1; // rsi
  unsigned int i; // eax

  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键恢复成代码后勉强可以看到汇编,不能反编译。

.text:0000000000401A90 loc_401A90:                             ; CODE XREF: .text:0000000000401B00↓j
.text:0000000000401A90                 movsx   eax, bl
.text:0000000000401A93
.text:0000000000401A93 loc_401A93:                             ; CODE XREF: .text:0000000000401A69↑j
.text:0000000000401A93                 mov     edx, 41 
.text:0000000000401A98                 sub     edx, eax ;41减去下标
.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]  ;下标的2倍
.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] ;input[(2*i)%42]
.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  ;获取input[41-i]
.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;这样就可以直接跳到加密部分分析了。
这里我们经过短暂(雾)的分析汇编盯针出主要的加密逻辑。

for (int i = 0; i < 42; i++) {
    input[41 - i] ^= input[(2* i)%42];
}

写出解密脚本

#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]);
    }
  
}
//flag{G00d_jOb_!_7h1s_i5_nOt_0nIy_junkc0d3}