iOS逆向分析笔记


layout: wiki
title: iOS逆向分析笔记
categories: Reverse_Engineering
description: iOS逆向分析笔记
keywords:
url: https://lichao890427.github.io/ https://github.com/lichao890427/


  • Android命令和IOS命令对照关系
  • Mac/iOS环境配置

    • Mac环境配置
    • iOS环境配置
  • 分析工具

    • Class-Dump用法
    • IDA反汇编

      • 寻找oc函数调用栈
      • 正常显示unicode中文字符
  • Debug for mac&ios

    • lldb调试
    • IDA调试
    • Gikdbg调试
  • Trace/Hook for mac/ios

    • 系统支持
    • OC支持
    • Frida
    • Cycript

      • 远程连接Cycript
      • 编译JS
      • ?命令
      • 语法特点
      • 兼容OC语法
      • 基本功能
      • OC运行时功能
      • 调试功能
      • Hook功能
      • 其他功能
      • 其他CYCRIPT模块
    • iOS实例分析
  • Jailbreak Development Tools

    • Theos
    • IOSOpenDev
  • Mac&iOS file format analysis

    • 砸壳
    • mach-o格式分析
    • App目录和文件
  • Objective C Reversing

    • 杂项
    • Function
    • Block
    • Class

      • 类型定义
      • 成员函数分析
      • 成员变量分析
      • meta-class存在的原因
      • Objc_msgSend调用流程
      • Runtime Ability
      • 其他
    • 异常处理
    • Reflection

      • java与objc反射对比
  • iOS Attack&Defense

    • AntiDebug – AntiAntiDebug
    • JailBreak Detect – Anti JailBreak Detect

      • 沙盒完整性检测
      • 文件系统检测
      • 检测装载点

        • 检测fstab
        • statfs函数检测
        • 检测软链接
        • 其他软链接
      • URL Scheme检测
      • 系统内核环境变量检测
      • 检测DYLD_INSERT_LIBRARIES
      • 运行进程检测

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 %sn",ret, path);
    return ret;
}
void init() __attribute__((constructor));
void init()
{
    printf("im inn");
}
//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: %fn", "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,#31n”
    “mov r1,#0n”
    “mov r2,#0n”
    “mov r12,#26n”
    “svc #80n”
#endif
#ifdef __arm64__
asm volatile(
    “mov x0,#26n”
    “mov x1,#31n”
    “mov x2,#0n”
    “mov x3,#0n”
    “mov x16,#0n”
    “svc #128n”
#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 setgidn", sExecPath);
break;
case restrictedBySegment:
dyld::log("main executable (%s) has __RESTRICT/__restrict sectionn", sExecPath);
break;
case restrictedByEntitlements:
dyld::log("main executable (%s) is code signed with entitlementsn", 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和””

运行进程检测

@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

ARM64 汇编——寄存器和指令

  • iOS 中的 armv7,armv7s,arm64 这些都代表什么?

ARMv7|ARM7s|ARM64都是ARM处理器的指令集
真机32位处理器需要ARMv7,或者ARMv7s架构,
真机64位处理器需要ARM64架构。

  • 处理器和寄存器的位数

32位处理器,能同时处理32位的数据,所以对应寄存器为32位的;
64位处理器,能同时处理64位的数据,所以对应寄存器为64位的;
寄存器位数一般会对应处理器位数,两者一般相等,但也有例外8086

  • ARM 指令长度

ARM处理器用到的指令集分为 ARM 和 THUMB 两种。ARM指令长度固定为32bit,THUMB指令长度固定为16bit。所以 ARM64指令集的指令长度为32bit

  • ARM中字的长度

ARM中 一个word是32位,也就是4Byte大小

寄存器

ARM64 有34个寄存器,包括31个通用寄存器、SP、PC、CPSR。

寄存器 位数 描述
x0-x30 64bit 通用寄存器,如果有需要可以当做32bit使用:WO-W30
FP(x29) 64bit 保存栈帧地址(栈底指针)
LR(x30) 64bit 通常称X30为程序链接寄存器,保存子程序结束后需要执行的下一条指令
SP 64bit 保存栈指针,使用 SP/WSP来进行对SP寄存器的访问。
PC 64bit 程序计数器,俗称PC指针,总是指向即将要执行的下一条指令,在arm64中,软件是不能改写PC寄存器的。
CPSR 64bit 状态寄存器
  • x0-x7: 用于子程序调用时的参数传递,X0还用于返回值传递

  • x0 - x30 是31个通用整形寄存器。每个寄存器可以存取一个64位大小的数。 当使用 r0 - r30 访问时,它就是一个64位的数。当使用 w0 - w30 访问时,访问的是这些寄存器的低32位,如图:

    ARM64 汇编——寄存器和指令
     
    image.png
     
  • CPSR(状态寄存器)

NZCV是状态寄存器的条件标志位,分别代表运算过程中产生的状态,其中:

  • N, negative condition flag,一般代表运算结果是负数
  • Z, zero condition flag, 指令结果为0时Z=1,否则Z=0;
  • C, carry condition flag, 无符号运算有溢出时,C=1。
  • V, oVerflow condition flag 有符号运算有溢出时,V=1。
  • Xcode在真机中运行项目,添加断点,lldb中查看各寄存器状态register read

    ARM64 汇编——寄存器和指令
     
    E2A993F9-F400-46C2-9B4C-E38DD2D6E383.png

指令

  • ARM64经常用到的汇编指令
MOV    X1,X0         ;将寄存器X0的值传送到寄存器X1
ADD    X0,X1,X2     ;寄存器X1和X2的值相加后传送到X0
SUB    X0,X1,X2     ;寄存器X1和X2的值相减后传送到X0
AND    X0,X0,#0xF    ; X0的值与0xF相位与后的值传送到X0
ORR    X0,X0,#9      ; X0的值与9相位或后的值传送到X0
EOR    X0,X0,#0xF    ; X0的值与0xF相异或后的值传送到X0
LDR    X5,[X6,#0x08]        ;X6寄存器加0x08的和的地址值内的数据传送到X5
STR X0, [SP, #0x8]         ;X0寄存器的数据传送到SP+0x8地址值指向的存储空间
STP  x29, x30, [sp, #0x10]    ;入栈指令
LDP  x29, x30, [sp, #0x10]    ;出栈指令
CBZ  ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
CBNZ ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)
CMP  ;比较指令,相当于SUBS,影响程序状态寄存器CPSR 
B/BL  ;绝对跳转#imm, 返回地址保存到LR(X30)
RET   ;子程序返回指令,返回地址默认保存在LR(X30)

其中 MOV 指令只能用于寄存器之间传值,寄存器和内存之间传值通过 LDRSTR

  • ARM指令又一个重要特点就是所有指令都是带有条件的,就是说汇编中就需要根据状态寄存器中的一些状态来控制分支的执行。例如:

    ARM64 汇编——寄存器和指令
     
    131CC631-51C9-4601-AA45-EAC6EA43BEF3.png

    图中指令 b 是跳转指令,后边跟着跳转的条件 eq,那么这个 eq 是什么意思呢?

     

  • ARM指令的结构

    ARM64 汇编——寄存器和指令
    ARM指令编码表

    上图列出了不同种类ARM指令的编码格式,文章开头讲过ARM指令长度固定为 32bit即图中的0-31位。

28-31位是条件码,21-24为操作码,12-19为寄存器编号

上边提到的跳转条件eq实际就是28-31位对应的条件码,但是28-31位都是二进制数据不好记,所以就对二进制的条件码取了好记的助记符,例如 eq

eq英文单词equal的意思,注意这里equal并不是c语言当中==的意思,这里根据状态寄存器的条件标志位Z来判断,如果Z = 1eq成立,如果Z = 0eq不成立,就是NE

  • ARM指令包含4位的条件码列表:
操作码 条件码助记符 标志 含义
0000 EQ Z=1 相等
0001 NE(Not Equal) Z=0 不相等
0010 CS/HS(Carry Set/High or Same) C=1 无符号数大于或等于
0011 CC/LO(Carry Clear/LOwer) C=0 无符号数小于
0100 MI(MInus) N=1 负数
0101 PL(PLus) N=0 正数或零
0110 VS(oVerflow set) V=1 溢出
0111 VC(oVerflow clear) V=0 没有溢出
1000 HI(HIgh) C=1,Z=0 无符号数大于
1001 LS(Lower or Same) C=0,Z=1 无符号数小于或等于
1010 GE(Greater or Equal) N=V 有符号数大于或等于
1011 LT(Less Than) N!=V 有符号数小于
1100 GT(Greater Than) Z=0,N=V 有符号数大于
1101 LE(Less or Equal) Z=1,N!=V 有符号数小于或等于
1110 AL 任何 无条件执行(默认)
1111 NV 任何 从不执行

ARM指令所有指令都是带有条件的,默认是AL即无条件执行,当指令带有默认条件时不需要明确写出。

看个例子:
OC代码:

ARM64 汇编——寄存器和指令
D34A79BA-7AAE-471A-BB25-846EF0DBD9D8.png

汇编代码:

ARM64 汇编——寄存器和指令
 
1FF4BE4B-6C1D-49CB-8DB6-FE6C32C2DA18.png

汇编代码注释:

 
ARM64 汇编——寄存器和指令
 
AE06E4F8-8CED-4979-A5F1-7DB5A48EA164.png
  • 后续添加:

adrp是计算指定的符号地址到run time PC值的相对偏移

 - STR Wt, addr

Store Register: stores word from Wt to memory addressed by 
addr.


 - STR Xt, addr

Store Register (extended): stores doubleword from Xt to
 memory addressed by addr.


 - STUR Wt, [base,#simm9]

Store (Unscaled) Register: stores word from Wt to memory addressed by base+simm9.


 - STUR Xt, [base,#simm9]

Store (Unscaled) Register (extended): stores doubleword 
from Xt to memory addressed by base+simm9.


- SCVTF Sm, Ro  

Converts an integer value to a floating-point value.


- FMUL Sd, Sn, Sm   

Multiplies two values.

参考

iOS 中的 armv7,armv7s,arm64,i386,x86_64 都是什么
ARM 指令的条件码
ARM视频教程
iOS开发同学的arm64汇编入门
iOS逆向第五篇(ARM64 汇编)
《iOS应用逆向工程》

php获取客户端的操作系统类型

function get_os(){
    $os='';
    $Agent=$_SERVER['HTTP_USER_AGENT'];
    if (eregi('win',$Agent)&&strpos($Agent'95')){
        $os='Windows 95';
    }elseif(eregi('win 9x',$Agent)&&strpos($Agent'4.90')){
        $os='Windows ME';
    }elseif(eregi('win',$Agent)&&ereg('98',$Agent)){
        $os='Windows 98';
    }elseif(eregi('win',$Agent)&&eregi('nt 5.0',$Agent)){
        $os='Windows 2000';
    }elseif(eregi('win',$Agent)&&eregi('nt 6.0',$Agent)){
        $os='Windows Vista';
    }elseif(eregi('win',$Agent)&&eregi('nt 6.1',$Agent)){
        $os='Windows 7';
    }elseif(eregi('win',$Agent)&&eregi('nt 5.1',$Agent)){
        $os='Windows XP';
    }elseif(eregi('win',$Agent)&&eregi('nt',$Agent)){
        $os='Windows NT';
    }elseif(eregi('win',$Agent)&&ereg('32',$Agent)){
        $os='Windows 32';
    }elseif(eregi('linux',$Agent)){
        $os='Linux';
    }elseif(eregi('unix',$Agent)){
        $os='Unix';
    }else if(eregi('sun',$Agent)&&eregi('os',$Agent)){
        $os='SunOS';
    }elseif(eregi('ibm',$Agent)&&eregi('os',$Agent)){
        $os='IBM OS/2';
    }elseif(eregi('Mac',$Agent)&&eregi('PC',$Agent)){
        $os='Macintosh';
    }elseif(eregi('PowerPC',$Agent)){
        $os='PowerPC';
    }elseif(eregi('AIX',$Agent)){
        $os='AIX';
    }elseif(eregi('HPUX',$Agent)){
        $os='HPUX';
    }elseif(eregi('NetBSD',$Agent)){
        $os='NetBSD';
    }elseif(eregi('BSD',$Agent)){
        $os='BSD';
    }elseif(ereg('OSF1',$Agent)){
        $os='OSF1';
    }elseif(ereg('IRIX',$Agent)){
        $os='IRIX';
    }elseif(eregi('FreeBSD',$Agent)){
        $os='FreeBSD';
    }elseif($os==''){
        $os='Unknown';
    }
    return $os;
}

微擎数据库操作相关函数文档说明

微擎系统数据库操作使用 PDO 兼容方式, 系统已对 PDO 兼容性进行检测及封装. 请使用以下函数进行数据库操作. 所有数据库操作均不进行错误提示, 如果要进行错误调试, 请在系统中配置为开发模式, 然后调用 pdo_debug 方法进行错误输出.

tablename – 转义数据表名
string tablename(string $table)
说明: 获取原始的数据表名, 微擎系统按照惯例在所有的表名增加了前缀来增强兼容性. 使用 tablename 函数, 方便将业务数据表名转换为原始的数据表名来进行数据库操作.
返回: 原始表名, 可以直接用于数据库查询操作
pdo – 初始化 pdo 对象实例
PDO pdo(bool $newinstance = false)
说明: 获取 pdo 对象实例, pdo 函数默认使用缓存起来的 PDO 对象, 可通过 $_W['pdo'] 或 $GLOBALS['pdo'] 方式直接获取默认的 PDO 对象. 如果要使用新的 PDO 实例, 请将 $newinstance 参数设置为 true
参数: $newinstance 是否要创建新实例
返回: PDO 对象
pdo_query – 执行一条非查询语句
int|bool pdo_query(string $sql, array $params = array())
说明: 执行一条非查询语句, 返回受影响的行数
参数:
	 $sql 要进行查询的语句, 其中可以包括查询绑定参数
	 $params 要绑定的参数集合
返回: 如果执行成功返回受影响的记录数, 失败将返回 false
pdo_insert – 执行一条数据插入
int|bool pdo_insert(string $table, array $data = array(), bool $replace = false)
说明: 对某条数据表插入一条新纪录
参数: 
	 $table 指明要操作的数据表
	 $data 要插入的数据记录, 格式为与数据表字段对应的关联数组
	 $replace 指明插入方式使用 INSERT 语句或是 REPLACE 语句(区别请参阅MySQL手册)
返回: 如果执行成功返回受影响的记录数(一般为数字 1), 失败将返回 false
pdo_insertid – 获取上次插入的自增数字主键
int pdo_insertid()
说明: 获得最后一次插入的自增数字主键
返回: 成功将返回数字主键, 失败将返回 0. 使用此函数时, 请注意保证 PDO 实例正确. 只有使用默认实例才有效.
pdo_delete – 删除记录
int|bool pdo_delete(string $table, array $filter = array(), string $gule = 'AND')
说明: 对某条数据表删除纪录
参数: 
	 $table 指明要操作的数据表
	 $filter 删除时要删选的条件数据, 格式为与数据表字段对应的关联数组
	 $gule 指明筛选条件的组合方式, 有效值为 AND(代表所有条件均需满足), OR(其中一种条件满足)
返回: 如果执行成功返回受影响的记录数(删除成功的行数), 失败将返回 false
pdo_update – 执行数据更新
int|bool pdo_update(string $table, array $data = array(), array $filter = array(), string $gule = 'AND')
说明: 对某条数据表更新特定纪录
参数: 
	 $table 指明要操作的数据表
	 $data 要更新的数据记录, 格式为与数据表字段对应的关联数组
	 $filter 删除时要更新的条件数据, 格式为与数据表字段对应的关联数组
	 $gule 指明筛选条件的组合方式, 有效值为 AND(代表所有条件均需满足), OR(其中一种条件满足)
返回: 如果执行成功返回受影响的记录数, 失败将返回 false. 如果数据无变动将会返回数字 0, 注意与错误返回值 false 区别.
pdo_fetchcolumn – 查询 SQL 语句并获得首行指定列的值
mixed|bool pdo_fetchcolumn(string $sql, array $params = array(), int $column = 0)
说明: 执行制定的查询语句, 返回首行指定列的值, 默认返回首行首列
参数:
	 $sql 要进行查询的语句, 其中可以包括查询绑定参数
	 $params 要绑定的参数集合
	 $column 要返回的列
返回: 如果执行成功将返回查询出来的数据结果首行指定列的值, 如果执行失败或指定的列无效将返回bool值 false
pdo_fetch – 按照 SQL 语句查询一条记录
array|bool pdo_fetch(string $sql, array $params = array())
说明: 按照语句及绑定参数获取一条记录, 绑定参数部分请参阅PHP手册 PDO部分
参数:
	 $sql 要进行查询的语句, 其中可以包括查询绑定参数
	 $params 要绑定的参数集合
返回: 如果执行成功将返回查询出来的数据结果(关联数组结构, 格式与查询结果结构相同), 如果执行失败将返回bool值 false
pdo_fetchall – 按照 SQL 语句查询所有记录
array|bool pdo_fetch(string $sql, array $params = array(), string $keyfiled此 = '')
说明: 按照语句及绑定参数获取一条记录, 绑定参数部分请参阅PHP手册 PDO部分
参数:
	 $sql 要进行查询的语句, 其中可以包括查询绑定参数
	 $params 要绑定的参数集合
	 $keyfield 返回的数据结果将以此字段的值为键名呈现为关联数组
返回: 如果执行成功将返回查询出来的数据结果(关联数组结构, 格式与查询结果结构相同), 如果执行失败将返回bool值 false
pdo_debug – 获取数据库操作记录列表
array pdo_debug(bool $output = true, array $append = array())
说明: 获取本次请求周期内所有的数据库操作记录, 也可以用来记录操作记录. 这个函数只有将系统配置中设置为开发模式才会生效
参数: 
	 $output 如果传递 true, 将会直接使用 print_r 打印所有操作记录; 如果传递 false, 则不会打印操作记录, 仅通过返回值返回 
	 $append 用于记录操作记录, 开发者通常不需要使用这个参数
返回: 返回本次请求生命周期内所有的操作记录 

PHP开发微信公众号 应用access_token缓存

在做微信开发时access_token是不可避免的,很多微信接口都需要access_token,这里我就分享一下在web开发中使用PHP如何获取access_token,并写入缓存文件。

首先看看微信官方文档的说明。

access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

公众平台的API调用所需的access_token的使用及生成方式说明:

 

  1. 为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务;
  2. 目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡;
  3. access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。

公众号可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在微信公众平台官网-开发者中心页中获得(需要已经成为开发者,且帐号没有异常状态)。注意调用所有微信接口时均需使用https协议。 

接口调用请求说明

 

 

http请求方式: GET

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

 

{“access_token”:”ACCESS_TOKEN”,”expires_in”:7200}

参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误):

 

{“errcode”:40013,”errmsg”:”invalid appid”}

接下来就上PHP的具体实现方式。

access_token是通过get方式的来取的,所以我们要构建一下curl_get函数,以获取返回结果

 

function curlGet($url){

$ch = curl_init();

$header = “Accept-Charset: utf-8”;

curl_setopt($ch, CURLOPT_URL, $url);

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, “GET”);

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);

curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);

curl_setopt($curl, CURLOPT_HTTPHEADER, $header);

curl_setopt($ch, CURLOPT_USERAGENT, ‘Mozilla/5.0 (compatible; MSIE 5.01; Windows NT 5.0)’);

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

curl_setopt($ch, CURLOPT_AUTOREFERER, 1);

curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$temp = curl_exec($ch);

return $temp;

}

PHP代码

 

/**********获取access_token(基础版)***************/

function getAccessToken($appid,$appsecret){

$url_get=’https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=’.$appid.’&secret=’.$appsecret;

$json=$this->json_decode(curlGet($url_get));

$access_token=$json->access_token

;}

由于asscess_token的过期时间只有7200秒,而接口调用次数是有限制的,所有我们可以把获取asscess_token的函数改进一下,把它存进数据库,然后再进行过期判断。

  • 如果asscess_token还未过期,则直接调用;
  • 如果已过期则获取asscess_token并更新数据库,以务下次使用;
 

function checkAccessToken($appid,$appsecret){

$condition = array(‘appid’=>$appid,’appsecret’=>$appsecret);

$access_token_set=M(‘AccessToken’)->where($condition)->find();//获取数据

if($access_token_set){

//检查是否超时,超时了重新获取

if($access_token_set[‘AccessExpires’]>time()){

//未超时,直接返回access_token

return $access_token_set[‘access_token’];

}else{

//已超时,重新获取

$url_get=’https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=’.$appid.’&secret=’.$appsecret;

$json=json_decode(curlGet($url_get));

$access_token=$json->access_token;

$AccessExpires=time()+intval($json->expires_in);

$data[‘access_token’]=$access_token;

$data[‘AccessExpires’]=$AccessExpires;

$result = M(‘AccessToken’)->where($condition)->save($data);//更新数据

if($result){

return $access_token;

}else{

return $access_token;

}

}

}else{

/*数据库中无$appid,$appsecret对应的记录需要再做处理,如插入到数据库

return 0;*/

}

}