iOS 逆向指南:動態分析

當靜態分析無法獲取足夠的信息時,就需要進行動態分析,在 app 運行時,追蹤方法調用、查看內存信息。最后找到想要分析的關鍵函數。

這篇文章包括:

  • 環境搭建
  • 反調試
  • 動態調試的思路
  • lldb 調試命令與腳本
  • cycript 配置與使用
  • frida 配置與使用
  • IDA 動態調試

環境搭建

安裝 openSSH

參照靜態分析中的安裝 openSSH小結。

用 USB 進行 SSH 連接

openSSH 默認是用 wifi 連接到 iOS 設備的,但是這樣速度慢,不穩定。因此可以安裝usbmuxd,用 USB 連接:

brew install usbmuxd

安裝后就可以用iproxy工具,將設備上的端口號映射到電腦上的某一個端口:

iproxy 2222 22

用 USB 連接設備到 mac 上,之前 openSSH 連接 iOS 的命令是ssh root@10.5.53.182,現在改成ssh root@localhost -p 2222

修改 debugserver

使用 lldb 調試需要準備 debugserver。使用 OSX 中的 lldb 遠程連接 iOS 上的 debugserver,由 debugserver 作為 lldb 和 iOS 的中轉,執行命令和返回結果。在默認情況下,iOS 上并沒有安裝 debugserver,只有在設備連接過一次 Xcode,安裝了開發者插件后,debugserver 才會被 Xcode 安裝到iOS的/Developer/usr/bin/目錄下。

在 iOS 11 越獄之前,需要對 debugserver 進行重簽名,在 iOS 11 上可以直接使用/Developer/usr/bin/debugserver,或者直接用 Xcode 對 iOS 上的 app 進行調試。iOS 11 之前用 Xcode 調試需要對 app 進行重簽名,而 iOS 11 之后不需要重簽名 app 也能調試了。

iOS 11 之前重簽名 debugserver 步驟:

1.拷貝 debugserver 到本地計算機中:scp root@iOSDeviceIP:/Developer/usr/bin/debugserver ~/debugserver

2.然后用 ldid 添加權限。由于 ldid 不支持 fat 二進制文件,所以要給 debugserver 瘦身,通過 lipo 指定要支持的指令類型,例如:lipo -thin arm64 ~/debugserver -output ~/debugserver

3.給 debugserver 添加 task_for_pid 權限,保存以下內容為 ent.xml 文件:

<!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>

然后執行以下命令添加權限:ldid -Sent.xml debugserver

4.給 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>run-unsigned-code</key>
     <true/>
     <key>get-task-allow</key>
     <true/>
     <key>task_for_pid-allow</key>
     <true/>
 </dict> 
 </plist>

然后運行以下命令給的 debugserver 簽名:codesign -s - --entitlements entitlements.plist -f debugserver

5.重新拷貝 debugserver 回手機中:scp ~/debugserver root@iOSDeviceIP:/usr/bin/debugserver

6.第一次使用 debugserver 時需要為其添加可執行權限:chmod +x /usr/bin/debugserver

連接到指定進程進行調試

準備好 debugserver 后,就可以調試任意第三方 app 了。

  1. SSH 到 iOS,使用 debugserver 來 attach 一個進程,要查看當前正在運行的進程,使用ps -e命令。比如我們要 attach 的進程號為 693,我們可以輸入如下命令:debugserver *:1234 -a 693

  2. iOS 11 上debugserver *:1234中的*:1234要替換成localhost:1234。如果用的是 Electra 越獄,命令變成/Developer/usr/bin/debugserver localhost:1234 -a 693,如果用的是unc0ver越獄,則是debugserver localhost:1234 -a 693。同理,下文中的對應命令也要相應的替換

  3. 如果要用 debugserver 啟動 app,而不是附加到已經啟動的 app,則使用debugserver *:1234 <app二進制文件路徑>,例如debugserver *:1234 /var/containers/Bundle/Application/107F3307-2900-4720-B9BA-0C7792D89DF2/APP_TO_DEBUG.app/APP_TO_DEBUG

  4. Mac 端打開終端,輸入 lldb,回車,進入 lldb 界面,使用process connect命令連接客戶端。
    用 WiFi 連接到 iOS 設備時:process connect connect://iOSDeviceIP:1234

如果要用 usbmux 連接,則先使用iproxy 1234 1234進行一次端口轉發,再使用process connect connect://localhost:1234,即可用 USB 連接到 iOS 設備。

回車后需要等待幾分鐘,時間有點久。

連接成功后,即可用 lldb 命令進行調試。

反調試

有些 app 使用了反調試功能,禁止了動態調試。

系統提供了禁止調試依附的接口,可以通過ptrace syscall svc指令調用,禁止調試。也可以通過sysctl檢查 ptrace isatty 或者 ioctl 檢查終端 task_get_exception_ports獲取異常端口等方式檢查是否正在被調試,之后再讓 app 崩潰。

可以參考關于反調試&反反調試那些事反調試與繞過的奇淫技巧

如果你發現 app 一調試就閃退,多半就是有反調試機制。

為驗證是否調用了 ptrace 可以用 debugserver -x backboard *:1234 binaryPath 啟動 app,然后下符號斷點 b ptracec 之后看 ptrace 第一行代碼的位置,然后 p $lr 找到函數返回地址,再根據 image list -o -f 的ASLR偏移,計算出原始地址。最后在 IDA 中找到調用ptrace的代碼,分析如何調用的ptrace。其他的反調試類似,參考上面的文章。

常用的動態調試方法

斷點

使用lldb的br s -a [地址]命令,在指定地址處下斷點。但是動態調試時,無法準確地找到需要斷點的地址。可以先靜態分析 app 的二進制文件,找到需要研究的方法,再在方法處下斷點。

根據二進制文件中方法的地址,找到需要斷點的地址

app 加載到內存里時,有一個偏移:

運行時的地址 = 二進制文件中的相對地址 + 偏移量

使用image list列出所有加載的模塊,查看偏移量,找到第一行:

(lldb) image list [0] 7A6179DA-8D91-315A-8BD2-546A54648D37 0x00000001000bc000 /Applications/APP_TO_DEBUG.app/APP_TO_DEBUG

其中的0x00000001000bc000就是加載的基址,偏移量就是0x1000bc000

例如,需要分析-[CLoginController keyboardWillShow]方法,方法在二進制文件中的地址為0x0000000100723bcc

hopper6.png

而這個二進制文件的基址為0x100000000

hopper7.png

所以此函數在文件中的偏移量是0x0000000100723bcc - 0x100000000 = 0x723bcc。因此當前內存中的運行時地址是0x1000bc000 + 0x723bcc = 0x1007dfbcc

反匯編指定地址

找到地址后,可以使用di --start-address <address> -count 10命令來反匯編找到的地址,如果反匯編結果和靜態分析中的匯編代碼一致,說明找到的是正確的:

(lldb) di --start-address 0x1007dfbcc -c 10
DuoYiIMOrig`-[CLoginController keyboardWillShow:]:
0x1007dfbcc <+0>:  stp    d11, d10, [sp, #-0x80]!
0x1007dfbd0 <+4>:  stp    d9, d8, [sp, #0x10]
0x1007dfbd4 <+8>:  stp    x28, x27, [sp, #0x20]
0x1007dfbd8 <+12>: stp    x26, x25, [sp, #0x30]
0x1007dfbdc <+16>: stp    x24, x23, [sp, #0x40]
0x1007dfbe0 <+20>: stp    x22, x21, [sp, #0x50]
0x1007dfbe4 <+24>: stp    x20, x19, [sp, #0x60]
0x1007dfbe8 <+28>: stp    x29, x30, [sp, #0x70]
0x1007dfbec <+32>: add    x29, sp, #0x70            ; =0x70 
0x1007dfbf0 <+36>: sub    sp, sp, #0x40             ; =0x40

和上面 hooper 中的匯編代碼比較,可以看到是一致的。

在32位設備上,可能會出現反匯編出來的是 arm 指令集,出現很多unknown opcode的指令,和 hopper 中顯示的不一致。可以加上-A thumbv7顯示 thumb 指令集的反匯編結果:di --start-address 0x1007dfbcc -c 10 -A thumbv7

再用br set -a 0x1007dfbcc打斷點:

(lldb) br set -a 0x1007dfbcc
Breakpoint 1: where = DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550, address = 0x00000001007dfbcc

查看寄存器的值

當觸發了斷點后,可以用register read查看當前寄存器的值:

Process 1252 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:](self=0x0000000123e492f0, _cmd="keyboardWillShow:", aNotification=@"UIKeyboardWillShowNotification") at CLoginController.m:550 [opt]
    
(lldb) register read 
General Purpose Registers:
        x0 = 0x0000000123e492f0
        x1 = 0x0000000191ad138c
        x2 = 0x0000000125a81580
        x3 = 0x00000001a2d26a50  CoreFoundation`__block_literal_global
        x4 = 0x0000000000000002
        x5 = 0x0000000000000001
        x6 = 0x0000000000000000
        x7 = 0x0000000000000000
        x8 = 0x0000000125a81580
        x9 = 0x0000000191ad138c
       x10 = 0x000000012407f400
       x11 = 0x0000008a000000ff
       x12 = 0x000000012407fcc0
       x13 = 0x000005a1011f1c65
       x14 = 0x000000000022a802
       x15 = 0x000000000000358f
       x16 = 0x00000001011f1c60  (void *)0x000001a1011f1c89
       x17 = 0x00000001007dfbcc  DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550
       x18 = 0x0000000000000000
       x19 = 0x0000000125a81580
       x20 = 0x0000000123da3cb0
       x21 = 0x0000000000000000
       x22 = 0x0000000000000000
       x23 = 0x00000001a8cae000  CoreFoundation`_CFXNotificationPost.samples + 352
       x24 = 0x00000001a8cae000  CoreFoundation`_CFXNotificationPost.samples + 352
       x25 = 0x0000000000000000
       x26 = 0x000000010b651440
       x27 = 0x00000001a8ca9ef8  __kCFNull
       x28 = 0x0000000000000001
        fp = 0x000000016fd3ffe0
        lr = 0x0000000183be2b10  CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
        sp = 0x000000016fd3ffe0
        pc = 0x00000001007dfbcc  DuoYiIMOrig`-[CLoginController keyboardWillShow:] at CLoginController.m:550
      cpsr = 0x60000000

(lldb) po 0x0000000123e492f0
<CLoginController: 0x123e492f0>

如果Mac上安裝了chisel,還可以用pinternals遍歷出對象的實例變量。或者調用私有方法_ivarDescription打印實例變量:po [0x0000000123e492f0 _ivarDescription]

查看調用堆棧

thread backtrace查看調用堆棧,縮寫為btthread backtrace -e true可以顯示線程嵌套的堆棧。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001007dfbcc DuoYiIMOrig`-[CLoginController keyboardWillShow:](self=0x0000000123e492f0, _cmd="keyboardWillShow:", aNotification=@"UIKeyboardWillShowNotification") at CLoginController.m:550 [opt]
    frame #1: 0x0000000183be2214 CoreFoundation`_CFXRegistrationPost + 400
    frame #2: 0x0000000183be1f90 CoreFoundation`___CFXNotificationPost_block_invoke + 60
    frame #3: 0x0000000183c51b8c CoreFoundation`-[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1504
    frame #4: 0x0000000183b23e64 CoreFoundation`_CFXNotificationPost + 376
    frame #5: 0x0000000184658e0c Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 68
    frame #6: 0x000000018a4cbb40 UIKit`-[UIInputWindowController postStartNotifications:withInfo:] + 400
    frame #7: 0x000000018a4cdcf0 UIKit`__77-[UIInputWindowController moveFromPlacement:toPlacement:starting:completion:]_block_invoke.907 + 388
    frame #8: 0x0000000189b2d0f0 UIKit`+[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 636
    frame #9: 0x0000000189bfe52c UIKit`+[UIView(UIViewAnimationWithBlocks) _animateWithDuration:delay:options:animations:start:completion:] + 128
    frame #10: 0x000000018a4cd76c UIKit`-[UIInputWindowController moveFromPlacement:toPlacement:starting:completion:] + 1368
    frame #11: 0x000000018a4d4268 UIKit`-[UIInputWindowController setInputViewSet:] + 1444
    frame #12: 0x000000018a4cce38 UIKit`-[UIInputWindowController performOperations:withAnimationStyle:] + 56
    frame #13: 0x0000000189bbe278 UIKit`-[UIPeripheralHost(UIKitInternal) setInputViews:animationStyle:] + 1276
    frame #14: 0x0000000189b1da78 UIKit`-[UIResponder(UIResponderInputViewAdditions) reloadInputViews] + 80
    frame #15: 0x0000000189b7bb4c UIKit`-[UIResponder becomeFirstResponder] + 600
    frame #16: 0x0000000189b7bebc UIKit`-[UIView(Hierarchy) becomeFirstResponder] + 148
    frame #17: 0x0000000189bfe0b4 UIKit`-[UITextField becomeFirstResponder] + 60
    frame #18: 0x0000000189ca5128 UIKit`-[UITextInteractionAssistant(UITextInteractionAssistant_Internal) setFirstResponderIfNecessary] + 192
    frame #19: 0x0000000189ca4630 UIKit`-[UITextInteractionAssistant(UITextInteractionAssistant_Internal) oneFingerTap:] + 3024
    frame #20: 0x000000018a0bff80 UIKit`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 64
    frame #21: 0x000000018a0c3688 UIKit`_UIGestureRecognizerSendTargetActions + 124
    frame #22: 0x0000000189c8a73c UIKit`_UIGestureRecognizerSendActions + 260
    frame #23: 0x0000000189b290f0 UIKit`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 764
    frame #24: 0x000000018a0b3680 UIKit`_UIGestureEnvironmentUpdate + 1100
    frame #25: 0x000000018a0b31e0 UIKit`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 408
    frame #26: 0x000000018a0b249c UIKit`-[UIGestureEnvironment _updateGesturesForEvent:window:] + 268
    frame #27: 0x0000000189b2730c UIKit`-[UIWindow sendEvent:] + 2960
    frame #28: 0x0000000189af7da0 UIKit`-[UIApplication sendEvent:] + 340
    frame #29: 0x000000018a2e175c UIKit`__dispatchPreprocessedEventFromEventQueue + 2736
    frame #30: 0x000000018a2db130 UIKit`__handleEventQueue + 784
    frame #31: 0x0000000183bf6b5c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #32: 0x0000000183bf64a4 CoreFoundation`__CFRunLoopDoSources0 + 524
    frame #33: 0x0000000183bf40a4 CoreFoundation`__CFRunLoopRun + 804
    frame #34: 0x0000000183b222b8 CoreFoundation`CFRunLoopRunSpecific + 444
    frame #35: 0x00000001855d6198 GraphicsServices`GSEventRunModal + 180
    frame #36: 0x0000000189b627fc UIKit`-[UIApplication _run] + 684
    frame #37: 0x0000000189b5d534 UIKit`UIApplicationMain + 208
    frame #38: 0x00000001000da9a4 DuoYiIMOrig`main(argc=<unavailable>, argv=<unavailable>) at main.m:15 [opt]
    frame #39: 0x0000000182b055b8 libdyld.dylib`start + 4

恢復 OC 符號

第三方 app 往往都去除了符號,建議進行一下恢復符號表的操作。恢復符號表后,在調試時就能直接在堆棧中看到方法名,免去了計算偏移量然后在 hopper 里查找的麻煩。參考:iOS符號表恢復&逆向支付寶, restore-symbol

用 Xcode 直接調試

除了用命令行,也可以直接用 Xcode 進行 lldb 調試,有了圖形界面,也能使用Debug UI HierarchyDebug Memory Graph工具。參考iOS逆向:用Xcode直接調試第三方app

如果是 iOS 11 之前的越獄設備,需要重簽名后才能用 Xcode 調試。iOS 11 之后沒有限制,可以直接用 Xcode 調試 App Store 上下載的 app。

lldb常用命令

要想充分發揮 lldb 的動態調試功能,必須要學會使用 lldb 命令。

lldb命令可以在官網查看:GDB to LLDB Command Map。也可以參考:與調試器共舞 - LLDB 的華爾茲

常用命令如下:

求值、打印

  • expression, expr, e:后面可以執行一段代碼
  • print, prin, pri, p。是expression --的縮寫。可以用p/x, p/t, p/c, p/s分析打印16進制、二進制、字符、字符串格式
  • poe -o --的縮寫。表示以 對象 (Object) 的格式來打印結果
  • 求值之后會保存為臨時變量,使用變量時以$開頭:e int $a = 2 p $a * 19

流程控制

  • process continue, continue, c
  • thread step-over, next, n
  • thread step in, step, s
  • thread step-out, finish
  • thread return <RETURN EXPRESSION>:返回指定值

斷點

  • breakpoint list, br li:列出所有斷點
  • breakpoint enable, breakpoint disable, br dis, br del:后面跟斷點的序號,打開、關閉某個斷點
  • breakpoint set -f main.m -l 16:在源碼文件的某一行斷點
  • b main.m:17b_regexp-break的縮寫
  • 符號斷點:b isEven, br s -F isEven
  • 用正則表達式進行符號斷點:br set -r '正則'
  • 斷點條件:breakpoint modify -c 'i == 99' 1
  • 斷點時附加自定義操作:breakpoint command add 1

監控地址

  • 內存監控:
// 獲取需要監控的內存地址
p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))

(ptrdiff_t) $0 = 8

watchpoint set expression -- (int *)$myView + 8:監控_layer的地址

  • 變量監控:watchpoint set variable -w read_write
  • 條件監控:watchpoint modify -c '(global==5)'

內存,棧信息

  • 打印參數:frame variable, fr v
  • 打印方法名和行數:frame info
  • 打印寄存器的值:register read
  • 修改寄存器的值:register write rax 123
  • 打印棧回溯:thread backtrace, bt, bt all
  • 打印線程嵌套的棧回溯:thread backtrace -e true
  • 讀取內存:memory read --size 4 --format x --count 4 0xbffff3c0, me r -s4 -fx -c4 0xbffff3c0
  • 獲取內存創建棧:script import lldb.macosx.heap malloc_info --stack-history 0x10010d680。可以快速追溯對象的創建來源,參考iOS逆向:在任意app上開啟malloc stack追蹤內存來源

反匯編

  • disassemble --start-address 0x1eb8 --end-address 0x1ec3
  • disassemble --start-address 0x1eb8 --count 20
  • disassemble --frame --mixed, di -f -m
  • image list
  • image lookup --address 0x1ec4

lldb 命令擴展

lldb 可以使用 python 腳本編寫自定義功能。可以安裝 facebook 的開源庫chisel,提供了很多非常有用的命令。

安裝步驟如下。

Mac 中用brew install chisel下載 chisel,默認安裝到/usr/local/opt/chisel

也可以手動從 github 上下載。

下載后打開 Mac 上的~/.lldbinit,如果不存在則手動創建一個。在里面添加chisel

# ~/.lldbinit

# 如果是通過 brew install chisel 安裝
command script import /usr/local/opt/chisel/libexec/fblldb.py

# 如果是手動下載,則填寫 chisel 里的 fblldb.py 路徑
command script import /path/to/fblldb.py

之后重啟 Xcode,就能使用下面這些非常有用的命令了。

命令 描述
目錄
pdocspath 打印 app 的沙盒 Documents 目錄
pbundlepath 打印 app 的 bundle 目錄
對象查找
fv 用正則查找所有類的 view 實例
fvc 用正則查找所有類的 view controller 實例
findinstances 在內存中查找某個類的所有實例
flicker 閃爍某個 view,用于快速定位
對象分析
pinternals 打印對象內部的所有實例變量
pkp -valueForKeyPath:獲取對象的數據
pmethods 打印類的所有方法
poobjc 用 ObjC++ 語言執行和獲取表達式的結果,expression -O -l ObjC++ —的縮寫
pproperties 打印對象或者類的屬性
pivar 打印對象的某個 ivar
wivar 給對象的某個實例變量地址設置 watchpoint,監控變化
pclass 打印某個對象的類繼承鏈
pbcopy 打印對象并且把結果復制到粘貼板
pblock 打印 block 的實現函數地址和簽名
pactions 打印 UIControl 的 target 和 action
斷點
bdisable 用正則查找并關閉一組斷點
benable 用正則查找并開啟一組斷點
binside 用相對地址設置斷點,自動加上 ALSR 偏移
bmessage 給某個類的 method 設置斷點,同時會在其父類上查找 method
pinvocation 打印方法調用堆棧,僅支持x86
視圖查找
visualize 顯示 UIImage, CGImageRef, UIView 或 CALayer 的圖片內容,用 Mac 的預覽打開,在調試繪圖時非常有用
taplog 打印觸摸到的 view,用于快速定位
border 給 view 加上邊框,用于定位某個 view 對象
unborder 移除 view 或 layer 的邊框
caflush 修改 UI 后刷新 Core Animation 界面
hide 隱藏 view 或 layer
show 顯示一個 view 或者 layer,相當于執行view.hidden = NO
mask 給 view 添加半透明的 mask,可以用來查找被隱藏的 view
unmask 移除 view layer 的 mask
setinput 給作為 first responder 的 text field 或 text view 輸入文本
slowanim 減慢動畫速度
unslowanim 動畫速度回復正常
present Present 一個 view controller
dismiss 消除 present 出來的 view controller
視圖層級
pvc 循環打印 view controller 的層級
pviews 循環打印 view 的層級
pca 打印 layer 樹
vs 在 view 層級中搜索 view
ptv 打印最頂層的 table view
pcells 打印最頂層 table view 的所有可見的 cell
presponder 打印 UIResponder 響應者鏈
其他工具
sequence 執行多條命令,用;分隔
pjson 打印 NSDictionary 或 NSArray 的 JSON 格式
pcurl 用 curl 的格式顯示 NSURLRequest (HTTP)
pdata 用字符串的形式顯示 NSData
mwarning 模擬內存警告
視圖調試
alamborder 給有約束錯誤的 view 加上邊框
alamunborder 有約束錯誤的 view 加上邊框
paltrace 打印 view 的約束信息,相當于調用_autolayoutTrace
panim 是否正在執行動畫,相當于調用[UIView _isInAnimationBlock]

幾個有用的私有方法

NSObject有一些很有用的私有方法,可以方便查看對象的內容:

  • _methodDescription:打印對象或者類的整個繼承鏈上的方法列表,同時顯示方法的地址,可以直接用于斷點

  • _shortMethodDescription :打印對象或者類的方法列表,不顯示父類

  • _ivarDescription:打印對象或者類的所有實例變量和值

自定義 lldb 腳本

你可以 用 Python 腳本編寫自己的 lldb 命令,可以進一步提升動態調試的效率。

命令別名

可以在~/.lldbinit中添加 lldb 的初始化命令,如果沒有這個文件就創建一個。

command alias添加快捷命令,例如:

# reloadscript 命令:修改腳本文件后,重新加載
command alias reloadscript command source ~/.lldbinit

之后輸入reloadscript就相當于輸入command source ~/.lldbinit

編寫自定義腳本

編寫 Python 腳本,格式如下:

# some_script.py
import lldb

# 執行命令
def run(debugger, command, result, internal_dict):
    """
    Print root view controller of key window
    """
    print("hello world!")
    debugger.HandleCommand('po (id)[(id)[(id)[UIApplication sharedApplication] keyWindow] rootViewController]')
    

# lldb 啟動入口
def __lldb_init_module(debugger, internal_dict):
    # 添加 ptopvc 命令
    debugger.HandleCommand('command script add -f some_script.run ptopvc')

如果有chisel,可以直接使用chisel里封裝好的模塊和各種函數:

# some_script.py
import lldb
import fblldbbase as fb
    
# 可以同時聲明多個命令
def lldbcommands():
  return [ SomeCommand() ]

# 定義命令
class SomeCommand(fb.FBCommand):
  
  # 命令名
  def name(self):
    return 'ptopvc'

  # 描述
  def description(self):
    return 'Print root view controller of key window'

  # 選項
  def options(self):
    return [
      fb.FBCommandArgument(short='-v', long='--verbose', help='Show ivar of the result object', default=False, boolean=True)
    ]

  # 參數
  def args(self):
    return [ fb.FBCommandArgument(arg='instance or class', type='instance or Class', help='an Objective-C Class.') ]

  # 執行命令
  def run(self, arguments, options):
    print("hello world!")
    fb.evaluateExpression('(id)[(id)[(id)[UIApplication sharedApplication] keyWindow] rootViewController]')

詳情請見chisel代碼。

寫腳本時可以隨時在 lldb 里調用reloadscript命令重新加載,進行測試。

腳本提供了操作 lldb 的接口,例如設置斷點、執行命令。不過編寫命令有些坑:

  • 大部分 OC 方法和函數都需要明確聲明返回值類型
  • 指針聲明時需要初始化,不會默認設為 nil,否則在使用時會出現野指針

導入自定義腳本

打開~/.lldbinit添加:

# 導入自定義腳本的路徑
command script import /path/to/some_script.py

# 可以通過 chisel 提供的函數導入目錄下的所有腳本
script fblldb.loadCommandsInDirectory('/Users/xxx/Documents/code/lldbScript/')

實戰演練:追蹤block回調

下面演練一下 lldb 調試的過程。

有時候邏輯是通過block回調來執行的,追蹤調用路徑時,需要找出block的執行地址。直接打印block對象并不會顯示執行地址,需要分析內存才能找出。下面的分析流程和 lldb 命令pblock是一樣的。

block的結構

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
        unsigned long int reserved;     // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        // void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        // void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        // const char *signature;                         // IFF (1<<30)
        void* rest[1];
    } *descriptor;
    // imported variables
};

enum {
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
    BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code
    BLOCK_IS_GLOBAL =         (1 << 28),
    BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE =     (1 << 30),
};

查看invoke指針的地址

演示代碼如下:

- (void)modifyUIAtBackbround {
    void(^crash)() = ^ {
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self.view addSubview:[[UIView alloc] init]];
        });
    };
    
    crash();
}

當獲取到一個block變量時:

Printing description of $x1:
<__NSStackBlock__: 0x16fd465b8>

查看0x16fd465b8的內存,由于字節對齊的原因,結構體內的數據是按照最大的8字節對齊的:

(lldb) memory read --size 8 --format x 0x16fd465b8
0x16fd465b8: 0x00000001a94520d8 0x00000000c2000000
0x16fd465c8: 0x00000001000bf77c 0x000000010012c9f0
0x16fd465d8: 0x0000000131e0a610 0x00000001700517f0
0x16fd465e8: 0x00000001700517f0 0x000000016fd46650

void *isa 占用8字節,int占用4字節,所以invoke指針的值是0x00000001000bf77cdescriptor的地址是0x000000010012c9f0

對地址反匯編:

disassemble -a 0x00000001000bf77c
MyApp`__38-[ViewController modifyUIAtBackbround]_block_invoke_2:
    0x1000bf77c <+0>:   sub    sp, sp, #0x30             ; =0x30 
    0x1000bf780 <+4>:   stp    x29, x30, [sp, #0x20]
    0x1000bf784 <+8>:   add    x29, sp, #0x20            ; =0x20 
    0x1000bf788 <+12>:  adrp   x8, 125
    0x1000bf78c <+16>:  add    x8, x8, #0xe0             ; =0xe0 
    0x1000bf790 <+20>:  stur   x0, [x29, #-0x8]
    0x1000bf794 <+24>:  mov    x9, x0
    0x1000bf798 <+28>:  str    x9, [sp, #0x10]
    0x1000bf79c <+32>:  ldr    x9, [x0, #0x20]
    0x1000bf7a0 <+36>:  ldr    x1, [x8]
    0x1000bf7a4 <+40>:  mov    x0, x9
    0x1000bf7a8 <+44>:  bl     0x100113e98               ; symbol stub for: objc_msgSend
    0x1000bf7ac <+48>:  mov    x29, x29
    0x1000bf7b0 <+52>:  bl     0x100113eec               ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x1000bf7b4 <+56>:  adrp   x8, 124
    0x1000bf7b8 <+60>:  add    x8, x8, #0xed0            ; =0xed0 
    0x1000bf7bc <+64>:  adrp   x9, 126
    0x1000bf7c0 <+68>:  add    x9, x9, #0x308            ; =0x308 
    0x1000bf7c4 <+72>:  ldr    x9, [x9]
    0x1000bf7c8 <+76>:  ldr    x1, [x8]
    0x1000bf7cc <+80>:  str    x0, [sp, #0x8]
    0x1000bf7d0 <+84>:  mov    x0, x9
    0x1000bf7d4 <+88>:  bl     0x100113e98               ; symbol stub for: objc_msgSend
    0x1000bf7d8 <+92>:  adrp   x8, 124
    0x1000bf7dc <+96>:  add    x8, x8, #0xed8            ; =0xed8 
    0x1000bf7e0 <+100>: ldr    x1, [x8]
    0x1000bf7e4 <+104>: bl     0x100113e98               ; symbol stub for: objc_msgSend
    0x1000bf7e8 <+108>: adrp   x8, 125
    0x1000bf7ec <+112>: add    x8, x8, #0xe8             ; =0xe8 
    0x1000bf7f0 <+116>: ldr    x1, [x8]
    0x1000bf7f4 <+120>: ldr    x8, [sp, #0x8]
    0x1000bf7f8 <+124>: str    x0, [sp]
    0x1000bf7fc <+128>: mov    x0, x8
    0x1000bf800 <+132>: ldr    x2, [sp]
    0x1000bf804 <+136>: bl     0x100113e98               ; symbol stub for: objc_msgSend
    0x1000bf808 <+140>: ldr    x0, [sp]
    0x1000bf80c <+144>: bl     0x100113ebc               ; symbol stub for: objc_release
    0x1000bf810 <+148>: ldr    x0, [sp, #0x8]
    0x1000bf814 <+152>: bl     0x100113ebc               ; symbol stub for: objc_release
    0x1000bf818 <+156>: ldp    x29, x30, [sp, #0x20]
    0x1000bf81c <+160>: add    sp, sp, #0x30             ; =0x30 
    0x1000bf820 <+164>: ret

查看block的簽名

如果要進一步查看block的簽名,首先檢查block的flags,確定內存布局:

(lldb) p (BOOL)(0x00000000c2000000 & (1<<30))
(BOOL) $37 = YES
(lldb) p (BOOL)(0x00000000c2000000 & (1<<25))
(BOOL) $38 = YES

flags BLOCK_HAS_COPY_DISPOSE為YES,說明descriptor里有dispose_helperdispose_helpersignature在第8 + 8 + 8 + 8 = 32個字節。

查看descriptor的內存,第32個字節的內容:

(lldb) memory read --size 8 --format x 0x000000010012c9f0
0x10012c9f0: 0x0000000000000000 0x0000000000000028
0x10012ca00: 0x00000001000bf824 0x00000001000bf870
0x10012ca10: 0x0000000100115c77 0x0000000000000100
0x10012ca20: 0x0000000000000000 0x0000000000000028
(lldb) po (const char *)0x0000000100115c77
"v8@?0"

查看簽名:

(lldb) po [NSMethodSignature signatureWithObjCTypes:"v8@?0"]
<NSMethodSignature: 0x17027bd80>
    number of arguments = 1
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?'
        flags {isObject, isBlock}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

Cycript

Cycript 是 Saruik 大佬開發的動態調試工具,內置了一套 JavaScript 解釋器,可以用 js 腳本和 OC 交互,用 js 執行 OC 代碼,內置了一些很有用的功能。官網:http://www.cycript.org,源碼地址:https://git.saurik.com/cycript.git

其實 cycript 的大部分功能通過 lldb 都能實現。它的優勢是集成了越獄系統中的 substrate 庫,可以快速地進行 hook,并且 js 語法寫起來比較簡單。

安裝

越獄機去 Cydia 中可以直接搜索下載 cycript。現在 cycript 的兼容性有點問題,沒有適配 iOS 11 越獄,因此 iOS 11 在 Cydia 里找不到 cycript。需要自己去使用這個bfinject,如果安裝失敗,則嘗試這個分支:klmitchell2/bfinject

除了從第三方安裝,你也可以去官網下載 cycript 的 SDK 集成到 app 中使用。

使用

安裝后,ssh 連接到 iOS 設備,使用ps -e找到想要調試的進程,使用cycript -p <pid>連接指定的進程號后,就進入了cycript的調試控制臺。

在控制臺里,可以把 js 和 OC 語法混用。

方法調用和求值

調用 OC 方法:

cy# UIApplication.sharedApplication().windows[0].contentView().subviews()[0]
#"<SBFStaticWallpaperView: 0x1590ca730; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x1590cabd0>>"
cy# var c = [[UIApp windows][0] contentView]
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"

通過地址獲取對象:

cy# c = #0x10e883d40
#"<UIView: 0x10e883d40; frame = (0 0; 320 568); layer = <CALayer: 0x10e883e00>>"

獲取實例變量的值:

cy# c->_subviewCache
@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"]

打印對象的實例變量:

cy# *c
{isa:"UIView",_layer:#"<CALayer: 0x10e883e00>",_gestureInfo:null,_gestureRecognizers:null,_subviewCache:@[#"<SBFStaticWallpaperView: 0x11459fc40; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x11459ee70>>"],_charge:0,_tag:0,_viewDelegate:null,_backgroundColorSystemColorName:null,_countOfMotionEffectsInSubtree:1,_viewFlags:@error,_retainCount:8,_tintAdjustmentDimmingCount:0,_shouldArchiveUIAppearanceTags:false,_interactionTintColor:null,_layoutEngine:null,_boundsWidthVariable:null,_boundsHeightVariable:null,_minXVariable:null,_minYVariable:null,_internalConstraints:null,_constraintsExceptingSubviewAutoresizingConstraints:null}

調用 C 函數

cy# extern "C" int getuid();
(extern "C" int getuid())
cy# getuid()
501
cy# getuid = dlsym(RTLD_DEFAULT, "getuid")
(typedef void*)(0x7fff885f95b0)
cy# getuid()
throw new Error("cannot call a pointer to non-function")
cy# getuid = (typedef int())(getuid)
(extern "C" int getuid())
cy# getuid()
501

添加 category

cy# @implementation NSObject (MyCategory)
    - description { return "hello"; }
    - (double) f:(int)v  { return v * 0.5; }
    @end
cy# o = [new NSObject init]
#"hello"
cy# [o f:3]
1.5

查找指定類的對象

Cycript 的choose命令可以列出指定類的所有實例對象,和 lldb 命令findinstances類似:

cy# choose(SBIconModel)
[#"<SBIconModel: 0x1590c8430>"]
cy# var views = choose(SBIconView)
[#"<SBIconView: 0x159460fa0; frame = (27 92; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x159518ae0>; layer = <CALayer: 0x159461220>>",#"<SBIconView: 0x159468e50; frame = (114 356; 60 74); opaque = NO; gestureRecognizers = <NSArray: 0x15946d2f0>; layer = <CALayer: 0x1592c9a70>>",...

hook

通過 js 原型操作對象:

cy# var oldm = NSObject.prototype.description
(extern "C" id ":description"(id, SEL))

修改 prototype 進行 hook:

cy# NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; }
cy# [new NSObject init]
#"<NSObject: 0x100d11520> (of doom)"

Cycript 中也可以使用越獄機上的 hook 框架Cydia Substrate。使用MS.hookMessagehook OC 方法:

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)"

使用MS.hookFunctionhook C 函數:

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)]]

Frida

Frida 是一個跨平臺的動態調試工具,可以用 js 腳本和 OC 進行交互,從而執行代碼、打log、hook 函數。和 cycript 類似,不過兼容性比 cycript 要好。同樣的,frida 能做的,用 lldb 基本上也能做到。Frida 的優勢是跨平臺,以及提供的 js 庫、命令行,能夠實現腳本化。Frida 官網

安裝

Mac 端安裝 frida:

pip install --user frida-tools

iOS 設備在 Cydia 中添加源: https://build.frida.re,之后在 Cydia 中搜索 frida 安裝。

使用

列出進程

在 mac 上執行frida-ps -U等待 iOS 設備連接到 USB,連接到后就會列出 iOS 設備上正在運行的進程。

也可以用frida-ps -Uai列出正在運行的 app。

調用追蹤

可以使用frida-trace追蹤 app 的調用。

# Trace recv* and send* APIs in Safari
$ frida-trace -i "recv*" -i "send*" Safari

# Trace ObjC method calls in Safari
$ frida-trace -m "-[NSView drawRect:]" Safari
~ $ frida-trace -i "recv*" -i "read*" *twitter*
recv: Auto-generated handler: …/recv.js
# (snip)
recvfrom: Auto-generated handler: …/recvfrom.js
Started tracing 21 functions. Press Ctrl+C to stop.
    39 ms   recv()
   112 ms   recvfrom()
   128 ms   recvfrom()
   129 ms   recvfrom()

連接指定進程

使用frida -U <app名>連接到指定進程,也可以同時注入 js 腳本:frida -n Twitter -l demo1.js

連接上后,就可以執行 frida 的 js 命令,以及運行注入的 js 腳本。

Frida 提供了強大的 js 庫,可以去官網查看完整的 API 文檔:JavaScript API。這里只列出一些有用的接口。

執行腳本

與 OC 交互

參考JavaScript API: ObjC

獲取 OC 類列表:ObjC.classes

獲取指定類:var NSString = ObjC.classes.NSString;,并調用類方法:NSString.stringWithString_("Hello World");

調用實例方法:NSString.alloc().initWithString_("Hello World");

GCD 線程:

ObjC.schedule(ObjC.mainQueue, function () {
    NSString.stringWithString_("Hello World");
});

獲取內存中指定類的所有實例:

ObjC.choose(ObjC.classes.UIViewController, {
            onMatch: function (instance) {
                console.log("Found instance: " + instance);
            },
            onComplete: function () { }
                            // 搜索完畢
        });
var viewControllers = ObjC.chooseSync(ObjC.classes.UIViewController)

獲取 OC 對象:new ObjC.Object(ptr("0x1234"))

可以通過 js 對象的屬性獲取 OC 對象的內容:

  • $kind: string specifying either instance, class or meta-class
  • $super: an ObjC.Object instance used for chaining up to super-class method implementations
  • $superClass: super-class as an ObjC.Object instance
  • $class: class of this object as an ObjC.Object instance
  • $className: string containing the class name of this object
  • $protocols: object mapping protocol name to ObjC.Protocol instance for each of the protocols that this object conforms to
  • $methods: array containing native method names exposed by this object’s class and parent classes
  • $ownMethods: array containing native method names exposed by this object’s class, not including parent classes
  • $ivars: object mapping each instance variable name to its current value, allowing you to read and write each through access and assignment

調用 C 函數

獲取 C 函數指針:

var sqlite3_sql = Module.getExportByName('libsqlite3.dylib', 'sqlite3_sql');

var openPtr = Module.findExportByName(null,"open");

調用 C 函數:

var sqlite3_sql = new NativeFunction(sqlite3_sqlPtr, 'char', ['pointer']);
sqlite3_sql(statement);

var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var fd = open(Memory.allocUtf8String('/tmp/test.txt'), 0);

hook

Hook OC 方法:

// 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);
  },
  onLeave: function (retval) {
    // 執行后調用
    // 修改返回值
    retval.replace(1)
  }
});

替換 C 函數(OC 方法同理):

var openPtr = Module.getExportByName(null, 'open');
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
Interceptor.replace(openPtr, new NativeCallback(function (pathPtr, flags) {
  var path = pathPtr.readUtf8String();
  log('Opening "' + path + '"');
  var fd = open(pathPtr, flags);
  console.log('Got fd: ' + fd);
  return fd;
}, 'int', ['pointer', 'int']));

Hook C 函數:

Interceptor.attach(Module.getExportByName(null, 'open'), {
  onEnter: function (args) {
    // 執行前調用
    console.log('Context information:');
    console.log('Context  : ' + JSON.stringify(this.context));
    console.log('Return   : ' + this.returnAddress);
    console.log('ThreadId : ' + this.threadId);
    console.log('Depth    : ' + this.depth);
    console.log('Errornr  : ' + this.err);

    // Save arguments for processing in onLeave.
    this.fd = args[0].toInt32();
    this.buf = args[1];
    this.count = args[2].toInt32();
  },
  onLeave: function (result) {
    // 執行后調用
    console.log('----------')
    // Show argument 1 (buf), saved during onEnter.
    var numBytes = result.toInt32();
    if (numBytes > 0) {
      console.log(hexdump(this.buf, { length: numBytes, ansi: true }));
    }
    console.log('Result   : ' + numBytes);
  }
})

IDA 動態調試

IDA 也有一個動態調試工具,不過沒有 lldb 這么多針對 iOS 平臺的命令,只要是用來輔助逆向分析,查看控制流,打log。IDA 的 trace 功能可以從指令級別上記錄運行時程序的流程,查看寄存器和內存的值,不過 IDA 調試的同時不能使用 lldb,如果想要查看其他詳細信息,可以配合 cycript 或 frida。

如果你想分析代碼的控制流,可以使用 IDA 的動態調試。IDA 也有一些第三方插件用于輔助調試。

配置 debugger

IDA 提供了 iOS 的 debugger。首先將砸殼后的 app 用 IDA 分析完畢,再重簽名后安裝到 iOS 設備上,目的是讓 IDA 分析的二進制文件和設備上的保持一致。

之后設置 IDA 的 debugger 配置:

  1. 在 IDA 的Debugger->Switch Debugger中選擇Remote iOS Debugger
  2. 配置 debugger:打開Debugger->Debugger options,在彈出的面板中打開 Set specific options
    1. 設置Symbol path為當前設備的符號文件路徑,例如~/Library/Developer/Xcode/iOS DeviceSupport/11.2.2 (15C202)/Symbols/
    2. 勾選 Launch debugserver automatically
  3. 配置 process:打開Debugger->Process options,設置 ApplicationInput file為 app 二進制文件在 iOS 設備上的路徑,例如/var/containers/Bundle/Application/F366E63D-602B-47D9-B92E-1739A347192B/AppToDebug.app/AppToDebug
IDA_debugger_options.png
IDA_process_options.png

啟動調試

配置完后,在 iOS 設備上 kill 掉 app,就可以用 IDA 的Debugger->Start Process啟動進程進行調試。

啟動前可以先設置斷點,在斷點上設置 trace,可以用不同顏色表示控制流的路徑。

IDA_debugger.png

IDA 動態調試插件

IDA 有些開源插件用于增強動態調試功能。例如funcap可以記錄運行時的寄存器信息作為注釋,輔助分析。不過這個工具現在只支持 32 位。

其他的插件你可以自行搜索。不過能用到 iOS 上的動態調試插件并不多。

結尾

動態調試的整個流程以及用到的工具大部分都總結在此了。還有一個強力的工具這里沒有講解,就是 tweak 插件。由于內容有點多,留到之后的文章中再展開。

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容