Cycript 使用和动态注入原理

2,698 阅读3分钟

介绍

Cycript 允许开发人员通过 Objective-C/JavaScript 语言动态修改和调试 iOS 和 Mac OS X 上运行的 App。 (它也支持在 Android 和 Linux 上调试 App 进程。)

安装和使用

1.越狱 iOS 设备, 打开 Cydia 安装 Cycript 插件

2.连接越狱 iOS 设备

// PC电脑需要和 iOS 设备处于同一局域网, 10.100.170.85 为 iOS 设备局域网 ip 地址
// iOS 设备默认root密码是 alpine
ssh root@10.100.170.85 

3.查看当前加载到内存中的进程信息(进程id,进程名称,进程路径)

ps -A
ps -A | grep Preferences

图片.png 4.attach 到 app 进程, 例如系统的设置 App

cycript -p Preferences

常用命令

Objective-C 方法调用

[UIApp description]

// 查看当前的KeyWindow
var keyWindow = UIWindow.keyWindow()

keyWindow

// 隐藏状态栏
[UIApp setStatusBarHidden:YES];

调用 c 函数

extern "C" int getuid();
getuid()

fabsf(-5.0)

var a = malloc(128)

内存地址

// 查看指定内存地址的对象或方法信息
#0x108e3cd80
// 查看一个对象的所有属性和方法
*#0x108e3cd80

查看对象的属性和方法

// 查看一个对象的所有属性 
[obj _ivarDescription].toString()
// 查看一个对象的所有方法
[obj _methodDescription].toString() 

查看 view

// 格式化输出当前View的层级关系
keyWindow.recursiveDescription().toString()

// 显示当前View下的所有的UISwitch
choose(UISwitch)
[#0x113e40110 setOn:YES]

自定义 cy 文件

1.编写 tool.cy 代码

// 获取AppID
BWAPPID = NSBundle.mainBundle.bundleIdentifier;
// 获取沙盒目录
BWAPPPATH = NSBundle.mainBundle.bundlePath;

BWRootVC = function(){
    return UIApp.keyWindow.rootViewController;
};

BWGetCurrentVCFromRootVC = function(rootVC){
    var currentVC;
    if([rootVC presentedViewController]){
        rootVC = [rootVC presentedViewController];
    }
    
    if([rootVC isKindOfClass:[UITabBarController class]]){
        currentVC = BWGetCurrentVCFromRootVC(rootVC.selectedViewController);
    }else if([rootVC isKindOfClass:[UINavigationController class]]){
        currentVC = BWGetCurrentVCFromRootVC(rootVC.visibleViewController);
    }else{
        currentVC = rootVC;
    }
    return currentVC;
};

// 获取当前VC
BWCurrentVC = function(){
    return BWGetCurrentVCFromRootVC(BWRootVC());
};

2.将cy文件放到 iOS 设备上

scp /Users/BowenJin/Desktop/tool.cy root@10.100.170.85:/usr/lib/cycript0.9

3.导入和使用cy模块

@import tool
BWRootVC()
BWAPPID

Cycript 动态调试实现原理

git.saurik.com/cycript.git 下载源码

// pid 需要注入的进程 id, argv 是控制台输入的命令(代码)
void InjectLibrary(int pid, std::ostream &stream, int argc, const char *const argv[]) {
    auto cynject(LibraryFor(reinterpret_cast<void *>(&main)));
    auto slash(cynject.rfind('/'));
    _assert(slash != std::string::npos);
    cynject = cynject.substr(0, slash) + "/cynject";

    auto library(LibraryFor(reinterpret_cast<void *>(&MSmain0)));

#if defined(__APPLE__) && (defined(__i386__) || defined(__x86_64__))
    off_t offset;
    _assert(csops(pid, CS_OPS_PIDOFFSET, &offset, sizeof(offset)) != -1);

    // XXX: implement a safe version of this
    char path[4096];
    int writ(proc_pidpath(pid, path, sizeof(path)));
    _assert(writ != 0);

    auto fd(_syscall(open(path, O_RDONLY)));

    auto page(getpagesize());
    auto size(page * 4);
    auto map(_syscall(mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset)));

    _syscall(close(fd)); // XXX: _scope

    auto header(reinterpret_cast<mach_header *>(map));
    auto command(reinterpret_cast<load_command *>(header + 1));

    switch (header->magic) {
        case MH_MAGIC_64:
            command = shift(command, sizeof(uint32_t));
        case MH_MAGIC:
            break;
        default:
            _assert(false);
    }

    bool ios(false);
    for (decltype(header->ncmds) i(0); i != header->ncmds; ++i) {
        if (command->cmd == LC_VERSION_MIN_IPHONEOS)
            ios = true;
        command = shift(command, command->cmdsize);
    }

    _syscall(munmap(map, size)); // XXX: _scope

    auto length(library.size());
    _assert(length >= 6);
    length -= 6;

    _assert(library.substr(length) == ".dylib");
    library = library.substr(0, length);
    library += ios ? "-sim" : "-sys";
    library += ".dylib";
#endif

    std::ostringstream inject;
    inject << cynject << " " << std::dec << pid << " " << library;
    for (decltype(argc) i(0); i != argc; ++i) {
        inject << " '";
        for (const char *arg(argv[i]); *arg != '\0'; ++arg)
            if (*arg != ''')
                inject.put(*arg);
            else
                inject << "'\''";
        inject << "'";
    }

    FILE *process(popen(inject.str().c_str(), "r"));
    _assert(process != NULL);

    for (;;) {
        char data[1024];
        auto writ(fread(data, 1, sizeof(data), process));
        stream.write(data, writ);

        if (writ == sizeof(data))
            continue;
        _assert(!ferror(process));
        if (feof(process))
            break;
    }

    auto status(pclose(process)); // XXX: _scope (sort of?)
    _assert(status != -1);
    _assert(status == 0);
}

cynject 注入程序

打开 iOS 的 /usr/bin 目录下 cynject 的程序, 使用反编译工具打开分析

  1. 在函数subb308的位置可以看到,通过调用taskfor_pid获取到进程句柄结构。通过该结构,可以对进程能够有访问权限。

图片.png

mach_port_t rtask;
task_for_pid(mach_task_self(), pid, &rtask);

程序为了让内存中的dylib有执行能力,把dylib通过线程的方式来加载。继续往下看,就发现进程创建一个被挂起的线程 图片.png 拿到句柄结构,在进程的空间中申请内存,将dylib映射之后,写入到这片申请的空间里面。 所以,代码逻辑大概是这样的: 图片.png

vm_size_t codeSize = 124;
vm_address_t rAddress;
vm_allocate(rtask, &rAddress, codeSize, TRUE);
vm_write(rtask, rAddress, &code, (mach_msg_type_number_t)codeSize);
vm_protect(rtask, rAddress, codeSize, FALSE, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE);

最后,等dylib加载完全后,为dylib恢复启动并执行使其开始运行 图片.png 梳理一下大体的流程:

(1)获取 PID 的进程句柄
(2)在 PID 中创建一个被挂起的线程
(3)在 PID 进程中申请一片用于加载 DYLIB 的内存
(4)调用 RESUME ,恢复线程

更多内容

官网详细使用文档: www.cycript.org/manual/