一、概述
由于在工作中遇到了某翻譯so中有多線程調用,因此使用unidbg分析(基于unidbgMutilThread)并增加阻塞喚醒機制(futex系統調用),但仍未調用成功,因此本文概述對unidbg多線程的理解、android多線程的創建流程、實現簡單的阻塞喚醒、以及近段時間分析的總結,也希望大神網友能提出寶貴意見及分析方向,文末會有相關內容。
二、準備
android6.0(sdk23) ,kernel源碼
相關源碼路徑:
/bionic/libc/bionic/pthread_create.cpp
/bionic/libc/bionic/pthread_mutex.cpp
/bionic/libc/bionic/pthread_cond.cpp
/bionic/libc/bionic/clone.cpp
/bionic/libc/arch-arm/bionic/__bionic_clone.S
/bionic/libc/private/bionic_futex.h
/kernel/kernel/futex.c
三、開始分析
1. unidbgMutil的多線程創建分析
我們知道,在C中創建一個線程是要用到pthread_create這個函數的,這個函數簡單來說,在用戶空間通過mmap為子線程分配線程棧空間,在底層的是使用了clone這個系統調用創建線程。
因此unidbgMutil也選擇在clone這個系統調用里面實現自己的線程創建。
//com.github.unidbg.linux.ARM32SyscallHandler
private int pthread_clone(Backend backend, Emulator? emulator) {
. . . . . .
Pointer child_stack = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R1);
Pointer fn = child_stack.getPointer(0);
child_stack = child_stack.share(4);
Pointer arg = child_stack.getPointer(0);
child_stack = child_stack.share(4);
threadId = ++ThreadDispatcher.thread_count_index;
emulator.getThreadDispatcher().threadMap.put(threadId, new LinuxThread(emulator,child_stack, fn, arg));
. . . . . .
}
這里可以看到,在clone的系統調用里,我們取出了R1寄存器的值,然后又通過R1取得了fn、arg,接著創建一個LinuxThread對象,并把當前線程id和這個對象綁定在一起,存入全局的threadMap中。然后在LinuxThread里保存當前cpu上下文,保存線程棧,通過arg.getPointer(48) 獲取子線程函數的地址。通過this.arg.getPointer(52) 獲取子線程參數的地址。
其實到這里,我們需要分析一下,child_stack的連續取地址,arg的pointer 48,52的偏移究竟是什么,不然我們后續增加功能,修改代碼,就會一頭霧水。
2. Android 多線程分析
前邊簡單概述了pthread_create的相關內容,但如果要了解unidbg的多線程實現,我們則要詳細分析Android是如何創建多線程的。我們看代碼:
我們知道pthread_create一共有4個參數,這里要關注第三和第四個參數,也就是子線程函數的地址和參數。代碼塊1 調用了__allocate_thread函數,傳入thread變量(pthread_internal_t結構體,很重要),和child_stack指針。
進入后我們發現,這個函數的作用其實就是為我們的子線程,開啟一份棧空間,attr->guard_size是線程棧的保護區域這里是4k,__create_thread_mapped_space函數內部通過mmap系統調用,分配出一份匿名、私有的空間供子線程使用。然后將分配的內存大小,棧頂地址,賦值給threadp即pthread_internal_t。
到這里我們的棧空間已經分配完成,接下來就要進行子線程函數地址和參數的分配。也就是我們看到的在pthread_create代碼塊2那里,將start_routine和arg全都賦值給thread這個變量。然后就調用到clone這個函數。
clone:
int clone( int (*fn)(void *),
void *child_stack,
int flags,
void *arg,
.... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
通過查閱資料,linux中進程和線程的創建在內核中都是通過clone系統調用完成的,區別在于flags參數,因為線程是可以共享進程中的資源的,而進程和進程之間是隔離的,就是因為在clone系統調用中,flags參數的作用,如CLONE_VM,CLONE_FS,CLONE_SIGHAND等。
也就是說線程創建的本質是共享進程的虛擬內存、文件系統屬性、打開的文件列表、信號處理,以及將生成的線程加入父進程所屬的線程組中等等。這里flags參數在pthread_create內部已經寫好,我們這里只需要關注fn,child_stack和arg就可以了。
fn 表示 clone 生成的子進程/線程會調用 fn 指定的函數,我們發現這里的fn,并不是pthread_create中傳進來的子線程函數(start_routine),而是pthread_create內部的函數__pthread_start,而這個函數的參數必然不可能是子線程函數的參數,我們看一下,他的參數是thread變量(pthrea_internel_t),在我們前面的分析中,我們知道子線程的函數地址和函數參數就在這個thread變量中!
接著往下走,進入clone函數:
到這里,我們進入了_bionic_clone這個函數,這個函數在libc中是用匯編寫的,這里我們要注意下,_bionic_clone的參數和clone的參數位置,因為接下來我們要分析寄存器里的內容,如果參數搞混了就頭疼了。這里我們記住,fn雖然是clone要調用的子線程函數,但是我們真正的子線程函數在arg(thread)里。即fn -> __pthread_start,arg -> thread(子線程函數,參數),child_stack是mmap分配的,不用多說。
進入__bionic_clone這個匯編,他有7個參數,我們知道arm函數調用的參數傳遞,少于4個參數由R0-R3完成,多于4個參數用棧(sp)傳遞,并且入棧的方式是從右向左入棧。
這個代碼以及注釋已經寫得很清楚了,首先保存sp棧指針的值 mov ip, sp;然后將R4-R7入棧。linux的棧是高地址向低地址壓的,而且arm規定sp指向棧頂位置,因此下面兩條指令的含義是存儲原始的R4-R7寄存器的值,即將R4-R7入主線程的棧中,然后將ip中的值,也就是原始sp棧中的參數tid,fn,arg,加載到R4-R6寄存器中。
具體的stmfd,ldmfd,stmdb指令,可以查看相關資料,我畫了一個圖應該更容易理解這幾條指令。
接下來的指令stmdb r1!, {r5, r6},很重要,這條指令是理解unidbg中對child_stack的指令偏移的關鍵。stmdb的含義是,地址先減然后完成操作,因此r1寄存器的地址先減4(減4是因為32位)然后存入r6,再減4,存入r5。根據上邊的指令,r6里邊存的是arg參數,r5里邊存放的是fn指針。
接下來的指令ldr r7, =__NR_clone;swi #0;則是通過R7傳遞系統調用號,swi軟中斷(現在是svc指令,功能相同)從用戶空間(libc)真正進入到內核空間,之后的操作則是在內核態由kernel操作(位置在/kernel/kernel/fork.c -> SYSCALL_DEFINE5 -> do_fork完成,這里不是我們的重點),在unidbg里則是直接進入了ARM32SyscallHandler中的hook方法。
現在我們再來看一下child_stack的操作:
首先獲取R1寄存器的值(記得我們已經在"內核態"了),通過上邊的分析,我們已經非常清楚了,此時R1里的值就是fn,這個fn就是__pthread_start,child_stack.share(4);相當于R1地址加4,getPointer(0)就是獲取當前地址里的值,即arg,還記得這個arg實際上是一個pthread_internel_t的結構體,里面有我們子線程的函數地址和參數。
那么,this.fn = (UnidbgPointer) arg.getPointer(48);和UnidbgPointer this_arg=((UnidbgPointer) this.arg).getPointer(52);
猜想也能夠知道,就是pthread_internel_t的結構體里的子線程函數和參數,我們這里驗證一下pthread_internel_t所占的內存大小,由于類class(結構體struct)中定義的成員函數和構造和析構函數不占整體的空間。
因此可以計算,next,prev,cleanup_stack(指針類型占4字節),tid(int類型占4字節),join_state(枚舉類型占4字節),即5 * 4 = 20個字節。
其中attr為結構體,里面是int和指針類型,占4 * 6=24個字節,不過按照我這里的計算方式為44個字節偏移,少了4個字節,可能是計算join_state占用空間不對,或者在哪塊有內存對齊,有大神知道的話可以指導一下。
不過最終,start_routine所在的偏移是48個字節是沒毛病的,start_routine_arg所占的字節自然是48+4=52的位置。
到此,我們已經完整的分析了unidbgMutil的多線程創建機制,接下來將實現阻塞喚醒功能,以及提出我遇到的問題。
四、問題
當我在調用這個翻譯的so時,配置好環境后,用unidbg調用,在單線程的時候,有些是可以成功的。調用這個so分兩步:
(1) 加載模型
(2) 翻譯
但問題是大部分要傳入翻譯的字段,在unidbg里會陷入一個死循環,在系統調用號240的位置(futex),于是在大致看看so之后,發現這個so是使用多線程的,其中導入函數里面有很多關于線程同步的東西,鎖,信號量,條件變量等。于是我準備在unidbg的基礎上實現同步機制。
1. 測試
首先寫了一個demo,例子很簡單,就是創建3個線程,在子線程里進行加鎖,并用條件變量控制。主線程里是一個死循環,只有子線程操作完畢后,主線程才會退出循環,輸出完成的log。(測試用例的位置在unidbg-android/src/main/java/thread/Test )
2. 增加功能
在這個測試例子中,我們使用到了鎖(pthread_mutex_lock),條件變量(pthread_cond_wait/signal)對線程進行同步控制,而這些函數的底層機制都是使用到了futex這個系統調用,因此要了解一下linux futex機制。
(1) Futex概述
關于futex系統調用,網上資料很多,簡單來說,在android里可以實現進程/線程間阻塞喚醒功能。他的參數有很多,最主要的是前三個參數,第二個參數futex_op在android里只有兩個選項,FUTEX_WAIT,FUTEX_WAKE即阻塞和喚醒。
int futex ( int *uaddr, int futex_op, int val,
const struct timespec *timeout, /* or: uint32_t val2 */
int *uaddr2, int val3);
第一個參數uaddr是一個地址,地址里邊是一個int的值,一般被稱為futex字,或者futex變量。這個值一般是由用戶空間定義,比如pthread_mutex_lock函數在使用futex時,futex字就是&mutex->state這個值。
他的作用是當futex_op的類型為FUTEX_WAIT時,會比較futex字和第三個參數val的大小,如果相同表示要進入阻塞(不相等則失敗)。當futex_op的類型為FUTEX_WAKE時,第三個參數val的值,代表要喚醒阻塞著的進程/線程數,比如使用pthread_cond_broadcast時,val為INT_MAX,即喚醒所有線程。
(2) unidbg futex修改
知道了futex的原理,我們自己實現阻塞喚醒也就有了思路,由于實現多線程的方式是基于指令的時間片。
因此,阻塞對于我們來講,也就是在一個線程被阻塞后,unidbg切換線程時,不要切換到這個阻塞線程。喚醒就是可以重新切換到這個阻塞的線程。
因此我這里實現的方式比較簡單,在futex_wait里,將futex uaddr和當前線程id關聯起來,然后將當前線程id添加進阻塞線程。
喚醒的方式,同樣簡單粗暴,移除阻塞在uaddr上的任意一個線程即可。
然后,每當調用到futex阻塞和喚醒后,切換線程。
之前我切換線程時,直接在futex里進行切換,后來導致unicorn數據錯亂,一直報Invalid memory read (UC_ERR_READ_UNMAPPED)錯誤,這個錯誤是unicorn在emu_start里,如果某條指令出現問題,則會拋出異常,但是并不會告訴你是哪條指令。
幸運的是unidbg提供了tracecode的功能,于是經過多次調試后最終發現,在切換完線程進行保存/恢復寄存器上下文后,R0寄存器的值總是為0,這個奇怪的現象聯想到,這正是futex的返回值。系統調用返回后,會修改R0寄存器的值,進而導致了數據錯亂。接著我們把切換線程的代碼放到系統調用返回之后就OK了。
然后,我們的阻塞喚醒已經基本完成了(pthread_exit里有鎖會調用futex,會出現問題,不過線程已經退出了這個問題就沒有再研究)。
五、總結
到這里,本文也快結束了,其實本文看似是個分析貼,實則是一個求助帖,因為最后我仍然沒有把翻譯so調用成功。所以回過頭來,想了想近段時間一直在研究unidbg而減少了對翻譯so本身的研究,而對翻譯so的分析本身也充滿了挑戰。
所以請教各位網友,也想和大家交流一下,我們的目標是用unidbg成功調用so,并不需要還原so的算法,如何更好的去分析多線程的so,然后用unidbg模擬出來,目前我的思路可能就是看出錯堆棧,然后frida去hook原始so,比較跟unicorn調用的不同?
這個翻譯so在加載模型階段,會開啟4個線程,如果只單線程模式調用(只運行主線程),模型的加載可以成功,但后續的翻譯階段有的會陷入死循環。使用多線程加載時,加載模型階段失敗。希望有厲害的網友可以幫忙看一看。
最后,雖然沒有成功調用,但是對unidbg的理解又加深了一些,大致如下。
unidbg的內存布局:
[0xffffffffL-0xffff0000L] :svc #0 0xffff0fa0: bx lr
[0xffff0000L-0xfffe0000L]: ARMSvcMemory jni引用
[0xc0000000L-0xbff00000L] : 棧空間
[xxx - 0x40000000L] : so起始地址
- 打斷點:emulator.attach().addBreakPoint(address);
- 任意位置調試: emulator.attach().debug();
- 任意位置打印調用棧:emulator.getUnwinder().unwind();
- tracecode: emulator.traceCode(begin,end);
- patchcode: emulator.getMemory().pointer(address).setInt(patchCode); // nop 0xbf00bf00;
- 獲取modules:emulator.getMemory().getLoadedModules()。
- 繼承IOResolver接口,在resolve函數里可以監控open系統調用。
- 實現VirtualModule子類,注冊register方法,可以實現"虛擬"so的加載。
- 使用:
vm.setDvmClassFactory(newProxyClassFactory());ProxyDvmObject.createObject(vm,value);
通過反射可以直接使用java里的類。
-
Android
+關注
關注
12文章
3935瀏覽量
127348 -
寄存器
+關注
關注
31文章
5336瀏覽量
120232 -
Linux系統
+關注
關注
4文章
593瀏覽量
27392
發布評論請先 登錄
相關推薦
評論