作者:Hcamael@知道創宇404實驗室
日期:2022年11月30日
相關閱讀:
在 Android 中開發 eBPF 程序學習總結(一)
在 Android 中開發 eBPF 程序學習總結(二)

在研究uprobe的過程中,發現了Linux內核一個好用的功能。

本來是打算研究一下,怎么寫uprobe的代碼,寫好后怎么部署,然后又是怎么和相應的程序對應上的。但是資料太少了,基本上都是寫使用bpftrace或者bcc的例子,但是都不是我想要的,后面考慮研究一下bpftrace或者bcc的源碼。

不過在這個過程中,卻發現了一個Linux系統內置的uprobe插樁的功能。

一般在/sys/kernel/debug/tracing/目錄下,有一個uprobe_events文件,在Android設備下,沒有debug目錄,所以路徑一般為: /sys/kernel/tracing/uprobe_events

那么我們怎么通過這個文件進行uprobe插樁呢?

首先,我們寫一個測試代碼:

#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("Hello World!\n");
  return 0;
}

一個很簡單的,使用C語言開發的Hello World程序,編譯一下:$ gcc test.c -o /tmp/test

接著,我們再寫一個腳本:

#!/bin/bash
ADDR=`python3 -c 'from pwn import ELF,context;context.log_level="error";e=ELF("/tmp/test");print(hex(e.symbols["main"]))'`
echo "p /tmp/test:$ADDR %x0 %x1" > /sys/kernel/debug/tracing/uprobe_events
echo 1 | tee /sys/kernel/debug/tracing/events/uprobes/p_*/enable
echo 1 | tee /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe

把這個腳本運行起來,接著,我們再開一個終端,運行一下/tmp/test,隨后我們就能看到前一個終端里有輸出了:

root@ubuntu:~# /tmp/test.sh
1
1
            test-3326935 [001] ..... 1187528.405340: p_test_0x76c: (0xaaaaddbc076c) arg1=0x1 arg2=0xffffe00fb1d8

接下來,我來對這個解釋一下,這個過程中我做的事情:

  1. 首先使用pwntools計算出/tmp/test的main函數的地址
  2. 因為我的測試環境是arm64的Linux,所以參數寄存器是x0, x1......,如果是amd64架構的,參數寄存器就是di, si, dx......
  3. p /tmp/test:$ADDR %x0 %x1的含意就是在/tmp/test程序的ADDR地址處進行插樁,插入的代碼目的是輸出第一個參數和第二個參數的值,所以我們可以從結果中看到arg1=0x1 arg2=0xffffe00fb1d8,也就是說argc=0x1, argv = 0xffffe00fb1d8
  4. 當我們把上面的語句寫入到uprobe_events中后,將會在events/uprobes目錄下生成相應的事件目錄,默認情況下是以p_(filename)_(addr)的形式命名,所以,在當前測試環境中,這個目錄的路徑為: /sys/kernel/debug/tracing/events/uprobes/p_test_0x76c/
  5. 把1寫入到上面這個目錄的enable文件中,表示激活該事件,接著就是把1寫入到tracing_on,激活內核的日志跟蹤功能。
  6. 最后,我們就能從/sys/kernel/debug/tracing/trace_pipe目錄中看到相關的輸出了。

再來看看輸出的數據格式:

test-3326935, 監控到的程序名-該程序的pid
[001], CPUID
1187528.405340, 時間戳相關?
p_test_0x76c, 事件名
0xaaaaddbc076c, ELF地址
arg1 arg2,  就是我們自己定義的輸出內容

當我發現Linux內核功能,我是很驚訝的,竟然能這么容易的監控到任意程序的指定地址的信息,就是不知道對于一個程序來說,是否能發現自己被uprobe插樁了。

接著,我就繼續深入的研究了該功能,看看使用場景如何。

自定義事件名

事件名我們是可以自定義的,比如,我只要把事件語句改為:"p:test_main /tmp/test:$ADDR %x0 %x1"

那么事件名就為test_main了,生成的相應目錄就是/sys/kernel/debug/tracing/events/uprobes/test_main/

輸出字符串

通過研究發現,可以使用-/+加上offset,加上(addr)來輸出指定地址的內存,然后加上:type來指定該數據的類型,并且該操作是可以嵌套的,所以是可以輸出任意類型的結構體的。

比如我把事件語句改為: p:test_main /tmp/test:$ADDR %x0 %x1 +0(%x1):x64 +0(+0(%x1)):string

我們可以看看現在的輸出:

root@ubuntu:~# /tmp/test.sh
1
1
            test-3331614 [001] ..... 1189161.610316: test_main: (0xaaaad45607ac) arg1=0x2 arg2=0xffffff3cfef8 arg3=0xffffff3d06ea arg4="/tmp/test"

0xffffff3cfef8地址的內存為0xffffff3d06ea,而0xffffff3d06ea地址的內容為字符串:/tmp/test,也就是argv[1]的內容了。

返回值插樁

事件語句的開始是p,表示對當前地址進行插樁,但是如果換成r,那么就是對返回地址進行插樁,比如:r:test_main /tmp/test:0x7d4 %x0

0x7d4為main函數的ret指令的地址,然后得到的輸出為:

$ /tmp/test.sh "r:test_main /tmp/test:0x7d4 %x0"1
1
            test-3333703 [000] ..... 1189862.625909: test_main: (0xffffa1239e10 <- 0xaaaac4fa07d4) arg1=0x0

數據中多了一個:從當前地址0xaaaac4fa07d4要返回到地址0xffffa1239e10

libc庫插樁

libc庫的插樁跟普通程序沒啥區別,比如,一般https請求都是通過SSL_writeSSL_read來進行對明文的讀寫,從socket抓包,抓到的肯定是看不懂的密文。但是從SSL_writeSSL_read的第二個參數來抓取,得到的就是明文了。

我們來測試一下,一般curl使用的庫都是:/lib/aarch64-linux-gnu/libssl.so.1.1

所以我們首先需要使用pwntools從這個libc庫中獲取到SSL_writeSSL_read的地址,但是SSL_read又不同,因為函數入口點buf數據是無用的,需要該函數調用結束后,里面才有有效數據,但是在ret返回的時候,沒有寄存器儲存buf的地址,目前也沒找到辦法在函數入口的地方定義一個變量,然后返回的時候再取。

接著,我把libssl.so丟入了ida,找到了SSL_read函數:

__int64 __fastcall SSL_read(__int64 a1, __int64 a2, int a3)
{
  __int64 result; // x0
  unsigned int v4; // [xsp+20h] [xbp+20h] BYREF

  if ( (a3 & 0x80000000) != 0 )
  {
    ERR_put_error(20LL, 223LL, 271LL, "../ssl/ssl_lib.c", 1777LL);
    return 0xFFFFFFFFLL;
  }
  else
  {
    LODWORD(result) = sub_34830(a1, a2, a3, &v4, 0LL);
    if ( (int)result <= 0 )
      return (unsigned int)result;
    else
      return v4;
  }
}

通過SSL_read函數,我找到了sub_34830函數:

__int64 __fastcall sub_34830(__int64 a1, __int64 a2, __int64 a3, _QWORD *a4)
{
  unsigned int v6; // w21
  int v7; // w1
  __int64 v12; // x3
  __int64 v13; // x3
  __int64 v14[3]; // [xsp+40h] [xbp+40h] BYREF
  int v15; // [xsp+58h] [xbp+58h]
  __int64 v16; // [xsp+60h] [xbp+60h]

  if ( *(_QWORD *)(a1 + 48) )
  {
    v6 = *(_DWORD *)(a1 + 68) & 2;
    if ( v6 )
    {
      v6 = 0;
      *(_DWORD *)(a1 + 40) = 1;
    }
    else
    {
      v7 = *(_DWORD *)(a1 + 132);
      if ( v7 == 1 || v7 == 8 )
      {
        ERR_put_error(20LL, 523LL, 66LL, "../ssl/ssl_lib.c", 1744LL);
      }
      else
      {
        sub_49588(a1, 0LL);
        if ( (*(_DWORD *)(a1 + 1496) & 0x100) != 0 && !ASYNC_get_current_job() )
        {
          v12 = *(_QWORD *)(a1 + 8);
          v14[0] = a1;
          v14[1] = a2;
          v13 = *(_QWORD *)(v12 + 56);
          v14[2] = a3;
          v15 = 0;
          v16 = v13;
          v6 = sub_32AD8(a1, v14, sub_329A0);
          *a4 = *(_QWORD *)(a1 + 6168);
        }
        else
        {
          return (*(unsigned int (__fastcall **)(__int64, __int64, __int64, _QWORD *))(*(_QWORD *)(a1 + 8) + 56LL))(
                   a1,
                   a2,
                   a3,
                   a4);
        }     // 猜測這里是ctx->method->ssl_read
      }
    }
  }
  else
  {
    v6 = -1;
    ERR_put_error(20LL, 523LL, 276LL, "../ssl/ssl_lib.c", 1733LL);
  }
  return v6;
}

查看調用ctx->method->ssl_read的匯編代碼:

.text:00000000000348A4 loc_348A4                               ; CODE XREF: sub_34830+68↑j
.text:00000000000348A4                 LDR             X4, [X19,#8]
.text:00000000000348A8                 MOV             X3, X24
.text:00000000000348AC                 MOV             X2, X23
.text:00000000000348B0                 MOV             X1, X22
.text:00000000000348B4                 MOV             X0, X19
.text:00000000000348B8                 LDR             X4, [X4,#0x38]
.text:00000000000348BC                 BLR             X4
.text:00000000000348C0                 MOV             W21, W0
.text:00000000000348C4                 LDP             X23, X24, [SP,#0x70+var_40]
.text:00000000000348C8                 B               loc_348E8

我們能發現,buf被儲存在了X22寄存器里,然后當調用完ctx->method->ssl_read,這個時候X22寄存器里就是有效的明文數據了,所以我們可以把uprobe插在0x348C4,然后我們以字符串輸出寄存器X22,這就是明文數據了。

最后我們可以得到以下事件語句:

ADDR=`python3 -c 'from pwn import ELF,context;context.log_level="error";e=ELF("/lib/aarch64-linux-gnu/libssl.so.1.1");print(hex(e.symbols["SSL_write"]))'`
p:SSL_write /lib/aarch64-linux-gnu/libssl.so.1.1:$ADDR +0(%x1):string
p:SSL_read /lib/aarch64-linux-gnu/libssl.so.1.1:0x348C4 +0(%x22):string

然后啟動我們的腳本,再另一個終端里使用curl訪問百度,我們可以得到以下輸出:

root@ubuntu:~# /tmp/test.sh
1
1
            curl-3339154 [001] ..... 1191831.068149: SSL_write: (0xffffa4b5fc70) arg1="GET / HTTP/1.1
Host: www.baidu.com
User-Agent: curl/7.68.0
Accept: */*

"
            curl-3339154 [001] ..... 1191831.088676: SSL_read: (0xffffa4b5f8c4) arg1="HTTP/1.1 200 OK
Accept-Ranges: bytes
......

實際應用場景

普通程序

Android設備上的ssl庫是/system/lib64/libssl.so,如果使用該庫,那么uprobe插樁的思路跟上面的例子講的一樣。

某信APP

研究中發現,插樁了libssl.so,但是卻沒有辦法得到Chrome或者某信的流量。經過一番研究,我發現了這篇文章:自動定位webview中的SLL_read和SSL_write

原來某信用的是webview,其libc位于:/data/data/com.xxxx/app_xwalk_4317/extracted_xwalkcore/libxwebcore.so

隨后就把這個libc掏出來,丟入IDA,根據上面這篇文章中所說的,去定位SSL_writeSSL_read

然后就能成功獲取到流量了:

$ ./uprobe_test.sh
......
  NetworkService-19594 [006] .... 338986.936127: SSL_write: (0x75c2f17548) buf="GET /webview/xxxxx
......
  NetworkService-19594 [006] .... 338987.021581: SSL_read: (0x75c2f17320) buf="HTTP/1.1 200 OK
Date: Wed, 02 Nov 2022 10:29:42 GMT
Content-Type: text/html
Content-Length: 0
Connection: keep-alive
......

解密某信通信流量

上面的例子中,能抓到的都是在某信中訪問HTTPS網頁的流量,那發消息的流量呢?經過我一番搜索,發現其通信流量是使用Java_xxx_MMProtocalJni_pack函數來加密的,但是相關資料很少,估計都被公關掉了。

我就自能自行逆向了,但是沒有調試環境,這代碼也很難逆,就在我陷入僵局的時候,我發現了一個compressBound函數,再其之后還有一個compress2函數:

......
if ( *a4 == 1 )
        {
          v11 = compressBound(size);
          v12 = v11;
          v15 = v11;
          sub_3CA68((__int64)v16);
          sub_3CAA4(v16, v12);
          v13 = sub_3CDF8(v16);
          v14 = compress(v13, &v15, a1, size);
          sub_3CCD8(v16, (unsigned int)v15);
......

然后我就在該函數下插入uprobe,打印a1變量,果然,這個就是我們發送的消息的明文:

比如我向好像發送`Test123`消息,可以看到:
  binder:13658_8-15519 [005] .... 328460.408711: SSL_mm: (0x75ad943444) arg1=#


(好友ID)Test123 ?(???" arg2=0x27
發送表情:
  binder:13658_8-15519 [000] .... 328488.173019: SSL_mm: (0x75ad943444) arg1=\$


(好友ID)[發呆] ????(???"" arg2=0x28
發送圖片:
      mars::2961-2961  [005] .... 328527.422874: SSL_mm: (0x75ad943444) arg1="
%aupimg_xxxxx(好友ID) Z(x2" arg2=0x98

其他

Linux內核自帶的uprobe事件,可以讓我們不需要寫任何代碼,就監控系統用戶態的函數調用,打印數據,功能雖然單一,但十分強大。后續我考慮研究是否能對其進行擴展,還有,我們自己寫的uprobe是如何加載的。

參考

  1. https://mabin004.github.io/2020/07/24/%E8%87%AA%E5%8A%A8%E5%AE%9A%E4%BD%8Dwebview%E4%B8%AD%E7%9A%84SLL-read%E5%92%8CSSL-write/

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2029/