介绍
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
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 的程序, 使用反编译工具打开分析
- 在函数subb308的位置可以看到,通过调用taskfor_pid获取到进程句柄结构。通过该结构,可以对进程能够有访问权限。
mach_port_t rtask;
task_for_pid(mach_task_self(), pid, &rtask);
程序为了让内存中的dylib有执行能力,把dylib通过线程的方式来加载。继续往下看,就发现进程创建一个被挂起的线程 拿到句柄结构,在进程的空间中申请内存,将dylib映射之后,写入到这片申请的空间里面。
所以,代码逻辑大概是这样的:
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恢复启动并执行使其开始运行 梳理一下大体的流程:
(1)获取 PID 的进程句柄
(2)在 PID 中创建一个被挂起的线程
(3)在 PID 进程中申请一片用于加载 DYLIB 的内存
(4)调用 RESUME ,恢复线程
更多内容
官网详细使用文档: www.cycript.org/manual/