[轉(zhuǎn)]Frida實(shí)踐

Frida實(shí)踐

原鏈接:https://github.com/whyTalent/whyTalent/blob/033717eaf177fb39580a475fd913f00e52d33d33/Frida%E6%A1%86%E6%9E%B6/2%20frida%E5%AE%9E%E8%B7%B5.md
Frida分客戶端環(huán)境和服務(wù)端環(huán)境

  • 客戶端:編寫Python代碼伯诬,用于連接遠(yuǎn)程設(shè)備瘟裸,提交要注入的代碼到遠(yuǎn)程只泼,接受服務(wù)端的發(fā)來的消息等;
  • 服務(wù)端:需要用Javascript代碼注入到目標(biāo)進(jìn)程涨岁,操作內(nèi)存數(shù)據(jù),給客戶端發(fā)送消息等操作

也可以把客戶端理解成控制端胁镐,服務(wù)端理解成被控端啊楚,假如要用PC來對(duì)Android設(shè)備上的某個(gè)進(jìn)程進(jìn)行操作,那么PC就是客戶端,而Android設(shè)備就是服務(wù)端拾积。

image.png

按照功能層級(jí)殉挽,可以劃分可以分為四個(gè)級(jí)別:

  1. CPU 指令集級(jí)別的 inline-hook 框架: frida-gum
  2. 使用 JavaScript 引擎對(duì) gum 進(jìn)行封裝實(shí)現(xiàn)腳本拓展的能力: gum-js
  3. 運(yùn)行時(shí)進(jìn)程注入、腳本加載拓巧、RPC 通信管理等功能: frida-core
  4. 針對(duì)特殊運(yùn)行環(huán)境的 JS 模塊及其接口斯碌,如 frida-java-bridge、frida-objc-bridge

一肛度、frida自動(dòng)化基礎(chǔ)

1傻唾、frida Javascript 引擎

1)由于 iOS 的 JIT 限制,以及嵌?式設(shè)備的內(nèi)存壓?承耿,新版將默認(rèn)腳 本引擎從 V8 遷移? Duktape

2)在 Android 等?持 v8 的平臺(tái)上仍然可以使? enable-jit 選項(xiàng)切換回 v8

3)Duktape ? v8 缺失了?常多 ECMAScript 6 特性冠骄,如箭頭表達(dá)式、 let 關(guān)鍵字

箭頭函數(shù)

  • ECMAScript 6 引?的書寫匿名函數(shù)的特性
  • 需要啟? JIT加袋,或 frida-compile 轉(zhuǎn)譯才可在 frida 中使?
  • ? function 表達(dá)式簡(jiǎn)潔凛辣。適合編寫邏輯較短的回調(diào)函數(shù)
  • 語義并?完全等價(jià)。箭頭函數(shù)中的 this 指向?作?域中的上下?职烧;? function 可以通過 Function.prototype.bind ?法指定上下?

4)frida --debug 啟?調(diào)試需使? Duktape扁誓,不兼容 v8-inspector

5)以下代碼等價(jià)

// 普通函數(shù)
Process.enumerateModulesSync().filter(function(module) { return
module.path.startsWith('/Applications') }) 

// 箭頭函數(shù)
Process.enumerateModulesSync().filter(module => module.path.startsWith('/
Applications'))

2、npm && frida-compile

**命令參數(shù) **

  • -o 輸出?件名
  • -w 監(jiān)視模式蚀之,源?件改動(dòng)后?即編譯
  • -c 開啟 uglify 腳本壓縮
  • -b 輸出字節(jié)碼
  • -h 查看完整命令??法
  • -x, —no-babelify 關(guān)閉 babel 轉(zhuǎn)譯

特點(diǎn)

1)需求

  • 默認(rèn)使?的 Duktape 不?持最新的 ECMAScript 特性
  • 單個(gè) js ?件蝗敢,難以管理?型項(xiàng)?

2)可將 TypeScript 或 ES6 轉(zhuǎn)譯成 Duktape 可?的 ES5 語法

3)?持 Browserify 的打包,?持 ES6 modules足删、source map 和 uglify 代碼壓縮寿谴。甚? 可?成 Duktape 字節(jié)

4)?持使? require 或 es6 module 引?第三? npm 包

5)frida-compile 是 npm 包,需要 node.js 運(yùn)?環(huán)境失受,與 frida-python 不沖突拭卿,可同時(shí)安 裝使?价脾。其中,在npm 內(nèi)可創(chuàng)建?錄結(jié)構(gòu)伊佃、安裝依賴骤铃,并可在 package.json 中添加構(gòu)建腳本

6)使? TypeScript 可享受到類型系統(tǒng)帶來的?型項(xiàng)?管理便利

7)Babel 添加插件?持?級(jí)的語法特性(generator / async-await)

補(bǔ)充npm package.json文件介紹

1)創(chuàng)建package.json文件:使用 npm init 即可在當(dāng)前目錄創(chuàng)建一個(gè) package.json 文件,依次確認(rèn)即可創(chuàng)建惠桃∑忠模或npm init --yes 跳過回答問題步驟,直接生成默認(rèn)值的 package.json 文件

2)基礎(chǔ)屬性:

  • name:全部小寫辜王,沒有空格劈狐,可以使用下劃線或者橫線

  • version:x.x.x 的格式

  • description:描述信息,有助于搜索

  • main: 入口文件呐馆,一般都是 index.js

  • keywords:關(guān)鍵字肥缔,有助于在人們使用 npm search 搜索時(shí)發(fā)現(xiàn)你的項(xiàng)目

  • author:作者信息

  • license:默認(rèn)是 MIT

  • bugs:當(dāng)前項(xiàng)目的一些錯(cuò)誤信息

  • 指定依賴包:指定項(xiàng)目依賴的包,通過npm install 默認(rèn)下載

    • dependencies:在生產(chǎn)環(huán)境中需要用到的依賴
    • devDependencies:在開發(fā)汹来、測(cè)試環(huán)境中用到的依賴
  • scripts:指定了運(yùn)行腳本命令的npm命令行縮寫续膳,比如:start 指定了運(yùn)行npm run start時(shí),所要執(zhí)行的命令

    "scripts": {
        "prepare": "npm run build",  // npm run prepare
        "preinstall": "echo here it comes!",  // npm run preinstall
        "postinstall": "echo there it goes!", // npm run postinstall
        "start": "node index.js",  // npm run start
        "test": "tap test/*.js"    // npm run test
    }
    

3收班、frida session實(shí)例的生命周期

image.png

4坟岔、firda 平臺(tái)實(shí)踐

4.1 frida on Android

frida-java 是 frida 內(nèi)置庫,即 Java 命名空間下的函數(shù)摔桦,可對(duì) ART 和 Dalvik 運(yùn)?時(shí)插樁(源代碼 github/frida/frida-java)社付。其次,在 frida 框架基礎(chǔ)上完全由 javascript 實(shí)現(xiàn)邻耕,frida-gum 只實(shí)現(xiàn)了通?的?進(jìn)制插樁鸥咖。總的來說兄世,frida-java通過兩步實(shí)現(xiàn)js世界到j(luò)ava世界的單向通道扛或,首先利用frida-gum提供的js接口操作native世界,然后再基于jni連通到j(luò)ava世界碘饼。


image.png

1)操作對(duì)象或字段

a. 操作對(duì)象

frida 既可以 new 對(duì)象實(shí)例熙兔,也可以搜索已有的對(duì)象

  • $new:new 運(yùn)算符,初始化新對(duì)象艾恼。注意與 $init 區(qū)分
  • $alloc:分配內(nèi)存住涉,但不初始化
  • $init:構(gòu)造器?法,?來 hook, ?不是給 js 調(diào)?
  • $dispose:析構(gòu)函數(shù)
  • $isSameObject:是否與另?個(gè) Java 對(duì)象相同
  • $className:類名
if (!Java.available) 
    throw new Error('requires Android'); 

Java.perform(function() { 
    const JavaString = Java.use('java.lang.String'); 
    var exampleString1 = JavaString.$new('Hello World, this is an example string in Java.'); 
    console.log('[+] exampleString1: ' + exampleString1); 
    console.log('[+] exampleString1.length(): ' + exampleString1.length()); 
});
b. 訪問 / 修改對(duì)象成員

instance.field.value = newValue钠绍,這種?式不區(qū)分成員可?性舆声,即使是私有成員同樣可以直接訪問,其次除 value 的 settergetter 之外,fieldTypefieldReturnType 獲取類型信息

// 字段賦值和讀取要在字段名后加.value媳握,假設(shè)有這樣的一個(gè)類
package com.luoyesiqiu.app;
public class Person{
    private String name;
    private int age;
}

// 操作Person類的name字段和age字段
var person_class = Java.use("com.luoyesiqiu.app.Person");
// 實(shí)例化Person類
var person_class_instance = person_class.$new();

// 給name字段賦值
person_class_instance.name.value = "luoyesiqiu";
// 給age字段賦值
person_class_instance.age.value = 18;

// 輸出name字段和age字段的值
console.log("name = ",person_class_instance.name.value, "," ,"age = " ,person_class_instance.age.value);

frida 對(duì)數(shù)組做了封裝碱屁,直接取下標(biāo)即可訪問

// 注意 instance 和 Class 的區(qū)別
// Java.choose 找到實(shí)例后查詢字段的類型
Java.perform(function () { 
    var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity'); 
    
    Java.choose(MainActivity.$className, { 
        onMatch: function(instance) { 
            console.log(JSON.stringify(instance.P.fieldReturnType));
        }, 
        onComplete: function() {} 
    }); 
}) 

2)修改函數(shù)實(shí)現(xiàn) Hook Java

修改一個(gè)函數(shù)的實(shí)現(xiàn)后,如果這個(gè)函數(shù)被調(diào)用蛾找,則avascript代碼里的函數(shù)實(shí)現(xiàn)也會(huì)被調(diào)用

Java 層的插樁

// 格式
Java.use().method.implementation = hookCallback

// 由于 Java ?持同名?法重載娩脾,需要? .overload 確定具體的?法
Java.use('java.lang.String').$new.overload('[B', 'java.nio.charset.Charset')

JNI 層插樁:JNI 實(shí)現(xiàn)在 so 中,且符號(hào)必然是導(dǎo)出函數(shù)打毛,照常使? Interceptor 即可

a. 函數(shù)參數(shù)類型表示

基本類型縮寫表示表:

<div align="center"><img src="imgs/基本類型縮寫.png" alt="基本類型縮寫" style="zoom:80%;" /></div>

注意:

int[]類型:重載時(shí)要寫成[I

任意類:直接寫完整類名即可柿赊,比如:java.lang.String

對(duì)象數(shù)組:用左中括號(hào)接上完整類名再接上分號(hào) [java.lang.String;

b. 帶參數(shù)構(gòu)造函數(shù)
// 修改參數(shù)為byte[]類型的構(gòu)造函數(shù)的實(shí)現(xiàn)
ClassName.$init.overload('[B').implementation=function(param){
    //do something
}

// 修改多參數(shù)的構(gòu)造函數(shù)的實(shí)現(xiàn)
ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
    //do something
}

注:ClassName是使用 Java.use 定義的類,param是可以在函數(shù)體中訪問的參數(shù)

c. 無參構(gòu)造函數(shù)
// 默認(rèn)格式
ClassName.$init.overload().implementation=function(){
    //do something
}

// 調(diào)用原構(gòu)造函數(shù)
ClassName.$init.overload().implementation=function(){
    //do something
    this.$init();
    //do something
}

:當(dāng)構(gòu)造函數(shù)(函數(shù))有多種重載形式幻枉,比如一個(gè)類中有兩個(gè)形式的func:void func()void func(int)碰声,要加上 overload來對(duì)函數(shù)進(jìn)行重載,否則可以省略overload

d. 普通函數(shù) & 無參函數(shù)
// 修改函數(shù)名為func熬甫,參數(shù)為byte[]類型的函數(shù)的實(shí)現(xiàn)
ClassName.func.overload('[B').implementation=function(param){
    //do something
    //return ...
}

// 無參數(shù)的函數(shù)
ClassName.func.overload().implementation=function(){
    //do something
}

// 帶返回值, 則hook時(shí)也應(yīng)有返回值
ClassName.func.overload().implementation=function(){
    //do something
    return this.func();
}

: 在修改函數(shù)實(shí)現(xiàn)時(shí)胰挑,如果原函數(shù)有返回值,那么我們?cè)趯?shí)現(xiàn)時(shí)也要返回合適的值

3)函數(shù)調(diào)用&實(shí)例化

// 和Java一樣椿肩,創(chuàng)建類實(shí)例就是調(diào)用構(gòu)造函數(shù)瞻颂,而在這里用$new表示一個(gè)構(gòu)造函數(shù)
var ClassName = Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();

// 實(shí)例化以后調(diào)用其他函數(shù)
var ClassName = Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();

4)常用Java hook方法

a. 獲取調(diào)用堆棧

Android 提供了?具函數(shù)可以打印 Exception 的堆棧,此?式等價(jià) 于 Log.getStackTraceString(new Exception)

Java.perform(function () { 
    const Log = Java.use('android.util.Log'); 
    const Exception = Java.use('java.lang.Exception'); 
    const MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
    
    MainActivity.onClick.implementation = function(v) { 
        this.onClick(v); 
        console.log(Log.getStackTraceString(Exception.$new())); 
    }; 
}); 
b. 枚舉所有類方法
Java.perform(function() {
    //enter class name here: example android.security.keystore.KeyGenParameterSpec$Builder
    //class inside a class is defined using CLASS_NAME$SUB_CLASS_NAME
    var class_name = "android.security.keystore.KeyGenParameterSpec$Builder";
    var methodArr = Java.use(class_name).class.getMethods();
    
    console.log("[*] Class Name: " + class_name)
    console.log("[*] Method Names:")
    for(var m in methodArr)
    {
        console.log(methodArr[m]);
    }
});

5)Hook 動(dòng)態(tài)鏈接庫(loadLibrary)

Android中我們通常使用系統(tǒng)提供的兩種API:System.loadLibrary或者System.load來加載so文件:

// 加載的是libnative-lib.so覆旱,注意的是這邊只需要傳入"native-lib"
System.loadLibrary("native-lib");

// 傳入的是so文件完整的絕對(duì)路徑
System.load("/data/data/應(yīng)用包名/lib/libnative-lib.so")

System.loadLibrary()和System.load()的區(qū)別:

1)loadLibray傳入的是編譯腳本指定生成的so文件名稱,一般不需要包含開頭的lib和結(jié)尾的.so核无,而load傳入的是so文件所在的絕對(duì)路徑

2)loadLibrary傳入的不能是路徑扣唱,查找so時(shí)會(huì)優(yōu)先從應(yīng)用本地路徑下(/data/data/${package-name}/lib/arm/)進(jìn)行查找,不存在的話才會(huì)從系統(tǒng)lib路徑下(/system/lib团南、/vendor/lib等)進(jìn)行查找噪沙;而load則沒有路徑查找的過程

3)load傳入的不能是sdcard路徑,會(huì)導(dǎo)致加載失敗吐根,一般只支持應(yīng)用本地存儲(chǔ)路徑/data/data/${package-name}/正歼,或者是系統(tǒng)lib路徑system/lib等這2類路徑

4)loadLibrary加載的都是一開始就已經(jīng)打包進(jìn)apk或系統(tǒng)的so文件了,而load可以是一開始就打包進(jìn)來的so文件拷橘,也可以是后續(xù)從網(wǎng)絡(luò)下載局义,外部導(dǎo)入的so文件

5)重復(fù)調(diào)用loadLibrar, load并不會(huì)重復(fù)加載so,會(huì)優(yōu)先從已加載的緩存中讀取冗疮,所以只會(huì)加載一次

6)加載成功后會(huì)去搜索so是否有"JNI_OnLoad"萄唇,有的話則進(jìn)行調(diào)用,所以"JNI_OnLoad"只會(huì)在加載成功后被主動(dòng)回調(diào)一次术幔,一般可以用來做一些初始化的操作另萤,比如動(dòng)態(tài)注冊(cè)jni相關(guān)方法等

底層Android 加載動(dòng)態(tài)鏈接庫Java代碼:

// System.load("/data/data/應(yīng)用包名/lib/libnative-lib.so")
public static void load(String filename) {
    Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}

// System.loadLibrary("native-lib")
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

Hook底層System.loadLibrary方式:

// 1) overload 重載
Java.perform(function() {
    const System = Java.use('java.lang.System');
    const Runtime = Java.use('java.lang.Runtime');
    const VMStack = Java.use('dalvik.system.VMStack');

    // System.loadLibrary 函數(shù)重載
    System.loadLibrary.overload('java.lang.String').implementation = function(library) {
        console.log("[*] Loading dynamic library => " + library);
        try {
            // android OAID 動(dòng)態(tài)鏈接庫加載
            // PS: frida實(shí)踐過程so庫發(fā)現(xiàn)影響APP啟動(dòng),跳過so庫加載邏輯,規(guī)避導(dǎo)致此問題,理論上不影響APP整體功能
            if(library === 'msaoaidsec') {
                return;
            }
            
            const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
            return loaded;
        } catch(ex) {
            console.log(ex);
        }
    };
});

// 2) 重寫
Java.perform(function(){
    const system = Java.use('java.lang.System');
    const Runtime = Java.use('java.lang.Runtime');
    const VMStack = Java.use('dalvik.system.VMStack');
    
    system.loadLibrary.implementation = function(library){
        console.log("[*] Loading dynamic library => " + library);
        
        // this.loadLibrary(library);
        const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
        
        // 底層注入
        var mbase = Module.getBaseAddress('libluajava.so');
        Interceptor.attach(mbase.add(0xC999), {
            onEnter:function(args){
                console.log(hexdump(Memory.readPointer(args[2]),{ length: 100, ansi: true }));
            }
        });
    }
});

loadLibrary 內(nèi)部會(huì)修改 classloader,不能直接調(diào)用 this.loadLibrary(library),故主動(dòng)調(diào)用更底層的loadLibrary0

詳情:

6)hook dlopen 和 android_dlopen_ext

function hook_dlopen(module_name) {
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

    if (android_dlopen_ext) {
        Interceptor.attach(android_dlopen_ext, {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr) {
                    this.path = (pathptr).readCString();
                    if (this.path.indexOf(module_name) >= 0) {
                        this.canhook = true;
                        console.log("android_dlopen_ext:", this.path);
                    }
                }
            },
            onLeave: function (retval) {
                if (this.canhook) {
                    console.log("[*] android_dlopen_ext can hook");
                }
            }
        });
    }

    var dlopen = Module.findExportByName(null, "dlopen");
    if (dlopen) {
        Interceptor.attach(dlopen, {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr) {
                    this.path = (pathptr).readCString();
                    if (this.path.indexOf(module_name) >= 0) {
                        this.canhook = true;
                        console.log("dlopen:", this.path);
                    }
                }
            },
            onLeave: function (retval) {
                if (this.canhook) {
                    console.log("[*] dlopen can hook");
                }
            }
        });
    }
    console.log("android_dlopen_ext:", android_dlopen_ext, "dlopen:", dlopen);
}

詳情:

7)調(diào)用APP Router實(shí)現(xiàn)schema跳轉(zhuǎn)

假設(shè)DemoAPP的Router實(shí)例調(diào)用格式定義如下:

// 包名: com.android.router
// Builder是RouteRequest類的子類
APPRouter.routeTo(RouteRequest.Builder(url).build(), Application)

frida調(diào)用方式

Java.perform(function() {
    setTimeout(function() {
        Java.choose('android.app.Application', {
            onComplete: function() {

            },
            onMatch: function(instance) {
                const schema = "demoapp://test_schema"
                console.log("[*] routing to: " + schema);
                
                // 子類
                const RouteRequestBuilder = Java.use('com.android.router.RouteRequest$Builder');
                var request = RouteRequestBuilder.$new(schema).build()
                console.log(Java.use('com.android.router').routeTo(request, instance));
            }
        })
    }, 3000);
})

8)assert斷言驗(yàn)證

配合frida動(dòng)態(tài)測(cè)試框架四敞,通過assert模塊斷言驗(yàn)證執(zhí)行結(jié)果的準(zhǔn)確性

import assert from "assert";

export function image_test() {
    // assert
    assert.strictEqual(1, 1, "[*] image_test assert pass");

    const apples = 1;
    const oranges = 1;
    assert.strictEqual(apples, oranges, `image: apples ${apples} !== oranges ${oranges}`);
    console.log("[*] image test complete")
}

export function hook_view_click() {
    const View = Java.use('android.view.View');
    View.setOnClickListener.implementation = function (v: Object) {
        assert.ok(true, "[*] hook_view_click assert pass");
        this.setOnClickListener(v);
    };
}

4.2 frida on iOS

frida-objc:對(duì)應(yīng) Java泛源,ObjC api 是 frida 的另?個(gè)“?等公?”,源代碼 github/frida/frida-objc忿危,與 JVM 類似达箍,Objective C 也提供了 runtime api,其次frida 將 Objective C 的部分 runtime api 提供到 ObjC.api 中

1)特點(diǎn)

與 Java 顯著不同癌蚁,frida-objc 將所有 class 信息 保存到 ObjC.classes 中幻梯,直接對(duì)其 for in 遍歷 key 即可

// Objective C 實(shí)現(xiàn)
[NSString stringWithString:@"Hello World”]

// 對(duì)應(yīng) frida
var NSString = ObjC.classes.NSString; 
NSString.stringWithString_("Hello World”);

new ObjC.Object 可以將指針轉(zhuǎn)換為 Objective C 對(duì)象,但如果指針不是合法的對(duì)象或合法的地址努释,將拋出異车馍遥或?qū)е挛炊x?為

2)hook objective C

firda提供了3種方式hook objective C方法:

1)ObjC.classes.Class.method 以及 ObjC.Block :都提供了?個(gè) .implementation (獲取內(nèi)存地址)的 setter 來 hook ?法實(shí)現(xiàn),實(shí)際上就是 iOS 開發(fā)者熟悉的 Method Swizzling

2)使?攔截器 Interceptor.attach(ObjC.classes.Class.method.implementation)伐蒂,看上去很相似煞躬,但實(shí)現(xiàn)原理是對(duì) selector 指向的代碼進(jìn)? inline hook

3)Proxy 也是 Objective C 當(dāng)中的?種 hook ?式,其次frida 提供了ObjC.registerClass 來創(chuàng)建 Proxy

a. ObjC.classes.Class.method

格式ObjC.classes.className["funcName"]

其中逸邦,className指具體的類名稱恩沛,funcName指類方法名稱

const { AVModel } = ObjC.classes;

// 獲取函數(shù)內(nèi)存地址
const oldImpl = AVModel["- getDataGotoType"].implementation
// 函數(shù)替換
AVModel["- getDataGotoType"].implementation = ObjC.implement(AVModel["- getDataGotoType"], (handle, selector) => {
  console.log("AVModel.getDataGotoType hooked")
  // 返回值替換
  return ObjC.classes.NSString.stringWithString_("hello, world");
});

// 初始化對(duì)象
const model = AVModel.alloc().init();
// 調(diào)用對(duì)象為 hook 函數(shù)
console.log(model.getDataGotoType());
// 解除 hook
AVModel["- getDataGotoType"].implementation = oldImpl;
// 調(diào)用原函數(shù)
console.log(model.getDataGotoType());

b. Objc.Block

格式:new Objc.Block(target[, options])

其中,target 是一個(gè) NativePointer 對(duì)象

// Defining a Block that will be passed as handler parameter to +[UIAlertAction actionWithTitle:style:handler:]
var handler = new ObjC.Block({
  retType: 'void',
  argTypes: ['object'],
  implementation: function () {
  }
});

// Import ObjC classes
var UIAlertController = ObjC.classes.UIAlertController;
var UIAlertAction = ObjC.classes.UIAlertAction;
var UIApplication = ObjC.classes.UIApplication;

// Using Grand Central Dispatch to pass messages (invoke methods) in application's main thread
ObjC.schedule(ObjC.mainQueue, function () {
  // Using integer numerals for preferredStyle which is of type enum UIAlertControllerStyle
  var alert = UIAlertController.alertControllerWithTitle_message_preferredStyle_('Frida', 'Hello from Frida', 1);
  // Again using integer numeral for style parameter that is enum
  var defaultAction = UIAlertAction.actionWithTitle_style_handler_('OK', 0, handler);
  alert.addAction_(defaultAction);
  // Instead of using `ObjC.choose()` and looking for UIViewController instances
  // on the heap, we have direct access through UIApplication:
  UIApplication.sharedApplication().keyWindow().rootViewController().presentViewController_animated_completion_(alert, true, NULL);
})
c. Interceptor 攔截器

格式:Interceptor.attach(target, callbacks[, data])

其中缕减,target是 NativePointer 指定要攔截調(diào)用的函數(shù)的地址雷客,如果從Frida API獲取地址(例如Module.getExportByName() ),F(xiàn)rida將處理詳細(xì)信息

攔截C函數(shù)

fopen 函數(shù):其功能是使用給定的模式 mode 打開 filename 所指向的文件桥狡,如果文件打開成功搅裙,會(huì)返回一個(gè)指針,相當(dāng)于句柄裹芝。如果文件打開失敗則返回 0

原型如下:

FILE *fopen(const char *filename, const char *mode)
// a. 底層系統(tǒng)函數(shù)
Interceptor.attach(Module.findExportByName(null, "fopen"), {
    // onEnter 是進(jìn)入 fopen 函數(shù)時(shí)要執(zhí)行的代碼
    onEnter: function(args) {
        if (args[0].isNull()) return;
        var path = args[0].readUtf8String();
        console.log("fopen " + path);
 
    },
    // onLeave 是離開 fopen 函數(shù)時(shí)要執(zhí)行的代碼
    onLeave: function(retval) {
        console.log("\t[-] Type of return value: " + typeof retval);
        console.log("\t[-] Original Return Value: " + retval);
        retval.replace(0);  //將返回值替換成0
        console.log("\t[-] New Return Value: " + retval);
    },
})

// b. 自定義函數(shù)
// 自定義一個(gè) getStr 函數(shù)部逮,返回的參數(shù)是一個(gè)字符串指針,在 onLeave 函數(shù)中新建一個(gè)變量 string嫂易,分配內(nèi)存并填充字符串 4567789, 然后將返回值替換變量 string
Interceptor.attach(Module.findExportByName(null, "getStr"), {
    onEnter: function(args) {
        console.log("getStr");
    },
    onLeave: function(retval) {
        console.log("\t[-] Type of return value: " + typeof retval);
        console.log("\t[-] Original Return Value: " + retval.readUtf8String());
 
        var string = Memory.allocUtf8String("456789");  //分配內(nèi)存
        retval.replace(string); //替換
 
        console.log("\t[-] New Return Value: " + retval.readUtf8String());
    },
})
攔截 Objective-C 方法

frida 不僅可以攔截 C 函數(shù)兄朋,還可以攔截 Objective-C 方法,比如編寫腳本對(duì) +[NSURL URLWithString:] 進(jìn)行攔截怜械,代碼如下颅和,其中 onEnter 調(diào)用 ObjC.classes.NSString.stringWithString_ 給 NSString 傳遞新的值,這樣相當(dāng)于替換原本的 URL缕允。

var className = "NSURL";
var funcName = "+ URLWithString:";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');

Interceptor.attach(hook.implementation, {
    onLeave: function(retval) {
        console.log("[*] Class Name: " + className);
        console.log("[*] Method Name: " + funcName);
        console.log("\t[-] Type of return value: " + typeof retval);
        console.log("\t[-] Original Return Value: " + retval);
    },
 
    onEnter: function(args){
        var className = ObjC.Object(args[0]);
        var methodName = args[1];
        var urlString = ObjC.Object(args[2]);
 
        console.log("className: " + className.toString());
        console.log("methodName: " + methodName.readUtf8String());
        console.log("urlString: " + urlString.toString());
        console.log("-----------------------------------------");
 
        urlString = ObjC.classes.NSString.stringWithString_("http://www.baidu.com")
        console.log("newUrlString: " + urlString.toString());
        console.log("-----------------------------------------");
    }
});

3)示例

a. iOS / macOS 定位偽造

基礎(chǔ):

  • iOS 和 macOS 定位使?統(tǒng)? API:CLLocationManager

  • 需指定?個(gè) delegate 實(shí)現(xiàn)如下回調(diào)?法獲取相應(yīng)事件:

    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:
    (CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation;
    
  • 如下?法開始定位

    - (void)requestLocation;
    - (void)startUpdatingLocation
    

流程:

  • 先處理 requestLocation 等?法拿到 delegate 的指針
  • 在 delegate 上查找對(duì)應(yīng)回調(diào)?法是否存在融虽,逐個(gè) hook
  • CLLocation 的經(jīng)緯度是只讀屬性,需要?jiǎng)?chuàng)建新的副本灼芭。為了對(duì)抗時(shí)間戳等特征檢測(cè)有额,最好把正確的 CLLocation 除經(jīng)緯度之外所有的屬性復(fù)制上去
const { BWAJSEventAuthorizationHandler, CLLocation } = ObjC.classes;

var hook_cllocation = ObjC.classes.CLLocation["- coordinate"]
Interceptor.attach(hook_cllocation.implementation, {
  onLeave: function(return_value) {
    var spoofed_return_value = (new ObjC.Object(return_value)).initWithLatitude_longitude_(20.5937, 78.9629)
    return_value.replace(spoofed_return_value)
  }
});

var jsbHook = BWAJSEventAuthorizationHandler["- handleWithEvent:params:callback:"]
Interceptor.attach(jsbHook.implementation, {
  onEnter: function() {
    console.log("BWAJSEventAuthorizationHandler hooked")
  }
});
b. 調(diào)用openURL
// Get a reference to the openURL selector
var openURL = ObjC.classes.UIApplication["- openURL:"];

// Intercept the method
Interceptor.attach(openURL.implementation, {
  onEnter: function(args) {
    // As this is an ObjectiveC method, the arguments are as follows:
    // 0. 'self'
    // 1. The selector (openURL:)
    // 2. The first argument to the openURL selector
    var myNSURL = new ObjC.Object(args[2]);
    // Convert it to a JS string
    var myJSURL = myNSURL.absoluteString().toString();
    // Log it
    console.log("Launching URL: " + myJSURL);
  }
});
c. 攔截網(wǎng)絡(luò)請(qǐng)求
//判斷Object-C類方法是否已經(jīng)加載進(jìn)來
if(ObjC.available){
    console.log('\n[*] Starting Hooking');
    
    var _className = "AFHTTPSessionManager"; //類名
    var _methodName = "- POST:parameters:progress:success:failure:"; //方法名
    
    // 通過ObjC.classes返回當(dāng)前注冊(cè)類的映射表找到想要hook的類名、方法名
    var hooking = ObjC.classes[_className][_methodName];
    console.log('className is: ' + _className + ' and methodName is: ' + _methodName);
    
    const pendingBlocks = new Set()
    Interceptor.attach(hooking.implementation,{ 
        onEnter: function(args) {
            // args[0]:self, args[1]:The selector
            // args[2]: 請(qǐng)求url args[3] 請(qǐng)求參數(shù)
            var param = new ObjC.Object(args[2]);            
            var param2 = new ObjC.Object(args[3]);
            
            const block = new ObjC.Block(args[5]);
            pendingBlocks.add(block); // Keep it alive
            
            const appCallback = block.implementation;
            block.implementation = (success1, success2) => {
              console.log('網(wǎng)絡(luò)請(qǐng)求成功回調(diào)success1'+success1+'success2'+success2);
              const result = appCallback(success1, success2);
              pendingBlocks.delete(block);
              return result;
            };
        },
        onLeave:function(returnValue){
            //如下代碼則是在函數(shù)調(diào)用之后 打印函數(shù)的返回值及函數(shù)返回值類型
            console.log('Return value of: ');
            console.log(' ' + this._className + ' --> ' + this._methodName);
            var typeValue = Object.prototype.toString.call(returnValue);
            console.log("\t[-] Type of return value: " + typeValue);
            console.log("\t[-] Return Value: " + returnValue);
        }
    });
}
d. 攔截類所有方法

想對(duì)某個(gè)類的所有方法進(jìn)行批量攔截,可以使用ApiResolver接口巍佑,它可以根據(jù)正則表達(dá)式獲取符合條件的所有方法

var resolver = new ApiResolver('objc')

resolver.enumerateMatches('*[T1TranslateButton *]', {
    onMatch: function (match) {
        console.log(match['name'] + ":" + match['address'])
    },
    onComplete: function () {}
})
e. 替換原有方法hook

Interceptor.attach() 可以在攔截目標(biāo)后茴迁,可以打印參數(shù),修改返回值萤衰,但無法阻止原方法的執(zhí)行

var didTap = ObjC.classes.T1TranslateButton['- _didTap:forEvent:']
 
var didTapOldImp = didTap.implementation
 
// 覆蓋實(shí)現(xiàn)
didTap.implementation = ObjC.implement(setTitle, function(handle, selector, arg1, arg2) {
 
  var self = ObjC.Object(handle)
  console.log("self -- ", self) 
 
  // 調(diào)用舊實(shí)現(xiàn)
  // didTapOldImp(handle, selector, arg1, arg2)
})

需要注意的是堕义,像_didTap:forEvent:這里需要傳遞兩個(gè)參數(shù),則ObjC.implement的回調(diào)中也需要寫明兩個(gè)參數(shù)(arg1脆栋、arg2)倦卖,即需要多少參數(shù)就寫多少,沒有則不用寫

二椿争、frida python實(shí)踐

1怕膛、get_device_manager

獲取設(shè)備管理器

manager = frida.get_device_manager()

2、enumerate_devices

選擇設(shè)備連接方式

# a. 通過 device manager 管理器獲取設(shè)備對(duì)象
devices = manager.enumerate_devices()
> Device(id="local", name="Local System", type='local')
> Device(id="socket", name="Local Socket", type='remote')
> Device(id="b868ca03", name="M2011J18C", type='usb')
device = manager.get_device(devices[2].id, timeout)  # 指定設(shè)備id連接

# b. 指定設(shè)備連接方式
device = frida.get_local_device()      # Local System
device = frida.get_usb_device(timeout) # usb
device = frida.get_remote_device()     # 遠(yuǎn)程, Local Socket

3秦踪、enumerate_applications 和enumerate_processes

獲取device上的所有App和進(jìn)程

# 等價(jià)于 frida-ps -aU
#  PID  Name    Identifier
#  5  ------  ---------------
#  21550  Gadget  re.frida.Gadget

pid = None
for a in device.enumerate_applications():
    if a.identifier == 're.frida.Gadget':
        pid = a.pid
        break

## 補(bǔ)充:獲取設(shè)備上正則運(yùn)行的進(jìn)程信息
# 等價(jià)于 frida-ps -a
#  PID  Name
#  -----  ------
#  32429  Gadget

all_processes = device.enumerate_processes()
    for per_process in all_processes:
        print(per_process)

4褐捻、attach

功能:附加進(jìn)程或APP,生成session實(shí)例

1)啟動(dòng)新的實(shí)例:device.spawn(‘path or bundle id’)

  • 可指定啟動(dòng)參數(shù)
  • ?持在進(jìn)程初始化之前執(zhí)??些操作
  • iOS 上如果已經(jīng) App 運(yùn)?(包括后臺(tái)休眠)會(huì)導(dǎo)致失敗

2)附加到現(xiàn)有進(jìn)程:device.attach(pid)

  • 可能會(huì)錯(cuò)過 hook 時(shí)機(jī)
  • spawn 在移動(dòng)設(shè)備容易出現(xiàn)不穩(wěn)定現(xiàn)象椅邓,可使? attach 模式
# 等價(jià)于 frida -U -f xx.xx.xx --no-pause
# 啟動(dòng)應(yīng)用進(jìn)入交互模式, 應(yīng)用于 App 未打開的情景

# a. pid 方式, app package (identifier)柠逞,適用于iOS
pid = device.spawn([self.package])
device.resume(pid)
time.sleep(2)  # Without it Java.perform silently fails
session = device.attach(pid)

# b.app name,適用于Android
subprocess.call("adb shell pm clear app.identifier", shell=True)         # 清理APP應(yīng)用數(shù)據(jù)
subprocess.call("adb shell monkey -p app.identifier -v 1", shell=True)  # 通過monkey指令指定包名喚醒APP
time.sleep(5)
session = device.attach('app name')
# session = device.attach('app identifier')

5景馁、create_script

注入JS腳本

"""
def create_script(
        self, source: str, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None
    ) -> Script
"""

# a. js字符串
jsScript = """
    console.log('this is inject javascript code')
"""
script = session.create_script(jsScript)

# b. js文件
with open("hook.js",mode='r',encoding='UTF-8') as f:
    Log.info('Inject script name: ' + full_js_file_name)
    script = session.create_script(f.read())
// 程序入口: hook.js

Java.perform(function() 
{
    // 獲取類
    var clazz = Java.use("com.unity3d.player.UnityPlayerActivity");
    // 獲取類中所有函數(shù)
    var methods = clazz.class.getDeclaredMethods();

    console.log("have method count:"+methods.length);
    var i=0
    if(methods.length > 0){
        //遍歷函數(shù)名
        methods.forEach(function(method){
            i = i+1
            console.log(i+":"+method);
        });
    }

});

6板壮、load

打印日志&執(zhí)行注入

# 打印js注入信息
def on_message(message, data):
    if message['type'] == 'send':
        Log.send(message['payload'])
    elif message['type'] == 'error':
        Log.error(message['description'])
    else:
        Log.error(message)

# 設(shè)備事件處理
script.on("message", on_message)  # listen
# script.off("message", on_message)  # remove listen

# 執(zhí)行
script.load()

# prevent the python script from terminating
log.info('Waiting for JavaScript...')
sys.stdin.read()

7、listen: on / off

# 設(shè)備事件處理
device_manager = frida.get_device_manager() 
device_manager.on('changed', on_changed) # listen
device_manager.off('changed', on_changed) # remove listener

# 監(jiān)聽設(shè)備插拔
device_manager.on('add', on_changed) 
device_manager.on('changed', on_changed) 
device_manager.on('remove', on_removed)

8合住、Demo示例

8.1 初始化設(shè)備

import frida

# 初始化設(shè)備連接
def init_device():
    Log.info('Current frida version: ' + str(frida.__version__))
    
    # 獲取設(shè)備管理器
    manager = frida.get_device_manager()
    Log.print('Select a frida device:')

    # 默認(rèn)設(shè)備連接方式
    devices = manager.enumerate_devices()
    for i, ldevice in enumerate(devices, 1):
        Log.print(str(i) + ' => ' + str(ldevice))

    # 選擇設(shè)備連接方式
    select = int(input())
    if select > len(devices):
        Log.error('Out of range.')
        sys.exit(1)

    device_id = devices[select - 1].id
    
    # 鏈接設(shè)備: 獲取指定 UID 設(shè)備
    device = manager.get_device(device_id, 1)
    Log.info('Connect to device \'' + device.name + '\' successfully.')

    return device


if __name__ == '__main__':
    try:
        device = init_device()

        # 遍歷需要hook的APP&進(jìn)程列表
        for per_hook_process in processes_to_hook:
            # 鏈接設(shè)備
            session = attach_android(per_hook_process['name'], per_hook_process['identifier'])

            # js 腳本注入
            for js_module in js_modules:
                process_name_var = 'var __process_name = "' + per_hook_process['identifier'] + '";'
                module_name_var = 'var __module_name = "' + js_module['name'] + '";'
                full_js_file_name = 'example/hook_' + js_module['type'] + '_' + js_module['name'] + '.js'

                with open(full_js_file_name) as f:
                    Log.info('Inject script name: ' + full_js_file_name)
                    script = session.create_script(process_name_var + module_name_var + f.read())

                script.on('message', on_message)
                Log.info('Load script name: ' + full_js_file_name)
                script.load()

        Log.info('Waiting for JavaScript...')
        print('----------------------------------------')
        sys.stdin.read()

    except Exception as e:
        Log.error(repr(e))

8.2 設(shè)備連接 Android & iOS

1)Android

def attach_android(app_name: str, app_identifier: str):
    """Android設(shè)備連接方式
    """
    try:
        # 清理APP應(yīng)用數(shù)據(jù)
        Log.info('Launching process \'' + app_name + '\'')
        subprocess.call("adb shell pm clear " + app_identifier, shell=True)

        # 通過monkey指令指定包名喚醒APP
        subprocess.call("adb shell monkey -v 1 -p " + app_identifier, shell=True)
    except frida.ExecutableNotFoundError as e2:
        Log.error('Unable to find execuable \'' + app_name + '\'.')

    Log.info('Attaching process \'' + app_name + '\'')
    time.sleep(5)

    return device.attach(app_name)

2)iOS

def attach_ios(app_name: str, app_identifier: str):
    """iOS設(shè)備連接方式
    """
    try:
        device.get_process(app_name)
    except frida.ProcessNotFoundError as e:
        Log.warn('Unable to find process \'' + app_name + '\', try to spawn...')

        # Must use identifier to spawn
        try:
            pid = device.spawn(app_identifier)
            device.resume(pid)
            time.sleep(5)
        except frida.ExecutableNotFoundError as e2:
            Log.error('Unable to find execuable \'' + app_name + '\'.')

    Log.info('Attaching: ' + app_name)

    return device.attach(app_name)

8.3 js腳本注入

js_modules = [
    {'type': 'android', 'name': 'env'},
]

def inject_js(session: frida.core.Session, modules: list, app_identifier: str):
    """注入JS腳本
    """
    # js 腳本注入
    for js_module in modules:
        process_name_var = 'var __process_name = "' + app_identifier + '";'
        module_name_var = 'var __module_name = "' + js_module['name'] + '";'
        full_js_file_name = 'example/hook_' + js_module['type'] + '_' + js_module['name'] + '.js'
        
        # 加載js腳本
        with open(full_js_file_name) as f:
            Log.info('Inject script name: ' + full_js_file_name)
            script = session.create_script(process_name_var + module_name_var + f.read())
        
        # 打印日志
        script.on('message', on_message)
        
        # 執(zhí)行注入
        Log.info('Load script name: ' + full_js_file_name)
        script.load()

8.4 執(zhí)行

if __name__ == '__main__':
    try:
        device = init_device()

        # 遍歷需要hook的APP&進(jìn)程列表
        for per_hook_process in processes_to_hook:
            # 鏈接設(shè)備
            session = attach_android(per_hook_process['name'], per_hook_process['identifier'])

            # js 腳本注入
            inject_js(session, js_modules)

        Log.info('Waiting for JavaScript...')
        print('----------------------------------------')
        sys.stdin.read()

    except Exception as e:
        Log.error(repr(e))

三绰精、frida-compile 實(shí)踐

1、環(huán)境配置

# 安裝 frida
pip install frida
pip install frida-tools

# 安裝 node
brew install node

# 環(huán)境配置完畢后聊疲,在工程目錄安裝項(xiàng)目依賴(package.json文件), 使用教程: https://www.runoob.com/nodejs/nodejs-npm.html
npm install
// package.json

"scripts": {
    "prepare": "npm run build",
    "build": "frida-compile agent/android.ts -o _android.js -c && frida-compile agent/ios.ts -o _ios.js -c",
    "watch": "frida-compile agent/android.ts -o _android.js -w && frida-compile agent/ios.ts -o _ios.js -w",
    "test_android": "python runner.py android",
    "test_ios": "python runner.py ios"
}
// runner.py

import sys
import subprocess
import time

if __name__ == '__main__':
    platform   = sys.argv[1] if len(sys.argv) >= 2 else ""
    extraParam = " ".join(sys.argv[2:]) if len(sys.argv) >= 4 else ""

    if platform == 'android':
        bundle = sys.argv[2] if len(sys.argv) == 3 else "com.app.application"
        
        // 殺死應(yīng)用 & 啟動(dòng)APP
        subprocess.call(f"adb shell am force-stop {bundle} && adb shell am start -n {bundle}/.MainActivityV2", shell=True)
        time.sleep(5)
        // 注入&執(zhí)行js腳本
        subprocess.call(f"frida -U -l _android.js -F {extraParam}", shell=True)
    elif platform == 'ios':
        bundle = sys.argv[2] if len(sys.argv) == 3 else "com.app.application"
        
        // 拉起應(yīng)用(iOS 手動(dòng)點(diǎn)擊啟動(dòng)無法 attach 進(jìn)程)
        subprocess.call(f"frida -U -f {bundle} {extraParam} &", shell=True)
        time.sleep(5)
        // 注入&執(zhí)行js腳本(注意attach的進(jìn)程是Gadget茬底,而不是對(duì)應(yīng)的bundleid)
        subprocess.call(f"frida -U -l _ios.js -n Gadget {extraParam}", shell=True)
    else:
        print("[*] Invalid platform " + platform)

2沪悲、編譯執(zhí)行測(cè)試代碼

1)安裝包含 Frida SDK 測(cè)試包获洲,通過 USB 線將手機(jī)與電腦連接

2)編譯 JavaScript 用例代碼,編譯生成 _android.js殿如、_ios.js 兩個(gè)文件贡珊,文件內(nèi)包含 import 測(cè)試用例

npm run build

3)執(zhí)行測(cè)試

# 安靜模式執(zhí)行測(cè)試腳本,30秒后自動(dòng)退出
npm run test_android -- com.app.application -q -t 30
npm run test_ios -- com.app.application -q -t 30

命令支持的參數(shù)如下:

--runtime {qjs,v8} :執(zhí)行 JS 腳本的引擎
--pause:創(chuàng)建進(jìn)程成功后涉馁,暫停應(yīng)用主線程(main thread)
-q :安靜模式(沒有 prompt)執(zhí)行完腳本后立即退出
-t TIMEOUT:在安靜模式下门岔,等待 N 秒后退出進(jìn)程

4)增量編譯:開啟一個(gè)終端,輸入 watch 命令監(jiān)聽 _android.js烤送、_ios.js 是否有變化寒随,有變化則會(huì)重新加載 js 文件

npm run watch

:增量編譯后,偶現(xiàn) Frida Session 關(guān)閉問題,遇到關(guān)閉后可以重新執(zhí)行測(cè)試命令

附錄

  1. FRIDA 實(shí)用手冊(cè)
  2. hacktricks: frida-tutorial-2
  3. Python frida.get_device() Examples
  4. Frida從入門到放棄
  5. 全平臺(tái)逆向工程資料
  6. (黑科技)Frida的用法--Hook Java代碼篇
  7. glider菜鳥: frida源碼閱讀之frida-java
  8. Frida開發(fā)環(huán)境搭建記錄
  9. [原創(chuàng)]FRIDA 使用經(jīng)驗(yàn)交流分享 frida + typescript
  10. JavaScript: assert 模塊
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末妻往,一起剝皮案震驚了整個(gè)濱河市互艾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讯泣,老刑警劉巖纫普,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異好渠,居然都是意外死亡昨稼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門拳锚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來假栓,“玉大人,你說我怎么就攤上這事晌畅〉福” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵抗楔,是天一觀的道長(zhǎng)棋凳。 經(jīng)常有香客問我,道長(zhǎng)连躏,這世上最難降的妖魔是什么剩岳? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮入热,結(jié)果婚禮上拍棕,老公的妹妹穿的比我還像新娘。我一直安慰自己勺良,他們只是感情好绰播,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尚困,像睡著了一般蠢箩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上事甜,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天谬泌,我揣著相機(jī)與錄音,去河邊找鬼逻谦。 笑死掌实,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邦马。 我是一名探鬼主播贱鼻,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宴卖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了邻悬?” 一聲冷哼從身側(cè)響起嘱腥,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拘悦,沒想到半個(gè)月后齿兔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡础米,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年分苇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屁桑。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡医寿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蘑斧,到底是詐尸還是另有隱情靖秩,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布竖瘾,位于F島的核電站沟突,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捕传。R本人自食惡果不足惜惠拭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庸论。 院中可真熱鬧职辅,春花似錦、人聲如沸聂示。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鱼喉。三九已至秀鞭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒲凶,已是汗流浹背气筋。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工拆内, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旋圆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓麸恍,卻偏偏與公主長(zhǎng)得像灵巧,于是被迫代替她去往敵國和親搀矫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容