iOS逆向分析笔记
layout: wiki
title: iOS逆向分析笔记
categories: Reverse_Engineering
description: iOS逆向分析笔记
keywords:
url: https://lichao890427.github.io/ https://github.com/lichao890427/
- Android命令和IOS命令对照关系
- Mac/iOS环境配置
- 分析工具
- Debug for mac&ios
- Trace/Hook for mac/ios
- Jailbreak Development Tools
- Mac&iOS file format analysis
- Objective C Reversing
- iOS Attack&Defense
Android命令和IOS命令对照关系
Android命令 | iOS命令 | |
---|---|---|
安装应用 | adb install -r <pkgname> | 真机安装:fruitstrap -b UCWEB.app/XXX.ipa 模拟器安装:xcrun simctl install booted <XXX.app/XXX.ipa> ideviceinstaller -i |
卸载应用 | adb uninstall -k <pkgname> | crun simctl erase [device ID] ideviceinstaller -u |
查看设备 | adb devices | instruments -s devices xcrun simctl list |
打开进程 | am start | open |
端口转发 | adb forward | tcprelay.py |
Mac/iOS环境配置
Mac环境配置
安装brew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装wget
brew install binutils
brew install wget
brew install python
安装pip
wget https://bootstrap.pypa.io/get-pip.py
sudo su
python get-pip.py
pip install -U pip
安装应用
pip install frida
brew install gcc
brew install llvm
brew install automake
brew install cmake
brew install git
brew install gdbre
https://sourceware.org/gdb/wiki/BuildingOnDarwin
codesign -s gdb-cert /usr/local/bin/gdb
iOS环境配置
安装CYDIA工具:
OpenSSH 基本命令
Tcpdump
AppSync 绕过系统验证,随意安装ipa
Apple File Conduit 安装后可在手机助手显示系统目录
samba windows 文件共享
syslog 日志存放在/var/log/syslog
安装python pip
Ncdu du command
Lsof lsof command
File file command
Less less command
Cyscript
Apt struct 提供apt-get
Adv-cmds finger fingerd lsvfs last md ps
File-cmds chflags compress ipcrm ipcs pax
Basic-cmds msg uudecode uuencode write
Shell-cmds killall mktemp renice time which
System-cmds iostat login passwd sync sysctl
Diskdev-cmds mount quota fsck fstyp fdisk tunefs
Network arp ifconfig netstat route traceroute
Syslog syslogon syslogoff /var/log/syslog
Wget
GNU Debugger ar nm objdump ranlib strip addr2line c++filt gdb objcopy objdump readelf
(compile your code with –mcpu=arm1176jzf-s)
CYDIA常用源:
http://apt.thebigboss.org
http://apt.saurik.com
http://apt.modmyi.com
http://repo666.ultrasn0wn.com
http://ctdua.zodttd.com
http://apt.weifeng.com
http://apt.feng.com
http://repo.feng.com
http://repo.xarold.com
http://julio.xarold.com
http://crak.cn/repo/
http://iphone.tgbus.com/cydia/
https://build.frida.re
分析工具
Android | Mac | iOS | |
---|---|---|---|
跟踪工具 | strace ltrace Introspy | dtruss dtrace | Frida Cycript Introspy |
文件操作 | adb push/pull | scp | |
日志 | logcat | idevicesyslog | /var/log/syslog |
调试工具 | gdb jdb IDA gkidbg | gdb IDA lldb | gdb IDA lldb gikdbg |
Hook框架 | XPosed/Cydia Substrate | Cydia Substrate | |
静态分析 | IDA dex2jar Apktool jadx jeb jd-gui | IDA classdump iNalyzer Hopper | IDA classdump iNalyzer Hopper |
Class-Dump用法
Class-dump是mac上的命令行工具用于解析Objective-C类接口,class-dump-z修复了一些bug并使用c++重写从而在mac, linux, win平台上运行,不支持x64 iphone,因此如果要解析mac os x程序的类,要用原始class-dump,而解析iphone的类使用class-dump-z
-
class-dump
运行于mac的工具用于解析objectc 运行时信息,生成classes, categories protocols,和otool –ov的结果类似,以objectivec语法表示更可读 -
Class-dump-z
速度快,便携且兼容各系统,修正ivar偏移处理,结构体名可读性高,属性化,隐藏继承和代理方法,参数名可读,修正头文件生成等
若由于AppStore加密等原因无法直接用classdump导出类的情况下,可以用cycript脚本weak_classdump在运行时进行同等操作
IDA反汇编
寻找oc函数调用栈
对于OC语法由于是通过消息机制进行函数调用的,因此无法直接找到调用者,这里通过脚本解决
import idc
def addxref(x, y, z):
"""
add reference for objc_meth_addr <=> objc_methname_addr <=> msgsend_call_addr
:param x: msgsend_call_addr
:param y: objc_meth_addr
:param z: objc_methname_addr
:return: nothing
"""
AddCodeXref(x, y, XREF_USER | fl_F)
# AddCodeXref(y, x, XREF_USER | fl_F)
# AddCodeXref(x, z, XREF_USER | fl_F)
# AddCodeXref(z, x, XREF_USER | fl_F)
AddCodeXref(y, z, XREF_USER | fl_F)
# AddCodeXref(z, y, XREF_USER | fl_F)
def addobjcref():
"""
add reference for math-o file
:return: nothing
"""
objc_meth_map = {}
methnamebegin = 0
methnameend = 0
forbitmeth = [
"alloc",
"allocWithZone:",
"allowsWeakReference",
"autorelease",
"class",
"conformsToProtocol:",
"copy",
"copyWithZone:",
"dealloc",
"debugDescription",
"description",
"doesNotRecognizeSelector:",
"finalize",
"forwardingTargetForSelector:",
"forwardInvocation:",
"hash",
"init",
"initialize",
"instanceMethodForSelector:"
"instanceMethodSignatureForSelector:",
"instancesRespondToSelector:",
"isEqual",
"isKindOfClass:",
"isMemberOfClass:",
"isProxy",
"isSubclassOfClass:",
"load",
"methodForSelector:",
"methodSignatureForSelector:",
"mutableCopy",
"mutableCopyWithZone:",
"performSelector:",
"performSelector:withObject:",
"performSelector:withObject:withObject:",
"respondsToSelector:",
"release",
"resolveClassMethod:",
"resolveInstanceMethod:",
"retain",
"retainCount",
"retainWeakReference",
"superclass",
"zone",
".cxx_construct",
".cxx_destruct",
]
# find the segment which contains objc method names
curseg = FirstSeg()
while curseg != 0xffffffff:
if "__objc_methname" == SegName(curseg):
methnamebegin = SegStart(curseg)
methnameend = SegEnd(curseg)
break
curseg = NextSeg(curseg)
# get objc method names
if methnamebegin != 0:
while methnamebegin < methnameend:
funcname = GetString(methnamebegin)
objc_meth_map[funcname] = methnamebegin
methnamebegin = methnamebegin + len(funcname) + 1
# get objc func table
funcmap = {}
addr = PrevFunction(-1)
while addr != 0xffffffff:
curname = GetFunctionName(addr)
if -1 != curname.find('['):
curname = curname.replace("[", "").replace("]", "")
curname = curname.split(" ")[1]
# may be more than one function with same sel but differenct class
if curname not in funcmap:
funcmap[curname] = []
funcmap[curname].append(addr)
addr = PrevFunction(addr)
# make xref
for (k, v) in objc_meth_map.items():
# find corresponding func addr
if k in funcmap and k not in forbitmeth:
farr = funcmap[k]
# find xref to code and make xref for each
curref = DfirstB(v)
while curref != 0xffffffff:
for f in farr:
addxref(curref, f, v)
curref = DnextB(v, curref)
print "added xref for " + k
if __name__ == "__main__":
addobjcref()
正常显示unicode中文字符
由于ida使用python对中文支持不好,这里通过脚本解决一定程度的问题
def find_utf16_string(addr):
start = SegStart(addr)
end = SegEnd(addr)
addr = start
while addr < end:
# get length
len = 1
while Name(addr + len) == "":
len = len + 1
totalstr = ""
for i in range(0, len, 2):
if Word(addr + i) > 0x100:
# read an unicode char
bytes = GetString(addr + i, 2)
try: # some chinese character not supported by python
comm = bytes.decode("utf-16")
if type(comm) == unicode:
comm = comm.encode("gbk")
else:
comm = '?'
except Exception as e:
comm = '?'
else:
# extract as ascii
comm = chr(Word(addr + i))
totalstr = totalstr + comm
MakeComm(addr, totalstr)
addr = addr + len
tofind = ["__ustring"]
seg = FirstSeg()
while seg != 0xffffffff:
if SegName(seg) in tofind:
find_utf16_string(seg)
seg = NextSeg(seg)
Debug for mac&ios
lldb调试
- 安装lldb和usb调试环境
brew install lldb libplist libusb usbmuxd ldid
wget http://cgit.sukimashita.com/usbmuxd.git/snapshot/usbmuxd-1.0.8.tar.bz2
tar xjfv usbmuxd-1.0.8.tar.bz2
cd usbmuxd-1.0.8/python-client/
python tcprelay.py -t 22:22 留作iphone命令行操作
python tcprelay.py –t 23946:23946 留作iphone调试
- 签名debugserver使之可以附加
- 创建签名文件entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
</dict>
</plist>
- 签名完成后送入终端
codesign -s - --entitlements entitlements.plist -f debugserver
scp debugserver root@127.0.0.1:/bin/
上述过程在Xcode经历一次调试后自动完成,debugserver位于iOS /Developer/usr/bin
- 拷贝ARMDisassembler,提升代码可读性
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/?.?/DeveloperDiskImage.dmg的/Library/PrivateFrameworks/ARMDisassembler.framework
scp –r –p ARMDisassembler.framework root@127.0.0.1:/System/Library/PrivateFrameworks
- 启动lldb-server
./bin/debugserver
debugserver host:port [program-name program-arg1 program-arg2 ...] 启动调试
debugserver host:port --attach=<pid> 进程id附加调试
debugserver host:port --attach=<process_name> 进程名附加调试
- 启动lldb-client
lldb -> process connect connect://127.0.0.1:23946
IDA调试
由于lldb扩展了RSP协议(gdb remote serial debug protocol), Ida调试使用原始gdb调试协议,功能没有lldb和gikdbg强,对于app只能下断点跟踪,功能十分有限,目前还在研究协议转换中。由于默认编译的程序会有PIE标志,导致模块地址随机化,ida无法直接附加,因此使用010Editor删除PIE标志,上传到远程机器后chmod 777改为可执行即可。步骤如下:
- 使用010editor等工具将可执行文件去除pie标志(mach_header的flags MH_PIE=0x200000,注意选择正确的架构)
- 拷贝可执行文件并载入到ida:scp root@127.0.01:/path/to/file /path/to/file
- 启动server端:debugserver –x backboard *:1234 /path/to/file (或附加调试)
- 转发端口:python tcprelay.py –t 1234:1234
- 根据程序架构选择ida(x86 x64)设置ida为gdb调试,设置入口断点,设置调试地址和端口为127.0.0.1:1234,即可
Gikdbg调试
官网http://www.gikir.com/product.php,由ollydbg进一步开发的面向android和ios的汇编语言调试工具,支持静态分析elf/mach-o文件和动态调试android/iOS App,目前只支持arm系统,该软件运行在window上,适合调试dylib和可执行文件和简单的app
- 1.配置服务器
从官网下载gikdbg
scp $(GIKDBG)/iserver/gikir_iserver.deb root@127.0.0.1:/var/tmp
ssh root@127.0.0.1
dpkg -i /var/tmp/gikir_iserver.deb
重启后打开gikir_server app(清除占用6080端口的进程)
另一种安装方式是添加cydia源http://apt.feng.com/geekneo,安装gikir_iserver
- 2.启动客户端
执行Gikdbg.exe
iDebug/Login(USB)登录
iDebug/File/Attach附加调试 Open启动调试
目前gikdbg可以调试控制台、动态库、app程序,支持usb/wifi,支持注入动态库,首次调试的程序需要打补丁:
- 1) 删除MH_PIE标志,让进程每次加载基址固定;
- 2) 记录App的UUID值;
- 3) 如果是FAT格式的App则禁用最低以及最高的架构版本;
- 4) 如果是加密的App则解密该App;
- 5) 注入调试辅助动态库gikir_iserver_injecter.dylib;
Trace/Hook for mac/ios
系统支持
mac&ios进程加载器dyld提供了设置环境变量DYLD_INSERT_LIBRARIES 的方式向目标进程注入动态库,另外mac&ios系统支持的hook为在mach-o的__DATA __interpose节数据,源码如下,编译成mac和ios的binary即可,该法适用于普通程序,不适用于app,因为app无法用命令行直接启动
#include <unistd.h>
#include <fcntl.h>
typedef struct interpose_s{
void* new_func;
void* orig_func;
} interpose_t;
int my_open(const char*, int ,mode_t);
__attribute__((used))
const interpose_t interposers[] __attribute__ ((section("__DATA, __interpose"))) =
{
{(void*)my_open, (void*)open},
};
int my_open(const char* path, int flags, mode_t mode)
{
int ret = open(path, flags, mode);
printf("%d = open %s\n",ret, path);
return ret;
}
void init() __attribute__((constructor));
void init()
{
printf("im in\n");
}
//gcc -dynamiclib l.c -o 1.dylib -Wall // compile to dylib
// lichao26de-iPhone:/tmp root# DYLD_INSERT_LIBRARIES=interpose.dylib cat 1
//im in
//3 = open 1
OC支持
load重写使该类在第一次加载时交换swizzled_setHidden和setHidden函数指针,导致调用swizzled_setHidden实际调用的是setHidden,反之亦然
#import <objc/runtime.h>
@implementation UIView(Loghiding)
- (BOOL)swizzled_setHidden {
NSLog(@"We're calling setHidden now!");
BOOL result = [self swizzled_setHidden];
return result;
}
+ (void)load {
Method original_setHidden;
Method swizzled_setHidden;
original_setHidden = class_getInstanceMethod(self, @selector(setHidden));
swizzled_setHidden = class_getInstanceMethod(self, @selector(swizzled_setHidden));
method_exchangeImplementations(original_setHidden, swizzled_setHidden);
}
@end
Frida
著名的跨平台注入&跟踪工具,普通安装方式pip install frida,越狱ios上安装方式:
- 添加源http://build.frida.re,安装frida,确保27042 27043端口不被占用
- 启动frida-server ./usr/sbin/frida-server
- 转发端口 python tcprelay.py –t 27042:27042 27043:27043
Cycript
著名的注入&跟踪工具,支持iOS/Mac/Android,支持ObjC/JavaScript1.7/C++11语法
远程连接Cycript
hcy=dlopen(”libcycript.dylib”,1) (可以使用各种方式将libcycript.dylib加载到进程中)
CYListenServer=(typedef void(short))(dlsym(hcy,”CYListenServer”))
CYListenServer(111) 调用函数
tcprelay –t 111:111 host上转发端口
cycript –r 127.0.0.1:111 host上连接server
编译JS
echo "[x*x for each(x in [1,2,3])]" | cycript -c > x.js
cat x.js
(function($cyv,x){$cyv=[];(function($cys){$cys=[1,2,3];for(x in $cys){x=$cys[x];$cyv.push(x*x)}})();return $cyv})()
?命令
?bypass 忽略错误
?debug 调试输出开关
?lower
?exit
?reparse 显示换行等字符
?syntax 语法高亮
?gc 强制js垃圾回收
语法特点
JS type | ObjC type |
---|---|
number | NSNumber (CFNumber) |
boolean | NSNumber (CFBoolean) |
string | NSString |
Array | NSArray |
object | NSDictionary |
[[NSArray arrayWithObjects:
[NSNumber numberWithInt:41],
"foo",
[NSNumber numberWithBool:YES],
[NSArray arrayWithObjects:[NSNumber numberWithInt:8], [NSNumber numberWithInt:6], nil],
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:12], "a",
[NSNumber numberWithInt:46], "b",
nil],
[NSNumber numberWithInt:36],
nil] indexOfObject:"foo"]
可以直接简写为:[[41,"foo",true,[8,6],{a:12,b:46},36] indexOfObject:"foo"]
兼容OC语法
[obj msg:var]
@selector(selname)
obj->ivar
*ptr 打印结构体或类成员
objc->[key] 获取实例的某成员
&var 获取Objc实例地址
@class classname : superclass {} 定义Objc类
+ methodname {function body}
- methodname {function body}
@end
new classname
@”str” 等价于”str”
[super …]
Selector(selname) 声明selector
Functor(function body, type encoding) 定义ObjC函数
new Functor(function(x,y){return (x+y).toString(16);}, "*dd") (double, double) → char*
block = ^ int (int value) { return value + 5; } 声明block
基本功能
-
导入js模块
import "/tmp/test.js"
-
导入cy模块
@import com.saurik.substrate.MS
(对应/usr/lib/cycript0.9/com/saurik/substrate/MS.cy) -
导入nodejs/cy模块
util=require(“util”)
utils=require(“/tmp/utils.cy”)
-
返回上一次执行结果
_
-
获取可执行程序参数
system.args
-
指针类型转换
Pointer(address, type encoding) 函数地址转换为encoding指定类型
Instance(address) 对象地址转换为ObjC对象地址
pt=(typedef int*)(oldpt) 强制类型转换
- 定义结构体
CGRect=(typedef struct {int x;int y;})
rect = new (struct CGRect)
rect.size 获取结构体大小
-
定义数组
arr=new (typedef char[10])
-
获取对象类型
obj.class
OC运行时功能
-
获取所有类
ObjectiveC.classes
-
获取所有接口
ObjectiveC.protocols
-
获取某实例所有方法
[MYCLASS (tab-key)]
-
获取实例的所有变量
*obj
-
由内存地址获取对象
[#0x18b6c8d0 show]
-
获取所有类实例
choose(SBIconView)
-
获取成员函数类型描述
selector.type(class)
selector(copyWithZone:).type(NSString) => @12@0:4^{_NSZone=}8.
- 修改函数
var oldm = NSObject.prototype.description
NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; }
[new NSObject init]
#"<NSObject: 0x100d11520> (of doom)"
调试功能
- 获取加载模块
utils.get_dyld_info()
ObjectiveC.images
-
修改内存权限
utils.mprotect(addr, size, utils.constants.PROT_READ)
-
读写内存
var foo = new int
*foo = 0x12345678
utils.hexdump(foo, 4)
- 获取当前回溯栈
function bt(){
return [NSThread callStackSymbols];
}
-
执行命令
utils.getOutputFromTask(“/bin/ls”, [])
-
调用函数
[obj msg: var] 调用oc函数
fopen(“/tmp”,”r”) 调用c函数
utils.apply("printf", ["%s %.3s, %d -> %c, float: %f\n", "foo", "barrrr", 97, 97, 1.5]) 反射调用c函数
- 反汇编
var method = class_getInstanceMethod(NSNumber,@selector(intValue));
var imp = method_getImplementation(method);
utils.disasm(imp, 10)
0x7fff83363b8c 1 55 push rbp
0x7fff83363b8d 3 4889e5 mov rbp, rsp
0x7fff83363b90 2 4157 push r15
0x7fff83363b92 2 4156 push r14
0x7fff83363b94 2 4155 push r13
- 汇编
var n = [NSNumber numberWithLongLong:10]
var method = class_getInstanceMethod([n class], @selector(longLongValue));
var imp = method_getImplementation(method);
utils.asm(imp, 'mov eax, 42; ret;')
Hook功能
- 记录OC函数调用(需要substrate)
utils.logify(NSNumber, @selector(numberWithDouble:))
[NSNumber numberWithDouble:1.5] //触发logifyt
2014-07-28 02:26:39.805 cycript[71213:507] +[<NSNumber: 0x10032d0c4> numberWithDouble:1.5]
注意:对静态成员函数,第一参为object_getClass(类名);对普通成员函数,第一参为object_getClass(实例)
底层实现:
cy# @import com.saurik.substrate.MS
cy# var oldm = {};
cy# MS.hookMessage(NSObject, @selector(description), function() {
return oldm->call(this) + " (of doom)";
}, oldm)
cy# [new NSObject init]
#"<NSObject: 0x100203d10> (of doom)"
- 记录C函数调用(需要substrate)
utils.logifyFunc("fopen", 2)
apply("fopen", ["/etc/passwd", "r"]); //触发logifyt
2015-01-14 07:01:08.009 cycript[55326:2042054] fopen(0x10040d4cc, 0x10040d55c)
cy# @import com.saurik.substrate.MS
cy# extern "C" void *fopen(char *, char *);
cy# var oldf = {}
cy# var log = []
cy# MS.hookFunction(fopen, function(path, mode) {
var file = (*oldf)(path, mode);
log.push([path.toString(), mode.toString(), file]);
return file;
}, oldf)
cy# fopen("/etc/passwd", "r");
(typedef void*)(0x7fff774ff2a0)
cy# log
[["/etc/passwd","r",(typedef void*)(0x7fff774ff2a0)]]
其他功能
-
获取所有控件元素
utils.find_subviews()
-
获取所有viewcontroller
utils.find_subview_controllers()
-
获取cpu类型
utils.getCpuType()
-
获取坐标
manager=choose(CLLocationManager)[0]
[manager location]
- 获取bundleid
NSBundle.mainBunble.bundleIdentifier
其他CYCRIPT模块
- cycript-utils https://github.com/Tyilo/cycript-utils/blob/master/utils.cy
- weak-dump 运行时的class-dump
- classdump-dyld weak-dump升级版,支持x64
``
iOS实例分析
-
方式一:MachO格式注入
在mach-o格式中增加LOAD_DYLIB command节,添加dylib,重签名即可 -
方式二:调试器(LLDB/GDB/Cycript等)注入
LLDB/GDB:po dlopen("/usr/lib/test.dylib",1)
Cycript:dlopen("/usr/lib/test.dylib",1) 调试状态下也可使用cycript
- 方式三:MobileLoader注入
CydiaSubstrate的MobileLoader组件用于加载第三方dylib给指定程序,MobileLoader首先在启动时使用DYLD_INSERT_LIBRARIES加载自身,之后加载/Library/MobileSubstrate/DynamicLibraries下的所有动态库,由于是全局的默认会在所有程序中加载,可以采用过滤配置plist文件加载dylib(iOS9以后必须存在plist才准予加载),plist文件名与dylib名相同: - Bundle:必须匹配app(s)的bundle-id才准予加载
- Classes:必须在目标进程中实现指定类(s)才准予加载
- Executables:必须匹配可执行文件名才准予加载
Filter = {
Executables = (“mediaserverd”);
Bundles = (“com.apple.sprintboard”, “net.whatsapp.WhatsApp”);
Mode = “Any”
};
- 方式一:Cydia Hook框架
MSImageRef MSMapImage(const char* file) 加载dylib
cont void* MSImageAddress(MSImageRef image)
bool MSHookProcess(pid_t pid, const char* library) 远程线程方式(vm_)注入dylib
MSImageRef MSGetImageByName(const char* file) 获取模块基址,优于dlopen
Void* MSFindSymbol(MSImageRef image, const char* name) 获取函数地址,优于dlsym
char* MSFindAddress(MSImageRef image, void** address)
Void MSHookFunction(void* symbol, void* replace, void** result) hook c函数
IMP MSHookMessage(Class _class, SEL sel, IMP imp, const char* prefix) hook oc消息
Void MSHookMessageEx(Class _class, SEL sel, IMP imp, IMP* result) hook oc消息
void MSHookClassPair(Class target, Class hook, Class old) 封装MSHookMessageEx
Hook c函数底层实现仍然是arm汇编的inline hook
Hook oc函数底层实现则是利用objective-c runtime function
对于 rettype funcname(type1 param1, type2 param2)的函数:
hook c function 方式1 -- MSHookFunction:
rettype (*old_funcname)(type1 param1, type2 param2);
rettype new_funcname(type1 param1, type2 param2)
{
……….work before hook……….
old_funcname(param1, param2);
……….work after hook…………
}
MSHookFunction((void*) funcname, (void*)&new_funcname, (void**)&old_funcname);
hook c function 方式2 – MSHookFunction-MSHook-MSHack:
MSHook(rettype, funcname, type1 param1, type2 param2)
{
……….work before hook……….
_funcname(param1, param2);//注意前面加’_’
……….work after hook…………
}
MSHookFunction(funcname, MSHake(funcname))
hook oc function 方式1 - MSHookMessageEx
hook oc function 方式2 - MSHookClassPair
hook oc function 方式3 - MSHookInterface
1. Theos越狱框架开发
优点:方便,一键部署,缺点:调试麻烦
$THEOS/bin/nic.pl
iphone/tweak
export THEOS_DEVICE_IP=???
make package install
2. XCode开发
特点:和前者相反,调试方便,只需要如前述修改mach-o type为可执行程序即可调试
#include <CydiaSubstrate.h>
void* handle = dlopen(“libsubstrate.dylib”, 1);
typedef void (*HOOK)(void*, void*, void**);
HOOK MSHookFunction = (HOOK)dlsym(handle, “MSHookFunction”);
MSHookFunction((void*)funcname, (void*)&oldfunc, (void**)&newfunc);
- 方式二:frida/frida-trace
frida安装:mac/linux/win下执行pip install frida,iOS上从frida源安装服务端,安好后服务端每次开机启动,占用端口27042/27043,因此在客户端执行python tcprelay.pt –t 27042:27042 27043:27043
frida-ps –R 枚举所有进程
frida-ps –R –a 枚举所有app进程
frida-ps –R –a –i 枚举所有安装的app及其bundle name
frida-trace –R –p PID 附加到进程(按进程id)
frida-trace –R –n name 附加到进程(按进程名,例如百度商户)
frida-trace –R –f FILE 拉起进程并跟踪(例如com.baidu.bshoppush)
hook c function frida-trace –i “recv*” –i “send*” ….
hook oc function frida-trace –m “-[NS* draw*]” …
实例:跟踪商户app二维码操作
frida-trace -R -f com.baidu.bshoppush -m "-[QRCode* *]" -f com.baidu.bshoppush
对生成的js进行编辑,自定义输出数据可以在控制台得到相应显示
获取JSPatch下发代码:
frida-trace –U –f com.baidu.waimai –m “+[JPEngine *evaluate*]”
js脚本内容
var data=new ObjC.Object(args[2]);
log(data.toString());
log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n") + "\n");
Jailbreak Development Tools
Theos
$ $THEOS/bin/nic.pl
NIC 1.0 - New Instance Creator
------------------------------
[1.] iphone/application
[2.] iphone/library
[3.] iphone/preference_bundle
[4.] iphone/tool
[5.] iphone/tweak
Choose a Template (required): 1
Project Name (required): iPhoneDevWiki
Package Name [com.yourcompany.iphonedevwiki]: net.howett.iphonedevwiki
Authour/Maintainer Name [Dustin L. Howett]:
Instantiating iphone/application in iphonedevwiki/...
Done.
$
IOSOpenDev
用于XCode的越狱程序开发插件http://iphonedevwiki.net/index.php/IOSOpenDev
Mac&iOS file format analysis
otool 类似于objdump,可以解析objc类信息
class-dump objc类接口信息解析成可读objc头文件
OBJC_HELP 环境变量打日志
OBJC_HELP=1 ./build/Debug/HelloWorld
objc: OBJC_HELP: describe Objective-C runtime environment variables
objc: OBJC_PRINT_OPTIONS: list which options are set
objc: OBJC_PRINT_IMAGES: log image and library names as the runtime loads
them
NSObjCMessageLoggingEnabled 环境变量用于打印objc_msgSend调用日志
NSObjCMessageLoggingEnabled=Yes ./hello
Hello World!
-[dcbz@megatron:~/code/HelloWorld/build]$ cat /tmp/msgSends-6686
+ NSRecursiveLock NSObject initialize
+ NSRecursiveLock NSObject new
+ NSRecursiveLock NSObject alloc
....
+ Talker NSObject initialize
+ Talker NSObject alloc
+ Talker NSObject allocWithZone:
- Talker NSObject init
- Talker Talker say:
- Talker NSObject release
- Talker NSObject dealloc
machoview 查看格式的gui工具 https://github.com/gdbinit/MachOView.git
dtrace 跟踪mac上objective-c函数调用
分析iOS二进制文件的过程:
- 1.如果是app store下载的app,需要先用工具砸壳,将代码数据区内存解密
- 2.从手机将文件拷贝到主机使用ida分析
- 3.将砸壳生成的文件修改PIE标志并重新签名,替换原始app,方便动态分析
砸壳
由于class-dump等工具的流行,App Store上发布的软件都经过加密处理(LC_ENCRYPTION_INFO所标志的区域),加载器dyld对可执行文件校验,根据fat头选择合适的架构,处理所有的command,使用posix_spawn函数启动进程。ios上所有第三方代码都需要使用developer id代码签名,而代码签名作为数据存储在mach-o格式command结构中,因此fat格式中得每个架构的文件都分别签名,并由内核验证,如果验证失败则会收到停止信号而退出。在越狱机上可以通过ldid进行伪签名通过签名校验。进行了加密后,无法直接用ida查看内部结构
-
dumpencrypted
https://github.com/stefanesser/dumpdecrypted/blob/master/dumpdecrypted.c,该工具注入目标进程内存,利用解密后的内存转储数据得到脱壳文件,时机在dyld加载后,init(__mod_init_func)节加载前 -
clutch
https://github.com/KJCracks/Clutch/releases,命令行工具。该工具使用posix_spawn函数以暂停态(POSIX_SPAWN_START_SUSPENDED)和ASLR关闭模式创建目标程序子进程,从而使目标进程不执行任何代码而得到系统解密的内存,后使用task_for_pid从mach port得到目标进程内存,最后更新头部的LC_ENCRYPTION_COMMAND,合并成文件。
mach-o格式分析
相关数据结构定义在/Developer/SDKs/iPhoneOS.sdk/usr/include/mach-o/loader.h
,总体结构包括:header结构、command表、数据区。header结构:用于指明cpu类型(x86?arm?...),文件类型(动态库?可执行文件?...),command表位置;如果文件中包含多个cpu的可执行文件,则会存在FAT header头指明每个cpu的文件位置,因此一个mach-o文件的开头可能是mach_header结构,此时文件只包含一种cpu架构的可执行文件,也可能是fat_header,存储不同mach_header的偏移
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; //静态库.a 目标文件.o 动态库.dylib 可执行文件 ………….
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
command表相当于pe的节表,描述文件和内存进行映射的表,包括__PAGEZERO(标记可执行文件的第一个节)、__TEXT、__DATA、__OBJC(objective-c运行库表用于描述类信息)、__IMPORT、__LINKEDIT(符号、字符串、重定位表),常用的command如下:
LC_SEGMENT/LC_SEGMENT_64 描述文件中得节和内存映射关系
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
LC_LOAD_DYLIB 要加载的动态库
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};
LC_MAIN 描述入口点
struct entry_point_command {
uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */
uint32_t cmdsize; /* 24 */
uint64_t entryoff; /* file (__TEXT) offset of main() */
uint64_t stacksize;/* if not zero, initial stack size */
};
LC_LOAD_DYLINKER 描述mach-o可执行文件加载器
struct dylinker_command {
uint32_t cmd; /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
LC_DYLD_ENVIRONMENT */
uint32_t cmdsize; /* includes pathname string */
union lc_str name; /* dynamic linker's path name */
};
LC_CODE_SIGNATURE 用codesign和ldid(plist)签名生成的结构,用于突破沙盒等权限限制
struct linkedit_data_command {
uint32_t cmd; /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
LC_DYLIB_CODE_SIGN_DRS or
LC_LINKER_OPTIMIZATION_HINT. */
uint32_t cmdsize; /* sizeof(struct linkedit_data_command) */
uint32_t dataoff; /* file offset of data in __LINKEDIT segment */
uint32_t datasize; /* file size of data in __LINKEDIT segment */
};
LC_ENCRYPTION_INFO/LC_ENCRYPTION_INFO_64 用于appstore加密程序
struct encryption_info_command {
uint32_t cmd; /* LC_ENCRYPTION_INFO */
uint32_t cmdsize; /* sizeof(struct encryption_info_command) */
uint32_t cryptoff; /* file offset of encrypted range */
uint32_t cryptsize; /* file size of encrypted range */
uint32_t cryptid; /* which enryption system,
0 means not-encrypted yet */
};
LC_SYMTAB 符号表
LC_UUID 文件唯一标识
App目录和文件
用户App位置/var/mobile/Applications/[GUID]/
- AppName.app 目录存放app静态数据和代码
- Documents目录存放持久化数据,和iTunes同步;包括sql数据库
- Library目录存放配置文件、缓存和cookie
- tmp目录存放临时文件
Objective C Reversing
研究方式:命令行编译+二进制对比+调试
Debug: clang/gcc –g -fobjc-arc -framework Foundation FKPerson.m main.m
Release: clang/gcc –O3 -fobjc-arc -framework Foundation FKPerson.m main.m
交叉编译arm: clang/gcc -x objective-c -arch armv7 -g -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk -framework Foundation main.m (arch=i386 x86_64 armv7 arm64)
objective-c编译为c++源码:clang/gcc –rewrite-objc –framework Foundation main.m
- meta-class 每个类都存在元类,
- super-class 父类
- root-class 根类
- selector 选择器(存储为字符串,其内存位置与方法一一对应)
- imp 普通函数指针
- id 通用数据类型
杂项
字符串存储:
@”” => 实际编译为CFString结构
Class CFString : objc_object
{
longlong info;
char* data;//真正的字符串存储位置
longlong length;//字符串长度
}
synchronized锁:
@synchronized(expression1){
expression2;
}
实际编译生成为:
id lock = expression1
objc_sync_enter(lock)
expression2;
objc_sync_exit(lock)
选择器@selector:
@selector(x) => 实际编译为”x”
关键字@encode:
@encode(type) => 实际编译为 该类型的描述符
关键字@autorelease:
@autorelease{expression;} => 实际编译为
objc_autoreleasePoolPush(…)
expression;
objc_autoreleasePoolPop
快速枚举for:
for(type a in b) {expression;}=> 实际编译为
for(int i=0;i<b.selRef_countByEnumeratingWithState_objects_count_;i++){
expression;
}
nil值:=> (void*)0
Function
传参所用寄存器,适用于普通函数和成员函数(id,sel)
arm架构:
a1 R0
a2 R1
a3 R2
a4 R3
a5 [sp+0]
a6 [sp+4]
…. arm64架构:
a1 W0
a2 W1
a3 W2
a4 W3
a5 W4
a6 W5
a7 W6
a8 [sp+0]
a9 [sp+8]
a10 [sp+16]
a11 [sp+24]
…….
X86架构(默认调用约定):
a1 [esp+0]
a2 [esp+4]
a3 [esp+8]
a4 [esp+8]
a5 [esp+12]
a6 [esp+16]
…….
x86_64架构:
a1 rdi
a2 rsi
a3 rdx
a4 rcx
a5 r8
a6 r9
a7 [rsp+0]
a8 [rsp+8]
……
Block
用于定义匿名函数,等价于lambda表达式,形式如下:
^ [返回值类型] (类型1 形参1, 类型2 形参2, ...)
{
}
定义Block变量形式如下:
返回值类型 (^块变量名) (类型1, 类型2, ...);
int (^hypot)(int, int) = ^(int num1, int num2)
{
returnnum1 * num1 + num2 * num2;
};
NSLog(@"%d",hypot(3,4));
编译得到:
v3= ((int (__fastcall *)(_QWORD, _QWORD, _QWORD))*(&__block_literal_global8 +2))(&__block_literal_global8, 3LL, 4LL);
NSLog(&cfstr_D, (unsigned int)v3);
其中__block_literal_global8将函数等相关信息封装成类(这点和vs-win一致),___main_block_invoke_2正是函数体实现:
__const:0000000100001060 ___block_descriptor_tmp7dq 0 ; DATA XREF:__const:0000000100001098o
__const:0000000100001068 dq 20h
__const:0000000100001070 dq offset aI16@?0i8i12 ; "i16@?0i8i12"
__const:0000000100001078 align 20h
__const:0000000100001080___block_literal_global8 dq offset __NSConcreteGlobalBlock
__const:0000000100001080 ; DATAXREF: _main+87o
__const:0000000100001088 dq 50000000h
__const:0000000100001090 dq offset ___main_block_invoke_2
__const:0000000100001098 dq offset___block_descriptor_tmp7
从源码Block_private.h可以得到构造的Block结构体
struct Block_layout
{
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);//实际调用的函数
struct Block_descriptor_1 *descriptor;
// imported variables
};
struct Block_descriptor_1
{
uintptr_t reserved;
uintptr_t size;
};
struct Block_descriptor_2
{
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
struct Block_descriptor_3
{
const char *signature;
const char *layout; //contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
从内部实现看,Block代码能生成3种类型:
NSGlobalBlock 代码中未操作外部变量或操作全局变量(如上例)
NSStackBlock 代码中操作外部栈变量
NSMallocBlock 代码中操作外部堆变量
下面分别讨论
第一种情况:
代码为最开始的例子,可见其中没有用到外部变量
实际产生的代码为:
int ___main_block_invoke(Block_layout this,int num1, int num2)
{
returnnum1 * num1 + num2 * num2;
}
Block_layout __block_literal_global =
{
__NSConcreteGlobalBlock,
0x50000000,
0,
&___main_block_invoke,
&___block_descriptor_tmp,
}
__block_literal_global. ___main_block_invoke(&__block_literal_global,3, 4);
第二种情况:
代码如下
__blockint my = argc;
int(^hypot)(int, int) = ^(int num1, int num2)
{
my+= 1;
returnnum1 * num1 + num2 * num2;
};
NSLog(@"%d%d", hypot(3, 4), my);
实际产生的代码为:
block_descriptor ___block_descriptor_tmp =
{
0,
28,
___copy_helper_block_,
___destroy_helper_block_,
"i16@?0i8i12",
16
};
int ___main_block_invoke(Block_layout this,int num1, int num2)
{
this->___stack_variable->my+= 1;
returnnum1 * num1 + num2 * num2;
}
Block_layout __block_literal_global =
{
__NSConcreteStackBlock,
0xC2000000,
0,
&___main_block_invoke,
&___block_descriptor_tmp,
&___stack_variable//存放所有栈变量
};
void __copy_helper_block_()
{
_Block_object_assign(my,argc)
}
void __destroy_helper_block_()
{
_Block_object_dispose(my,argc)
}
__block_literal_global.___block_descriptor_tmp.___copy_helper_block_();
__block_literal_global. ___main_block_invoke(&__block_literal_global,3, 4);
..................
__block_literal_global.___block_descriptor_tmp.___destroy_helper_block_();
第三种情况:
需要开启arc,暂无研究
Class
类型定义
Object描述通用对象,所有类继承自该类 struct objc_object{
Class isa; //描述类型
}
Class描述类,相当于模板,创建实例和使用静态方法时使用 struct objc2_class : objc2_object{//runtime/
//Class isa; //Class对象的Class即meta class
Class superclass; //父类Class
cache_t cache; //缓存调用过的成员函数
objc2_class_rw* data;
}
class_rw动态类数据,内存中呈现形式 struct objc2_class_rw{//runtime/objc-runtime-new.h class_rw_t
int flags; //标志位
int version;
objc2_class_ro* ro;
method_array_t methods; //链表结构方便随时添加函数
property_array_t properties
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char* demangledName;
}
类属性flags
RW_REALIZING 0x80000 class has started realizing
RW_HAS_INSTANCE_SPECIFI
class_ro静态类数据,二进制文件中呈现形式,在初始化后设置REALIZE转化成新结构class_rw struct objc2_class_ro{//runtime/objc-runtime-new.h class_ro_t
int flags; //标志位
int instanceStart; //在Instance中第一个ivar偏移
int instanceSize; //Instance大小
int reserved;
byte* ivar_layout;
char* name; //对应类名
objc2_meth_list* base_meths; //类拥有的成员方法(静态成员在metaclass中)
objc2_prot_list* base_prots; //类遵守的接口
objc2_ivar_list* ivars; //类拥有的成员变量
byte* weak_ivar_layout;
objc2_prop_list* base_props; //使用@property定义的属性
}
类属性flags
RO_META 1 meta-class
RO_ROOT 2 root-class
RO_HAS_CXX_STRUCTORS 4 has .cxx_construct/destruct
RO_HAS_LOAD_METHOD 8 has +load
RO_HIDDEN 16 visibility=hidden
RO_EXCEPTION 32 has attribute(objc_exception)
RO_REUSE_ME 64 available for reassignment
RO_IS_ARR 128 class compiled with –fobjc-arc
RO_HAS_CXX_DTOR_ONLY 256 has .cxx_destruct but no .cxx_construct
RO_FROM_BUNDLE 0x20000000 class is in unloadable bundle
RO_FUTURE 0x40000000 class is unrealized future
RO_REALIZED 0x80000000 class is realized
Instance——实例结构,操作实例或类成员函数中使用 struct objc_instance : objc_object{
//Class isa;
Member1; //成员变量1,2,3….
Member2;
}
Method List——描述类结构中包含的成员函数 struct objc2_method_list{
int entrySize; //每个objc2_method结构大小
int count; //后接count个objc2_method
}
Method——描述单个成员函数 struct objc2_method {
SEL method_name //方法名 setName:andAge:
char *method_types //方法类型 v28@0:8@16i24
IMP method_imp //实际地址 ptr of setNameandAge
}
成员函数指针定义:typedef id (*IMP)(id, SEL, ...);
函数修饰符method_types runtime.h
‘b’-bitfield
‘B’-bool
‘c’-char
‘C’-uchar
‘d’-double
‘f’-float
‘i’-int
‘I’-uint
‘l’-long
‘L’-ulong
‘n’-in for input
‘N’-inout both for input and output
‘o’-out for ouput
‘O’-bycopy instead of using a proxy/NSDistantObject, pass or return a copy of the object
‘q’-longlong
‘Q’-ulonglong
‘r’-const constant
‘R’-byref use a proxy(default)
‘s’-short
‘S’-ushort
‘v’-void
‘V’-oneway 允许在不同线程和进程使用,不可阻塞调用线程直到返回
‘^’-pointers
‘@’-object
‘[‘-array begin
‘]’-array end
‘{‘-structure begin
‘}’-structure end
‘(‘-union begin
‘)’-union end
‘#’-class
‘:’-selector
‘*’-char pointer
‘%’-atom
‘!’-vector
‘?’-undefine
Structure: returntype—stacksize—[argumenttype—bitoffset]*
v28@0:8@16i24 -> void stacksize=28 (pointer, selector, pointer, int)
Ivar List struct objc2_ivar_list{
int entrySize; //每个objc2_ivar结构大小
int count; //后接count个objc2_ivar
}
Ivar struct objc2_ivar{
int* offset; //存储该变量相对Instance结构偏移
char* name; //变量名
char* type; //类型描述符
int alignment_raw; //对齐
int size; //变量占用空间
}
Protocol List——描述遵守的接口 struct objc2_protocol_list{
longlong count;//后接count个Protocol
}
Protocol——描述单个接口 struct objc2_protocol : objc_object{
//Class isa;
char* mangledName;
objc2_protocol_list* protocols
objc2_method_list* instanceMethods;
objc2_method_list* classMethods;
objc2_method_list* optionalInstanceMethods;
objc2_method_list* optionalClassMethods;
objc2_method_list* instanceProperties;
int size;
int flag;
char** extendedMethodTypes;
char* _demangledName;
}
Property List——描述使用@property关键字定义的成员变量(和普通成员变量分开存放) struct objc2_prop_list{
int entrySize; //每个objc2_prop结构大小
int count; //后接count个objc2_prop
}
Property——描述单个@property成员变量 struct objc2_prop{
char* name;
char* attributes;// T@"NSString",&,V_a1
}
返回普通类型的静态成员函数调用
[FKPerson foo] objc_msgSend([FKPerson class], “foo”)
void _cdecl foo(FKPerson* self, SEL selector)
返回普通类型的普通成员函数调用
[person say] objc_msgSend(person, “say”)
void _cdecl say(FKPerson * self, SEL selector)
返回普通类型的多参数成员函数调用
[person setName:@”1” andAge:500] objc_msgSend(person, “setName:andAge:”, @”1”, 500)
void _cdecl setName:andAge:(FKPerson* self, SEL selector, NSString* name, int age)
成员函数中调用父类函数,父函数返回普通类型
[super init] objc_msgSendSuper(make_super super, “init”)
返回栈结构体的成员函数调用
[person func] objc_msgSend_stret(person, “func”)
成员函数中调用父类函数,父函数返回栈结构体
[super func] objc_msgSendSuper_stret(self, “func”)
返回栈浮点数 arm:不使用objc_msgSend_fpret
i386:float|double|long double使用objc_msgSend_fpret
x86-64:long double使用objc_msgSend_fpret
成员函数分析
- 1.每增加一个成员函数,类模板会增加method,由于名称一一对应,同一个类不允许存在同名函数
- 2.每个成员函数前两个参数分别是实例指针self和选择器SEL,之后才是用户为其定义的参数
- 3.带(+)修饰的成员函数本质为静态成员,属于该类的meta-class类成员,因此位于meta-class函数表中,而普通成员函数位于该类的函数表中
- 4.和c++不同的是,成员函数调用方式和普通函数相同,因此可以通过反射替换成普通函数
成员变量分析
- 1.每增加一个成员变量,类模板Class会增加ivar,以后使用该类模板创建的实例的对象结构也会增加该元素
- 2.只要有一个实际使用的成员变量,就会产生”类名.cxx_destruct”析构函数
- 3.根据成员变量属性为weak/strong,在进行赋值操作时采用objc_storeWeak/objc_storeStrong,默认类型为strong
- 4.对public成员变量的操作语法采用myclass->field形式,产生的逻辑也和c结构体相同,而更常规的方式是将成员变量写成@property中,这样编译器会自动为成员变量生成相应的getter和setter函数,使用kvc(键值编码)时会自动调用(msgsend)这些函数
meta-class存在的原因
- 1.直接从类对象进行的操作,例如调用静态成员函数,并不属于某个实例,因此需要存在于类类型中
- 2.当自身被子类化(setsuperclass)时,父类并不等同于所属类(isa != superclass),同理构造一个类要提供其isa
Objc_msgSend调用流程
[图片上传失败...(image-6df688-1516663329548)]
- 1.根据对象的isa找到类,在类的dispatch table中查找selector
- 2.如果未找到则找到该类的父类,并在父类的dispatch table中查找selector,直到NSObject类(该过程中优先查找cache)
- 3.如果所有子类和父类都无法找到该函数,则进行msgForward,如果用户添加了动态实现(resolveInstanceMethod)则调用
- 4.如果上一步失败,则尝试找到一个能响应该消息的对象(forwardingTargetForSelector),如果能找到则转发给他
- 5.如果上一步失败,则尝试获取一个方法签名(methodSignatureForSelector),如果获取不到直接抛异常
- 6.调用用户自己实现的forwardInvocation
Runtime Ability
Object id object_copy(id obj, size_t size) 拷贝实例内容
Class object_getClass(id obj) 返回Instance对应的Class
Class object_setClass(id obj, Class cls) 绑定Instance的Class
BOOL object_isClass(id obj) 判断所属
char* object_getClassName(id obj) 获取类名
void* object_getIvar(id obj, Ivar ivar) 获取成员变量值(按Ivar)
void object_setIval(id obj, Ivarl ivar, id value) 设置成员变量值
Ivar object_getInstanceVariable(id obj, char* name, void* value) 获取成员变量值(按变量名)
Ivar object_setInstanceVariable(id obj, char* name, void* value) 设置成员变量值
Class Class objc_getClass(char* name) 返回指定类名的(类型)对象
Class objc_getaMetaClass(char* name) 返回指定类名的meta-class对象
Class objc_lookUpClass(char* name) 返回指定类名且已注册的的(类型)对象
int objc_getClassList(Class* buffer, int bufferCount) 返回所有已注册类
Class* objc_copyClassList(int* outCount) 返回所有已注册类
char* class_getName(Class cls) 获取类名
BOOL class_isMetaClass(Class cls) 是否meta-class
Class class_getSuperClass(Class cls) 获取父类
Class class_setSuperClass(Class cls, Class newSuper)设置父类
int class_getVersion(Class cls) 获取版本
void class_setVersion(Class cls, int version) 设置版本
size_t class_getInstanceSize(Class cls) 获取实例大小
Ivar class_getInstanceVariable(Class cls, char*name) 获取实例Ivar
Ivar* class_copyIvarList(Class cls, int* outCount)
Method class_getInstanceMethod(Class cls, SEL name) 获取非静态方法
Method class_getClassMethod(Class cls, SEL name) 获取静态方法
IMP class_getMethodImplementation(Class cls, SEL name) 获取方法实现
BOOL class_conformsToProtocol(Class cls, Protocol* protocol)是否遵守协议
Method* class_copyMethodList(Class cls, int* outCount)
Protocol* class_copyProtocolList(Class cls, int* outCount)
objc_property_t class_getProperty(Class cls, char* name)
objc_property_t class_copyPropertyList(Class cls, int* outCount)
uchar* class_getIvarLayout(Class cls)
BOOL class_addMethod(Class cls, SEL name, IMP imp, char* types) 增加函数(绑定普通函数)
BOOL class_replaceMethod(Class cls, SEL name, IMP imp, char* types)替换函数
BOOL class_addIvar(Class cls, char* name, size_t size, uchar alignment, char* types)添加变量
BOOL class_addProtocol(Class cls, Protocol* protocol) 增加协议
BOOL class_addProperty(Class cls, char* name, objc_property_attribute_t* attrib,int count)
BOOL class_replaceProperty(Class cls, char* name, objc_property_attribute_t* attrib,int count)
void class_setIvarLayout(Class cls, uchar layout)
id class_createInstance(Class cls, size_t extrabytes) 创建实例
id objc_constructInstance(Class cls, void* bytes) 创建实例
void* objc_destructInstance(id obj)
Class objc_allocateClassPair(Class superclass, char* name, size_t extrabytes) 创建类和元类
void objc_registerClassPair(Class cls) 注册类
Class objc_duplicateClass(Class original, char* name, size_t extraBytes) 复制类
Method SEL method_getName(Method m) 获取函数名
int method_getNumberOfArguments
char* method_getTypeEncoding 获取函数类型字段
void method_getArgumentType 获取参数类型
void method_getReturnType 获取返回值类型
IMP method_getImplementation
IMP method_setImplementation(Method m, IMP imp)设置函数实现
void method_exchangeImplementations(Method m1, Method m2)
Ivar char* ivar_getName(Ivar v) 获取Ivar名
char* ivar_getTypeEncodeing(Ivar v) 获取Ivar类型字段
ptrdiff_t ivar_getOffset(Ivar v) 获取该ivar在instance中得偏移
Attribute ……
Protocol objc_copyProtocolList
protocol_getName
protocol_copyProtocolList
protocol_allocateProtocol
protocol_registerProtocol
protocol_addProtocol
protocol_addProperty
其他 char** objc_copyImageNames(int* outcount) 获取加载的动态库
char* class_getImageName(Class cls) 获取某类所属动态库
char** objc_copyClassNamesForImage(char* image, int* outCount)获取动态库中所有类
objc_loadWeak 获取weak值
objc_storeWeak 设置weak值
objc_setAssociatedObject 设置关联
objc_getAssociatedObject 获取关联
objc_removeAssociatedObjects 移除所有关联,恢复对象到原始状态
[图片上传失败...(image-a1f9cd-1516663329549)]
[图片上传失败...(image-cd3ede-1516663329549)]
[图片上传失败...(image-5133fe-1516663329549)]
[图片上传失败...(image-e6184-1516663329549)]
[图片上传失败...(image-ca05ba-1516663329549)]
其他
arc类型转换:
普通指针和objc指针转换:(实现调试器中任意内存当作类操作)
id obj1 = [[class1 alloc] init];
void* p = (__bridge void*)obj1;
id obj2 = (__bridge id)p;
@property:
@property(?,?,…)用于快速生成类成员及getter setter,其修饰符如下:
atomic 原子操作,线程安全(默认)
objc_getProperty objc_setProperty_atomic
nonatomic 非线程安全 ‘N’
readwrite 具有setter getter(默认)
readonly 具有getter ‘R’
assign 简单赋值(默认)
copy setter方法中深度复制传入对象 ‘C’
objc_getProperty objc_setProperty_atomic_copy
retain setter方法中对传入对象引用计数加一 ‘&’
strong 强引用(默认),和retain相似 ‘&’
初始化/赋值= 销毁objc_storeStrong
weak 对象消失后指针置nil ‘W’
初始化objc_initWeak 赋值objc_loadWeakRetained 销毁objc_destroyWeak/objc_autoreleaseReturnValue
__unsafe_unretain 对象引用计数不加一,对象释放后不置nil
autorelease 对象加入自动释放池 对应objc_autorelease
异常处理
objc提供异常处理机制
@try{
expr1;
}
@catch(NSException* ex){
expr2;
}
@finally{
expr3;
}
产生的流程如下:
......
flag = 0
expr1
label1:
expr3
...
if(flag & 1)
objc_exception_rethrow()
return
tail:
if(..)
{
expr2;
}
goto label1;
@throw语句层产生:objc_exception_throw()
Reflection
Objective-C是一种反射型语言,可以在运行时获取和修改自身状态,其中的实现存在于libobjc.A.dylib库中,这些“运行时”能力源于objective-c类结构组织较为灵活,并提供了操作自身结构的接口,同时在生成的可执行文件(mach-o)中存在_OBJC节,这些节中提供了足够的类构成信息,而Mac端gdb可以解析这些结构,而正由于objc提供了如此多的信息,因此也比c++在同等情况下逆向难度低一些。
LC_SEGMENT.__OBJC.__cat_cls_meth
LC_SEGMENT.__OBJC.__cat_inst_meth
LC_SEGMENT.__OBJC.__string_object
LC_SEGMENT.__OBJC.__cstring_object
LC_SEGMENT.__OBJC.__message_refs
LC_SEGMENT.__OBJC.__sel_fixup
LC_SEGMENT.__OBJC.__cls_refs
LC_SEGMENT.__OBJC.__class
LC_SEGMENT.__OBJC.__meta_class
LC_SEGMENT.__OBJC.__cls_meth
LC_SEGMENT.__OBJC.__inst_meth
LC_SEGMENT.__OBJC.__protocol
LC_SEGMENT.__OBJC.__category
LC_SEGMENT.__OBJC.__class_vars
LC_SEGMENT.__OBJC.__instance_vars
LC_SEGMENT.__OBJC.__module_info
LC_SEGMENT.__OBJC.__symbols
java与objc反射对比
objc | java | |
---|---|---|
获取类 | NSClassFromString myClass.class [myClass class] | Class.forName myClass.class |
检查继承 | isKindOfClass isMemberOfClass conformsToProtocol | class.isAssignableFrom instanceOf class.isInstance |
获取函数 | @selector NSSelectorFromString | getMethod |
调用函数 | perfromSelector objc_msgSend | invoke |
iOS Attack&Defense
AntiDebug - AntiAntiDebug
- sysctl P_TRACED标志 检测调试
可以检测调试器和跟踪器,但是不能检测注入和cycript:
#include <sys/types.h>
#include <sys/sysctl.h>
static int check_debugger( ) __attribute__((always_inline));
int check_debugger( )
{
size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc info;
int ret,name[4];
memset(&info, 0, sizeof(struct kinfo_proc));
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();
if((ret = (sysctl(name, 4, &info, &size, NULL, 0)))){
return ret; //sysctl() failed for some reason
}
return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}
- ptrace PT_DENY_ATTACH 防止调试
可以阻止调试器附加:
#import <dlfcn.h>
#import <sys/types.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif // !defined(PT_DENY_ATTACH)
void disable_gdb() {
void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
#ifdef __arm__
asm volatile(
“mov r0,#31\n”
“mov r1,#0\n”
“mov r2,#0\n”
“mov r12,#26\n”
“svc #80\n”
#endif
#ifdef __arm64__
asm volatile(
“mov x0,#26\n”
“mov x1,#31\n”
“mov x2,#0\n”
“mov x3,#0\n”
“mov x16,#0\n”
“svc #128\n”
#endif
直接附加调试器会产生segmentation fault:11 启动调试程序会在ptrace执行后退出
调试已经被调试的进程:直接失败产生日志:(os/kern) invalid task Exiting
- 反反调试:hook相应函数
#import <substrate.h>
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif
static int (*_ptraceHook)(int request, pid_t pid, caddr_t addr, int data);
static int $ptraceHook(int request, pid_t pid, caddr_t addr, int data) {
if (request == PT_DENY_ATTACH) {
request = -1;
}
return _ptraceHook(request,pid,addr,data);
}
%ctor {
MSHookFunction((void *)MSFindSymbol(NULL,"_ptrace"), (void *)$ptraceHook, (void **)&_ptraceHook);
}
- isatty检测调试
isatty函数在给定文件描述符被附加到调试器控制台时返回1,否则返回0
if(isatty(1)){
NSLog(@”Being Debugged isatty”);
}
else{
NSLog(@”isatty() bypassed”);
}
- task_get_exception_ports检测调试
调试器通常会监听异常端口,因此可以用task_get_exception_ports循环遍历以校验该端口是否设置
struct ios_execp_info{
exception_mask_t masks[EXC_TYPES_COUNT];
mach_port_ports[EXC_TYPES_COUNT];
exception_behavior_t behaviors[EXC_TYPES_COUNT];
thread_state_flavor_t flavors[EXC_TYPES_COUNT];
mach_msg_type_number_t count;
}
struct ios_execp_info* info = malloc(sizeof(struct ios_execp_info));
kern_return_t kr = task_get_exception_ports(mach_task_self(),EXC_MASK_ALL,info->masks,&info->count,info->ports
,info->behaviors,info->flavors);
for(int i=0;i<info->count;i++){
if(info->ports[i] != 0 || info->flavors[i] == THREAD_STATE_NONE){
NSLog(@”Beging debugged”);
}
else{
NSLog(@“bypassed”);
}
}
-
RESTRICT节——防注入
加载器dyld(ios7.0以后)源码中关于DYLD环境变量的逻辑pruneEnvironmentVariables
switch (sRestrictedReason) {
case restrictedNot:
break;
case restrictedBySetGUid:
dyld::log("main executable (%s) is setuid or setgid\n", sExecPath);
break;
case restrictedBySegment:
dyld::log("main executable (%s) has __RESTRICT/__restrict section\n", sExecPath);
break;
case restrictedByEntitlements:
dyld::log("main executable (%s) is code signed with entitlements\n", sExecPath);
break;
}
3种情况下DYLD环境变量会被忽视
- 1.可执行文件设置了setuid setgid位
- 2.可执行文件有__restrict节
- 3.可执行文件有特殊代码签名
由于受app store的限制,1和3都不能实现,而2可以设置Other linker flags为-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
使用该方法可以禁止dylib的注入,在生成的mach-o文件中会多出一个__RESTRICT节
这种方式是可以防止启动注入和运行时注入的,尝试用dumpdecrypted脱壳时会产生类似如下的日志:
dyld: warning, LC_RPATH @executable_path/Frameworks in /var/mobile/Applications/[id]/?.app/* being ignored in restricted program because of @executable_path
尝试用cycript附加会产生如下输入:
dlopen(/usr/lib/libcript.lib, 5): Library not loaded: /System/Library/PrivateFrameworks/JavaScriptCore.framework/JavaScriptCore
referenced from: /usr/lib/libcript.dylib
reason: image not found
*** _assert(status == 0):../Inject.cpp(143):InjectLibrary
而使用lldb则可以正常附加调试
- anti-anti-debug
改restrict节名,重签名(ldid –S)即可
JailBreak Detect – Anti JailBreak Detect
沙盒完整性检测
iOS设备上,用户app安装在/var/mobile/Application中受沙盒限制,而系统app安装在/Application中不受沙盒限制。越狱设备上很多第三方app也安装在/Application下从而不受沙盒限制而拥有更多权限。一些越狱工具会移除沙盒限制以允许特定行为(如fork vfork popen)
int result = fork();
if(!result)
exit(0);
if(result >= 0)//jail broken
{sandbox_is_compromised = 1};
监测点2:在沙盒中,执行opendir(“/dev”)会返回NULL
监测点3:system() getgid() ??
文件系统检测
检测常见的越狱工具目录和文件是否存在
struct stat s;
int is_jailbroken = stat(“/Applications/Cydia.app”, &s) == 0;
常见的目录和文件
/Applications/MxTube.app
/Applications/blackra1n.app
/Applications/RockApp.app
/Applications/WinterBoard.app
/Applications/SBSettings.app
/Library/LaunchDaemons/com.openssh/sshd.plist
/Applications/IntelliScreen.app
/Library/MobileSubstrate/DynamicLibraries/Veency.plist
/Applications/FakeCarrier.app
/private/var/mobile/Library/SBSettings/Themes
/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist
/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist
/System/Library/LaunchDaemons/com.ikey.bbot.plist
/Applications/Icy.app
/Applications/Loader.app
/private/var/tmp/cydia.log
/Library/MobileSubstrate/MobileSubstrate.dylib
/private/var/stash
/private/var/lib/apt
/private/var/lib/cydia
/usr/libexec/cydia
/usr/libeec/sftp-server
/var/cache/apt
/var/lib/apt
/var/lib/cydia
/var/log/syslog
/var/tmp/cydia.log
/var/tmp/cydia.log
/bin/bash
/bin/sh
/usr/sbin/sshd
/bin/mv
/usr/libexec/ssh-keysign
/etc/ssh/sshd_config
/etc/apt
检测装载点
检测fstab
越狱工具会替换/etc/fstab文件导致变小,IOS5上正常为80字节
struct stat s;
stat(“/etc/fstab”, &s);
return s.st_size;
statfs函数检测
在非越狱机上statfs(“/”)应该返回如下标志:buf->f_flags = MNT_RDONLY + MNT_ROOTFS + MNT_DOVOLFS + MNT_JOURNALED + MNT_MULTILABEL,同时statfs(“/var/mobile/Container/Data/Application/<APP_GUID>”)应该返回如下标志:buf->f_flags = MNT_NOSUID + MNT_NODEV + MNT_DOVOLFS + MNT_JOURNALED + MNT_MULTILABEL
检测软链接
检测/Appliations软链接,越狱工具会将其替换到/var/stash/…下
struct stat s;
if(lstat(“/Applications”, &s) != 0)[
if(s.st_mode & S_IFLNK)
exit(-1);
}
其他软链接
/Library/Ringtones
/Library/WallPaper
/usr/arm-apple-darwin9
/usr/include
/usr/libexec
/usr/share
/var/stash/Library/Ringtones
/var/stash/usr/include
/var/stash/Library/WallPaper
/var/stash/usr/libexec
/var/stash/usr/share
/var/stash/usr/arm-apple-darwin9
URL Scheme检测
在越狱机上Cydia会创建一个cydia://的URL scheme,因此如果调用该Scheme返回成功则机器越狱
[NSURL URLWithString @”cydia://package/com.example.package”]
系统内核环境变量检测
越狱时会增加2个内核环境变量用于绕过iOS代码签名机制,sysctlbyname函数用于检测系统信息,在非越狱机上,下面值应该为1
sysctlbyname(security.mac.proc_enforce)
sysctlbyname(security.mac.vnode_enforce
检测DYLD_INSERT_LIBRARIES
检测DYLD_INSERT_LIBRARIES是否存在,越狱环境下会出现”/Library/MobileSubstrate/MobileSubstrate.dylib”,getenv(“DYLD_INSERT_LIBRARIES”)
,检测返回NULL和”\0”
运行进程检测
@try{
NSArray* processes = [self runningProcesses]
for(NSDictionary* dict in processes){
NSString* process = [dict objectForKey:@”ProcessName”];
if([process isEqualToString:@”MobileCydia”]){
return true;
}
else if([process isEqualToString:”Cydia”]){
return true
}
}
}
@catch(NSException* exception){
return 0
}
+ (NSArray*)runningProcesses{
int mib[4] = {CTL_KERN,KERN_PROC,KERN_PROC_ALL,0};
size_t miblen =4;
size_t size;
int st = sysctl(mib,miblen,NULL,&size,NULL,0);
struct kinfo_proc* process = NULL;
struct kinfo_proc* newprocess = NULL;
do{
size += size/10
newprocess = realloc(process,size);
if(!newprocess){
if(process){
free(process);
}
return nil;
}
int st = sysctl(mib,miblen,NULL,&size,NULL,0);
st = sysctl
}while(st == -1 && errno == ENOMEM);
}
if(st == 0){
if(size % sizeof(struct kinfo_proc) == 0){
int nprocess = size/sizeof(struct kinfo_proc);
if(nprocess){
NSMutableArray* array = [[NSMutableArray alloc] init];
for(int I = nprocess – 1;I >= 0;i--){
NSString* processID = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_pid];
NSString* processName = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_comm];
NSString* processPriority = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_priority];
NSDate* processStartDate = [NSDate dateWithTimeInternvalSince1970:process[i].kp_proc.p_un.__p_starttime.tv_sec];
NSDictionary* dict = [[NSDictionary alloc] initWithObjects:[NSArray arrayWithObjects:processID, processPriority, processName, processStartDate, nil] forKeys:[NSarray arrayWithObject:@”ProcessID”, @”ProcessPriority”, @”ProcessName”, @”ProcessStartDate”, nil]];
[array addObject:dict];
}
free(process);
return array;
}
}
return nil;
}
反检测方式:hook