iOS內存掃描工具實現

 

由於不能告訴你的原因,我需要一個iOS下的,可以在指定App的內存中搜索字符串的工具。

找了一圈,發現一個比較接近的開源項目:rxmemscan,但是不支持搜索字符串,遂 修改 學習了一番。

又,修改後的源碼在這裏:https://github.com/liumazi/rxmemscan

 


環境搭建

 

運行環境

由於非越獄的iOS設備有諸多權限限制,首先需要對進行越獄,我使用的是CheckRa1n,越獄完成後,手機桌面會出現checkra1n圖標,點開後可以進一步安裝Cydia(越獄商店)。

 

開發環境

Theos是一個越獄開發工具包,在Mac上的安裝方法請自行搜索。:)

然後,rxmemscan還使用了readline庫,下載,拷貝到$THEOS/vendor目錄,即可。

至此,進入rxmemscan目錄,執行 $ make,應該就可以編譯成功啦!

如果希望編譯完直接安裝到設備上,可以修改Makefile中的THEOS_DEVICE_IP爲設備的局域網IP,再執行  $ make do FINALPACKAGE=1

 


實現原理

 

Darwin

Darwin是macOS和iOS系統的類UNIX核心,架構圖如下。而Darwin的核心是XNU,主要包括:Mach微內核、BSD層、libKern、I/O Kit。這裏我們主要使用Mach/BSD層提供的底層API。

圖片來源《Mac OS X and iOS Internals》
圖片來源《Mac OS X and iOS Internals》

 

任務端口

要使用Mach API操作另一個任務(進程),首先要調用task_for_pid,將BSD層的進程ID轉換得到一個Mach的“任務端口”。(類似於Windows下OpenProcess獲取進程句柄)

boolean_t rx_mem_scan::attach(pid_t pid) {
    _target_pid = pid;
    kern_return_t ret = task_for_pid(mach_task_self(), pid, &_target_task);
    if (ret != KERN_SUCCESS) {
        _trace("Attach to: %d Failed: %d %s\n", pid, ret, mach_error_string(ret));
        return false;
    }
    reset();
    return true;
}

 

虛擬內存

和其他操作系統類似,每一個Mach任務(進程)都有獨立的虛擬內存空間,空間內又分爲多個虛擬內存"區域"(VM Region),而每一個"區域"又分爲若干"頁",一個"頁"的大小通常爲4K或16K。"頁"用於分頁機制,以實現虛擬內存到物理內存的映射(地址轉換)。分頁機制是由處理器(按頁錶轉換)和操作系統(維護頁表)共同支持的。

使用XCode附帶的Debug Memory Graph、VM Tracker,以及配合vmmap命令,可以不同程度地觀察App的虛擬內存使用情況。

下面爲VM Tracker截圖(打開方式:Product -> Profile -> Allocations -> +VM Tracker)

高亮選中行對應 malloc(6 * 1024 * 1024);它佔用一個1536頁的MALLOC_LARGE區域(不同的MALLOC_XX區域用於滿足不同尺寸的malloc()調用);圖中幾種Size的含義:

  • Virtual — 虛擬內存佔用,對於malloc()而言,就是它實際分配的虛擬內存大小
  • Resident —  物理內存佔用,已映射到物理內存或者說分配了物理內存的部分
  • Dirty —  前者中不可丟棄的部分; 物理內存緊張時,Dirty頁可能會被交換到外存或被壓縮,非Dirty頁可能會被丟棄
  • Swapped — 被交換到外存或被壓縮的部分;低版本設備默認不開啓交換分區,但越獄後可手動設置

值得注意的是,調用malloc()之後,並沒有立即爲其分配物理內存,只有當訪問其中內容而觸發缺頁中斷時,操作系統纔會進一步分配物理內存頁,並建立映射關係(修改頁表)。

另外,關於Dirty Size,和我想象中的有點不同,當使用循環讀取所分配的數組,Resident Size會逐漸上升,這是理所當然的,但Dirty Size也會跟着上升,並且往往比Resident Size少一點,Why??可能需要仔細閱讀XNU的源碼才能找到答案。

 

VMMap

vmmap目前沒有可直接運行於iOS下的版本,需要先在Debug Memory Graph中導出一個.memgraph文件(File -> Export Memory Graph...),再使用vmmap分析,如下圖:

BTW,如果在XCode中使用模擬器(非真機)運行App,這個App其實是Mac上一個的進程哦,直接 $ vmmap pid 即可。

 

準備搜索

使用proc_pidinfo()函數(也可以用vm_region),找出目標進程虛擬內存空間中所有可寫的區域,待用。

void rx_mem_scan::init_regions() {
    struct proc_regioninfo region_info;
    kern_return_t ret;
    uint64_t addr = 0;
    int count = 0;

    _regions_p = new regions_t();
    do {
        if (addr) {
            boolean_t writable = (region_info.pri_protection & VM_PROT_DEFAULT) == VM_PROT_DEFAULT;

            if (writable) {
                region_t region;
                region.address = region_info.pri_address;
                region.size = region_info.pri_size;
                _regions_p->push_back(region);
                count ++;
            }

            // printf("%016llx - %016llx, %d, %d\n", region_info.pri_address, region_info.pri_address + region_info.pri_size, writable, region_info.pri_user_tag);
        }

        ret = proc_pidinfo(_target_pid, PROC_PIDREGIONINFO, addr, &region_info, sizeof(region_info));
        addr = region_info.pri_address + region_info.pri_size;
    } while (ret == sizeof(region_info));

    _trace("Writable region count: %d\n", (int)_regions_p->size());
}

將所有區域信息打印出來,看起來和上面vmmap的輸出是吻合的,其中pri_user_tag對應region type,定義見vm_statistics.h

 

讀取內存

使用vm_read_overwrite()函數,從目標進程"讀取"內存。注意,這個函數與vm_read()不同,應該並沒有做實際的數據拷貝,而是將region.address ~ region.address + region.size範圍對應的所有映射狀態同步給了region_data ~ region_data + region.size,對於Resident的部分,兩個進程中不同的虛擬內存地址對應的應該是相同的物理內存地址。

inline kern_return_t rx_mem_scan::read_region(data_pt region_data, region_t &region, vm_size_t *read_count) {
    kern_return_t ret = vm_read_overwrite(_target_task,
            region.address,
            region.size,
            (vm_address_t) region_data,
            read_count);
    return ret;
}

 

搜索字符串

在目標進程虛擬內存空間中,依次讀取所有可寫的區域並且搜索。代碼如下:

void rx_mem_scan::search_str(const std::string &str) {
    int matched_count = 0;
    long begin_time = get_timestamp();

    for (uint32_t i = 0; i < _regions_p->size(); ++i) {
        region_t region = (*_regions_p)[i];        

        vm_size_t raw_data_read_count;
        data_pt region_data_p = new data_t[region.size];
        kern_return_t ret = read_region(region_data_p, region, &raw_data_read_count);

        if (ret == KERN_SUCCESS) {
            //printf("Region address: %p, region size: %d, read count: %d\n", (void *)region.address, (int)region.size, (int)raw_data_read_count);

            data_pt data_itor_p = region_data_p;
            int str_len = str.length();            

            while (raw_data_read_count >= str_len) {
                data_pt str_itor_p = (data_pt)str.c_str();
                bool found = true;

                int i = 0;
                while (i < str_len)
                {
                    if (data_itor_p[i] != str_itor_p[i])
                    {
                        found = false;
                        break;
                    }

                    ++ i;
                }

                if (found)
                {
                    ++ matched_count;

                    while (i < 255 + str_len && i < raw_data_read_count)
                    {
                        if (0 == data_itor_p[i++]) // fast skip 0, for next compare
                        {
                            break;
                        }
                    }

                    int j = -1;
                    while (j > -256 && &data_itor_p[j] >= region_data_p && data_itor_p[j] >= 32) {
                        -- j;
                    }

                    char str_buff[256];

                    printf("\e[1;31mAddress: %p, string: \e[0m", data_itor_p);
                    
                    memcpy(str_buff, &data_itor_p[j + 1], -j - 1);
                    str_buff[-j - 1] = 0;
                    printf("%s", str_buff);

                    printf("\e[0;32m%s\e[0m", str_itor_p);

                    memcpy(str_buff, &data_itor_p[str_len], i - str_len);
                    str_buff[i - str_len] = 0;
                    printf("%s\n", str_buff);

                    raw_data_read_count -= i;
                    data_itor_p += i;
                } else {
                    raw_data_read_count -= 1;
                    data_itor_p += 1;                    
                }
            }

            // free_region_memory(region);
        } else {
            printf("Region address: %p, region size: %d, read failed\n", (void *)region.address, (int)region.size);
        }

        delete[] region_data_p;
    }

    long end_time = get_timestamp();
    printf("Result count: %d, time used: %.3f(s)\n", matched_count, (float)(end_time - begin_time)/1000.0f);
}

 

使用示例

$ ssh 遠程登錄到iPhone,$ ps 得到目標進程id,然後 $ rxmemscan pid,就可以愉快地搜索啦。。

 

有待優化

由於目標進程中的虛擬內存區域應該也存在很多未實際映射(到物理內存或外存)的頁,rx_mem_scan::search_str()中直接對整塊區域進行搜索(訪問),一方面可能會造成rxmemscan自身物理內存佔用升高,一方面也白白耗費搜索時間。如果排除掉未映射的頁,效率應該會有所提升吧。

 


參考資料

http://icetime.cc/2020/02/03/2020-02/關於iOS內存的深入排查和優化/

https://juejin.im/post/5a5e13c45188257327399e19

http://vlambda.com/wz_x6ylln0XQd.html

https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/vm/vm.html

https://www.cnblogs.com/murongxiaopifu/archive/2020/02/24/12357406.html

https://github.com/apple/darwin-xnu

https://book.douban.com/subject/25870206/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章