作者: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
接下來,我來對這個解釋一下,這個過程中我做的事情:
- 首先使用pwntools計算出/tmp/test的main函數的地址
- 因為我的測試環境是arm64的Linux,所以參數寄存器是
x0, x1......,如果是amd64架構的,參數寄存器就是di, si, dx...... p /tmp/test:$ADDR %x0 %x1的含意就是在/tmp/test程序的ADDR地址處進行插樁,插入的代碼目的是輸出第一個參數和第二個參數的值,所以我們可以從結果中看到arg1=0x1 arg2=0xffffe00fb1d8,也就是說argc=0x1, argv = 0xffffe00fb1d8- 當我們把上面的語句寫入到
uprobe_events中后,將會在events/uprobes目錄下生成相應的事件目錄,默認情況下是以p_(filename)_(addr)的形式命名,所以,在當前測試環境中,這個目錄的路徑為:/sys/kernel/debug/tracing/events/uprobes/p_test_0x76c/ - 把1寫入到上面這個目錄的enable文件中,表示激活該事件,接著就是把1寫入到
tracing_on,激活內核的日志跟蹤功能。 - 最后,我們就能從
/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_write和SSL_read來進行對明文的讀寫,從socket抓包,抓到的肯定是看不懂的密文。但是從SSL_write和SSL_read的第二個參數來抓取,得到的就是明文了。
我們來測試一下,一般curl使用的庫都是:/lib/aarch64-linux-gnu/libssl.so.1.1。
所以我們首先需要使用pwntools從這個libc庫中獲取到SSL_write和SSL_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_write和SSL_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是如何加載的。
參考
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2029/
暫無評論