2025騰訊遊戲安全大賽(安卓決賽)

查看 104|回复 10
作者:ngiokweng   
前言
上一年雖然止步於初賽,但之後找了時間復現了一下決賽,大概花了2、3周才復現完( 還是在有文章參考的情況下 ),內容很多,大致包括保護分析、vm分析、透視、自瞄實現。
今年的決賽比較不同,他給了2種外掛,考察的是外掛功能的分析和外掛檢測,如下圖。
我對外掛的實現與檢測都沒有太深入的研究,下文的檢測方式大多都是參考網上的文章現學現賣的,有寫錯的地方還請指正。


image.png (68.52 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

前置準備
三件套與初賽一樣:
GName:0xADF07C0
GObject:0xAE34A98
GWorld:0xAFAC398
利用GObject來dump SDK,記為SDKO.txt。
./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
ACEInject分析
ACEInject外掛功能分析
ACEInject這個Zygisk模塊會注入libGame.so,將其拉入IDA分析,發現沒有混淆。
在.init_array裡發現它調用pthread_create創建了一個線程,對應線程回調函數如下。
具體邏輯是先獲取libUE4.so的基址,在sub_1618中修改libUE4_base + 0x6711AC4的權限( rwx ),然後將*(libUE4_base + 0x6711AC4)置為0x52A85908。
最後的anti是一些反調試邏輯。


image1.png (40.87 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

libUE4.so的0x6711AC4處是一條mov指令。


image2.png (25.35 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

而0x52A85908對應的arm64匯編是mov w8, #0x42c80000。


image3.png (52.56 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

查看偽代碼,w8最終會賦給*(v5 + 0x460),而v5大概率是個UE4的對象。


image4.png (18.89 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

hook後發現v5可能是以下uobj
[UObjectName]  SphereComp
[UObjectName]  SphereComp   
[UObjectName]  ProjectileComp
[UObjectName]  ProjectileComp
[UObjectName]  SphereComp
[UObjectName]  SphereComp
[UObjectName]  ProjectileComp
[UObjectName]  ProjectileComp
在SDKO.txt裡搜SphereComponent,發現它0x460偏移處是SphereRadius屬性,看來libGame.so中的修改目標就是它。
Class: SphereComponent.ShapeComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object
    float SphereRadius;//[Offset: 0x460, Size: 0x4]
ACEInject反調試分析
anti函數如下,主要是一些frida、hook、調試檢測。


image5.png (26.37 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

調試檢測1:/proc/self/stat


image6.png (33.37 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

端口檢測:包括IDA、frida的默認端口。


image7.png (45.94 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

調試檢測2:TracerPid


image8.png (35.46 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

frida檢測:


image9.png (51.01 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传



image10.png (37.16 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

hook檢測( 應該 ):


image11.png (42.1 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

ACEInject檢測方案
從上述分析可知,libGame.so會修改libUE4.so的sub_6711A54中某處的字節碼。
因此可以通過crc32來判斷sub_6711A54是否被修改,sub_6711A54原始的crc32是0x49d5c836。
uLong get_crc32(uint8_t* addr, size_t size) {
    return crc32(0, addr, size);
}
bool is_sub_6711A54_modify(uint64_t base) {
    // func offset: 0x6711A54
    // size: 0x224
    // orig sub_6711A54 crc32 = 0x49d5c836;
    uLong crc_val = get_crc32(reinterpret_cast(base + 0x6711A54), 0x224);
//    LOGD("crc_val: 0x%llx", crc_val);
    return crc_val != 0x49d5c836;
}
在線程中不斷調用is_sub_6711A54_modify來檢測是否被修改,當libGame.so注入後,成功檢測。
當然更通用的做法可能是對整個.text段進行crc32,或者對一些重要的函數分別進行crc32,這裡針對單一函數的做法只是作為一個演示。
注:不知為何我用Xiaomi8 Lite( Magisk環境 )在測試時發現有時雖然libGame.so成功注入,但卻修改失敗?有時卻能修改成功,有點玄學。而在另一部非Magisk環境的手機手動注入libGame.so時卻能100%修改成功,有點神奇。
cheat分析
elf可執行文件的起始執行函數是start,如下。
一開始以為br x16那裡會跳到具體邏輯,但嘗試用frida stalker hook那處地址時並沒有觸發。


image12.png (19.88 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

用frida stalker簡單trace後發現,__libc_init會跳到0x241d90。
0x241b04: bl #0x5f6bc47440
0x2b0440: adrp x16, #0x5f6bc4b000
0x2b0444: ldr x17, [x16, #0xfb0]
0x2b0448: add x16, x16, #0xfb0
0x2b044c: br x17
0x241d90: sub sp, sp, #0x70
注:後來用IDA9看才發現原來之前是因為沒有正確解析__libc_init的參數,導致看不到sub_241d90。


image13.png (31.36 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

記0x241d90為start_process,這裡會通過am start來啟動APP,啟動成功後會調用usleep等待APP加載so,然後調用sub_241BF0實現外掛邏輯。


image14.png (31.34 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

sub_241BF0如下,一開始先初始化了ImGui,然後循環調用MainLoopStep。


image15.png (43.26 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

對比ImGui源碼,以此手動還原MainLoopStep中的一些符號。
可以看到點擊「初始化輔助」按鈕後,會調用init_cheat進行初始化,看看它的實現。


image16.png (59.15 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

「初始化輔助」分析
init_cheat初始化流程大致如下。
從/proc/pidof com.ACE2025.Game/maps獲取libUE4.so的基址,保存到全局變量。


image17.png (29.5 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

通過process_vm_readv系統調用來跨內存訪問訪問libUE4.so中的一些值,保存到全局變量。


image18.png (56.99 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

遍歷獲取MyProjectCharacter對象( 暫時未知是基於libUE4的哪個全局變量來獲取的 ),然後保存其中的PlayerCameraManager屬性到全局變量。


image19.png (71.55 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

將上述遍歷過程用frida實現,如下:
function test_cheat_init() {
    let unknow1 = base.add(0xAF75B08).readPointer();
    let unknow2 = base.add(0xAF75B08).add(8).readU32();
    console.log("unknow1: ", hexdump(unknow1));
    console.log("FirstPersonCharacter_C: ", All_Objects["FirstPersonCharacter_C"]);
    for(let i = 0; i
由此可以看出0xAF75B08指向的位置保存著Character對象數組,0xAF75B08 + 8指向的位置保存著數組大小。


image20.png (21.24 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

最後調用get_ThirdPerson遍歷獲取 & 保存TP_ThirdPersonCharacter對象( 同上 ),後面還進行了一些操作,但應該不太重要,先不看了。


image21.png (13.33 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传



image22.png (32.2 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

透視繪制分析
init_cheat初始化後,inited會置為true,然後會調用process_cheat_options處理勾選的外掛功能。


image23.png (38.19 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

process_cheat_options是個巨大的函數,一開始會遍歷所有TP_ThirdPersonCharacter對象,收集它們的信息( 同樣利用process_vm_readv系統調用 ),用來計算繪制的參數。


image24.png (52.25 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

中間一大片類似如下結構的代碼,應該是在獲取 & 處理UE4角色的骨骼信息。


image25.png (59.05 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

最後根據勾選的參數來繪制。


image26.png (73.01 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传



image27.png (105.4 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

總的來說process_cheat_options是個巨大的透視框、自瞄框繪制函數。
自瞄分析
自瞄開關的bool值保存在g_aimbot。


image28.png (32.67 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

查看其交叉引用,有兩處,一開始以為所有外掛實現邏輯都在process_cheat_options,但分析了很久都沒有發現其中有實現自瞄的邏輯,基本上都是ImGui的繪制邏輯。
只好仔細分析另一處交叉引用,終於發現寫的操作,但它不是跨進程的寫,是如何實現自瞄的?


image29.png (43.84 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

按x找到g_dev_uinput_fd的初始化邏輯,如下:


image30.png (27.58 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

首先調用get_dev_input_event2_fd遍歷/dev/input目錄,獲取指定的Input Event( 大概是觸控屏幕的事件 ),我的設備會返回/dev/input/event2。


image31.png (64.87 KB, 下载次数: 0)
下载附件
2025-4-14 10:41 上传

然後調用create_virtual_device創建 & 初始化一個虛擬設備。
查資料發現:「uinput是Linux用戶空間模擬輸入設備事件的機制,通過此機制,用戶空間程序可以向系統發送假的輸入事件。」
注:uinput是android內置的一個內核模塊,對其進行open、read、write、ioctl等操作會觸發對應的回調( 這些回調定義在內核中 )。


image32.png (87.97 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

最後會調用parse_dev_input_event2,應該是在解析/dev/input/event2?猜測是上述創建的虛擬設備需要其中的一些數據?
又或者是攔截了/dev/input/event2,使其中的事件重定向到上述創建的虛擬設備?


image33.png (23.53 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

至此g_dev_input_event2_fd初始化成功,之後只要通過write對g_dev_input_event2_fd寫入數據( 特定事件 )即可實現屏幕控制。
回到之前一大堆對g_dev_uinput_fd進行write操作的地方,將該函數記為ctrl_uinput_to_aimbot,大概就是這裡實現的自瞄( 當然前面還進行了一大堆的計算 )。


image34.png (70.57 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

cheat檢測方案
自瞄檢測思路
思路一:/dev/kmsg中有cheat創建虛擬設備的記錄。


image35.png (36.8 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

嘗試監控/dev/kmsg,但發現在so中沒有訪問/dev/kmsg的權限。
void* watch_dev_kmesg(void* arg) {
    char path[] = "/dev/kmsg";
    int fd = open(path, O_RDONLY);
    if (fd
思路二:嘗試監測/sys/devices/virtual/input/目錄。
cheat啟動前:


image36.png (32.92 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

cheat啟動後:多了個input47 ( 47是編號,不是固定的 )


image37.png (33.91 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

它的名字是隨機的。


image38.png (17.42 KB, 下载次数: 0)
下载附件
2025-4-14 10:55 上传

但APP的lib文件同樣沒有訪問/sys/devices/virtual/input/的權限。
偶然發現lstat能訪問/sys/devices/virtual/input/目錄以及其下的子目錄,觀察發現cheat創建的虛擬設備的st.st_mtim、st.st_atim、st.st_ctim這三者會相等,並且等於當前的時間。
因此檢測思路如下:
[ol]
  • 遍歷/sys/devices/virtual/input/inputX,X為9 ~ 255( 觀察我手上僅有的兩部設備,推測input0~input8是系統自帶/保留的,新創建的input編號大概只能從9開始 )。
  • 根據上述條件來判斷是否cheat device( 閾值設置為0x10 )。
    [/ol]
    void* check_virtual_devices(void* arg) {
        char base_path[] = "/sys/devices/virtual/input";
        bool loged = false;
        while (!loged) {
            struct timespec now;
            clock_gettime(CLOCK_REALTIME, &now);
            for(int i = 9; i writeLine("[Cheat Device] %s is cheat device", buf);
                        loged = true;
                        break;
                    }
                }
            }
            sleep(1);
        }
        pthread_exit(0);
    }
    通用檢測思路
    從上述「初始化輔助」分析可知,cheat會通過libUE4.so + 0xAF75B08來遍歷某個Character數組,記這數組為arr。
    因此檢測的思路是將arr的所有元素複製到一片新的內存( 記為fake_memory ),在fake_memory最後插入一個mmap返回的地址( 記為never_access_address ),然後令libUE4.so + 0xAF75B08指向fake_memory,並將數組長度+1。
    正常情況下never_access_address永遠不會被訪問,即不會存在於物理內存空間。而執行init_cheat時會訪問這個地址,導致物理內存出現這個地址。
    而mincore函數能很方便地判斷一個地址是否存在於物理內存空間,具體檢測腳本如下:
    bool is_memory_exist(uint64_t addr) {
        int pagesize = getpagesize();
        unsigned char vec = 0;
        uint64_t start = addr & (~(pagesize - 1));
        mincore((void*)start, pagesize, &vec);
        if (vec == 1) {
            LOGD("內存頁: 0x%llx 在物理內存空間", addr);
        }else{
            LOGD("內存頁: 0x%llx 不在物理內存空間", addr);
        }
        return vec == 1;
    }
    uint64_t insert_memory () {
        if (!libUE4_base)
            libUE4_base = ElfUtils::findBaseAddress("libUE4.so");
        uint32_t arr_len = *reinterpret_cast((libUE4_base + 0xAF75B08 + 8));
        if (!arr_len) {
            return -1;
        }
    //    LOGD("arr_len: %d", arr_len);
        uint64_t arr_start = *reinterpret_cast((libUE4_base + 0xAF75B08));
        uint64_t fake_memory = reinterpret_cast(mmap(nullptr,getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_SHARED, 0, 0));
    //    LOGD("fake_memory: 0x%llx", fake_memory);
        for(int i = 0; i writeLine("[Mincore Detection] cheater access address: 0x%llx", never_access_address);
        pthread_exit(0);
    }
    點擊「初始化輔助」按鈕後,立即就被檢測到。


    image39.png (25.38 KB, 下载次数: 0)
    下载附件
    2025-4-14 10:55 上传

    其他檢測思路
    回顧一下,cheat程序是通過process_vm_readv來跨進程讀取libUE4.so的數據,然後繪制方框、射線等。
    思路一:異常捕獲,通過mprotect將libUE4.so的某片內存權限置為0,然後注冊信號回調捕獲異常。
    void signal_callback(int sig, siginfo_t *info, void *ucontext) {
        ucontext_t* ctx = reinterpret_cast(ucontext);
        if (ctx->uc_mcontext.pc uc_mcontext.pc >= (libUE4_base + libUE4_size)) {
            LOGD("[signalCallback] sig: %lx  pc: %llx  offset: %llx  lr: %llx", sig, ctx->uc_mcontext.pc, (ctx->uc_mcontext.fault_address), ctx->uc_mcontext.regs[30]);
        }
        int pagesize = getpagesize();
        uint64_t addr = libUE4_base + 0xADF07C0;
        uint64_t start = addr & (~(pagesize - 1));
        mprotect(reinterpret_cast(start), pagesize * 2, PROT_READ | PROT_WRITE);
    }
    void init_signal() {
        struct sigaction act;
        sigset_t sigset;
        sigfillset(&sigset);
        act.sa_mask = sigset;
        act.sa_sigaction = signal_callback;
        act.sa_flags = SA_SIGINFO;      // 代表使用sa_sigaction與非sa_handler
        sigaction(SIGSEGV, &act, 0);
    }
    void* test(void* arg) {
        sleep(5);
        init_signal();
        if (!libUE4_base)
            libUE4_base = ElfUtils::findBaseAddress("libUE4.so");
        if (!libUE4_size)
            libUE4_size = ElfUtils::findModuleSize("libUE4.so");
        LOGD("base: 0x%llx  size: 0x%llx", libUE4_base, libUE4_size);
        int pagesize = getpagesize();
        uint64_t addr = libUE4_base + 0xADF07C0;
        uint64_t start = addr & (~(pagesize - 1));
        while (true) {
            mprotect(reinterpret_cast(start), pagesize * 2, PROT_NONE);
    //        sleep(1);
    //        usleep(100);
        }
    }
    結果會導致外掛功能失靈,且無法捕獲cheat的誇內存訪問。
    思路二:由分析可知cheat初始化時會通過/proc//maps獲取libUE4.so的基址,因此嘗試利用inotify來監測/proc//maps,當訪問次數超過n次時代表非法訪問。
    void* watch_proc_maps(void* arg) {
        char path[0x100] = {0};
        snprintf(path, NAME_MAX, "/proc/%d/maps", getpid());
        int fd = inotify_init();
        if (fd mask & IN_ACCESS)){
                        ++access_count;
                    }
                    i += sizeof(struct inotify_event) + event->len;
                }
            }
            // 超過n次訪問, 代表不正常
            // 只記錄一次
            if (access_count > n && !loged) {
                loged = true;
                logManager->writeLine("[Illegal Access] target: %s  count: 0x%lx", path, access_count);
            }
        }
        pthread_exit(0);
    }
    結果是啟動cheat並初始化後,能順利監測到其訪問maps的行為,缺點是沒有更詳細的上下文。
    結語
    又一個周末獻給了騰訊,所幸也是有所收獲,願各位讀者也是如此。
    由於時間和能力有限,很多東西都沒有仔細深入分析,只能一筆帶過,屬實無奈。
    參考
  • https://pshocker.github.io/2022/05/14/Android内存读写断点-mprotect/
  • https://pshocker.github.io/2022/05/08/Android内存读写检测-inotify/
  • https://pshocker.github.io/2022/05/08/Android内存读写检测-mincore/
  • https://stackoverflow.com/questions/15623442/how-do-i-determine-the-files-corresponding-to-a-uinput-device
  • https://www.kernel.org/doc/Documentation/input/uinput.rst

    下载次数, 下载附件

  • ngiokweng
    OP
      


    jbczzz 发表于 2025-4-14 15:26
    大佬能贴个题的下载链接吗,官网好像下不了了

    論壇上傳不了大文件, 我放網盤吧: https://pan.baidu.com/s/17Q2jtgYz4fejIvHgi62ANg?pwd=jg8i
    chawlau   

    膜拜大佬!
    看样子腾讯在反外挂上面又要放大招了,反外挂一直都是游戏公司的重中之重,能够及时准确的检测到玩家使用外挂行为是游戏公司最想做的。但同时,误检误报也是需要极力避免的。
    冒个泡   

    这位是台湾同胞吗,欢迎!
    s22333   

    很不错的分享,如此一看20年过去了啊
    Koriki   

    大佬厉害 膜拜一下
    ngiokweng
    OP
      


    s22333 发表于 2025-4-14 13:18
    很不错的分享,如此一看20年过去了啊

    woc, 大佬研究了20年逆向嗎
    jbczzz   

    大佬能贴个题的下载链接吗,官网好像下不了了
    thinkernb945   

    大佬研究了20年逆向嗎
    qq1006111954   

    大神啊 非常不错的
    您需要登录后才可以回帖 登录 | 立即注册