Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
S
SandHook
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
SandHook
Commits
c6dc9d27
Commit
c6dc9d27
authored
Apr 01, 2019
by
swift_gan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update doc
parent
bb9a7b6e
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
262 additions
and
1 deletion
+262
-1
doc.md
doc/doc.md
+262
-1
inline_flow.png
doc/res/inline_flow.png
+0
-0
No files found.
doc/doc.md
View file @
c6dc9d27
...
...
@@ -241,6 +241,45 @@ static inline ArtMethod* DecodeArtMethod(jmethodID method_id) {
---
#### 方法入口
在 class link 时初步确定
```
cpp
//获取该 OAT 方法 Code 的入口地址,表示该方法已编译成机器码
// Install entry point from interpreter.
const
void
*
quick_code
=
method
->
GetEntryPointFromQuickCompiledCode
();
//获取该 Dex 方法 Code 的入口地址,表示该方法尚未编译,需要解释执行
bool
enter_interpreter
=
class_linker
->
ShouldUseInterpreterEntrypoint
(
method
,
quick_code
);
if
(
!
method
->
IsInvokable
())
{
EnsureThrowsInvocationError
(
class_linker
,
method
);
return
;
}
//如果是静态方法,并且不是构造函数,则把代码入口设置成一个桩函数的地址
//这个函数是通用的,应为所有 static 方法都要在类初始化时候去 resolve。
//那么先把这个方法设置成一个通用的跳板,当有其他方法调用到的时候,跳板方法将出发该类的初始化
//在该类初始化的时候,这些跳板方法才会被替换成真正的地址 ClassLinker::InitializeClass -> ClassLinker::FixupStaticTrampolines
if
(
method
->
IsStatic
()
&&
!
method
->
IsConstructor
())
{
// For static methods excluding the class initializer, install the trampoline.
// It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
// after initializing class (see ClassLinker::InitializeClass method).
method
->
SetEntryPointFromQuickCompiledCode
(
GetQuickResolutionStub
());
}
//如果是 JNI 方法,设置成通用的 JNI 函数跳板
else
if
(
quick_code
==
nullptr
&&
method
->
IsNative
())
{
method
->
SetEntryPointFromQuickCompiledCode
(
GetQuickGenericJniStub
());
}
//如果方法需要解释执行,则设置成解释执行的跳板
else
if
(
enter_interpreter
)
{
// Set entry point from compiled code if there's no code or in interpreter only mode.
method
->
SetEntryPointFromQuickCompiledCode
(
GetQuickToInterpreterBridge
());
}
```
---
### Quick & Optimizing
ART 中的 Compiler 有两种 Backend:
-
Quick
...
...
@@ -630,4 +669,226 @@ ldr x5, [x19, #PeerOffset]
## 基本实现
-
\ No newline at end of file
-
确定 ArtMethod 内存布局
-
Hooker 项解析
-
resolve 静态方法
-
resolve cache dex
-
禁用某方法JIT & 手动 JIT 编译
-
Hook 线程安全
-
原方法备份
-
inline hook
-
入口替换
---
### ArtMethod 内存布局
由于版本众多,以及 Android 平台的碎片化,Method 的内存布局往往是千变万化的。简单的根据版本写死 Offset 风险还是比较高的。
----
#### ArtMethod 的大小
首先最重要的的一点是确定 ArtMethod 的大小,前面我们知道,ArtMethod 被存放在线性内存区域,并且不会 Moving GC,那么,相邻的两个方法他们的 ArtMethod 也是相邻的,所以 size = ArtMethod2 - ArtMethod1
----
#### 内部元素偏移
-
我们可以在 Java 层反射得到一些值,或者说我们可以根据指定方法的属性确定预测值(accessFlag),然后我们根据预测值在 ArtMethod 中搜索偏移
-
根据元素在 ArtMethod 中的相对位置确定(code_entry 在最后)
---
### Hooker 项解析
-
首先 Hook 项承载了目标方法的信息,我们根据这些信息找到目标方法。
-
因为被 Hook 的方法会直接调到我们的 Hook 入口,Hook 入口本身也是一个 java 方法,所以参数需要和原方法匹配。
---
### resolve 静态方法
-
静态方法是懒加载的。
-
如果一个类没有被加载,那么其中的静态方法的入口统一为 art_quick_proxy_invoke_handler
-
第一次调用时,art_quick_proxy_invoke_handler 会走到类初始化流程
---
#### resolve 静态方法
那么很简单,只要手动调用就行了,但是要注意保证调用失败,这里使用不匹配的参数。
```
java
public
static
void
resolveStaticMethod
(
Member
method
)
{
//ignore result, just call to trigger resolve
if
(
method
==
null
)
return
;
try
{
if
(
method
instanceof
Method
&&
Modifier
.
isStatic
(
method
.
getModifiers
()))
{
((
Method
)
method
).
setAccessible
(
true
);
((
Method
)
method
).
invoke
(
new
Object
(),
getFakeArgs
((
Method
)
method
));
}
}
catch
(
Throwable
throwable
)
{
}
}
private
static
Object
[]
getFakeArgs
(
Method
method
)
{
Class
[]
pars
=
method
.
getParameterTypes
();
if
(
pars
==
null
||
pars
.
length
==
0
)
{
return
new
Object
[]{
new
Object
()};
}
else
{
return
null
;
}
}
```
---
### resolve dex cache
为了节省资源并且加快调用速度,和 ELF 的 got.plt 表类似,Caller 去搜索 Callee 的位置时,Callee 带着 index 去 DexCache 中找到对应位置的 Callee 的 ArtMethod 结构体。
但是,DexCache 是懒加载的,我们从 Hook 入口方法调用原方法这一行为 ART 是不知道的,所以无法自动完成这一动作,这里就需要我们手动完成这一操作。
当然后面我们我们也可以使用反射调用原方法来解决这一问题。
---
#### resolve 实现
-
6.0 以其以下 DexCache 相关的字段并没有被 ART 隐藏,所以可以直接通过反射在 Java 层完成
-
6.0 以上则需要在 Native 实现
-
8.1 以上 DexCache 最大为 1024,index 实际为真实 index % 1024 再去取,则有可能在运行期间被覆盖
-
所以 8.1 以后建议通过反射 invoke 调用原方法
---
### 手动 JIT
#### 编译策略
-
6.0 以其以下,默认在安装 apk 过程中会将 Dex 整体 OAT。
-
而 6.0 以上,默认策略是 quick_profile。即根据 profile 文件编译已知的热点方法,则大部分方法都不会被编译
-
则我们如果想使用 inline hook 的话,则必须手动将目标方法编译
-
除此之外,将 hook 入口方法编译可以避免一些意想不到的问题
---
#### 如何编译
-
ART 的主体是 libart.so,但是 Compiler 后端被单独编译到了 libart-compiler.so
-
我们只需要 dlsym libart-compiler.so 的导出方法 jit_compile_method 调用即可
-
需要注意 Android N 以上对 dlsym 的限制
---
```
cpp
extern
"C"
void
*
jit_load
(
bool
*
generate_debug_info
)
{
VLOG
(
jit
)
<<
"loading jit compiler"
;
auto
*
const
jit_compiler
=
JitCompiler
::
Create
();
CHECK
(
jit_compiler
!=
nullptr
);
*
generate_debug_info
=
jit_compiler
->
GetCompilerOptions
()
->
GetGenerateDebugInfo
();
VLOG
(
jit
)
<<
"Done loading jit compiler"
;
return
jit_compiler
;
}
extern
"C"
bool
jit_compile_method
(
void
*
handle
,
ArtMethod
*
method
,
Thread
*
self
,
bool
osr
)
REQUIRES_SHARED
(
Locks
::
mutator_lock_
)
{
auto
*
jit_compiler
=
reinterpret_cast
<
JitCompiler
*>
(
handle
);
DCHECK
(
jit_compiler
!=
nullptr
);
return
jit_compiler
->
CompileMethod
(
self
,
method
,
osr
);
}
```
---
#### 其他需要注意的
-
手动编译 JNI 方法将发生未知错误
-
在某些系统进程(zygote, system_server)里面,Compiler 是不需要初始化的,所以手动编译将会报错,很好理解,默认这些进程早就被 OAT 了
---
#### 禁用某个方法的 JIT
-
如果我们通过替换入口替换了原方法的 code_entry 来 hook,自然不希望当方法热度高的时候触发 JIT,那么,入口就会被替换掉了,Hook 失效。
-
除此之外,当 backup ArtMethod 被我们魔改后,profile 触发 hook 入口方法的 JIT 时,在编译 invoke backup 方法的字节码,将会遇到错误
```
java
ResolveCompilingMethodsClass
->
ClassLinker:
:
ResolveMethod
->
CheckIncompatibleClassChange
->
ThrowIncompatibleClassChangeError
```
---
#### 如何禁用?
ART 的判断逻辑
```
cpp
bool
IsCompilable
()
{
if
(
IsIntrinsic
())
{
// kAccCompileDontBother overlaps with kAccIntrinsicBits.
return
true
;
}
return
(
GetAccessFlags
()
&
kAccCompileDontBother
)
==
0
;
}
```
那么加上 kAccCompileDontBother 即可。
---
#### Hook 线程安全
由于在 Hook 时需要修改 ArtMethod 中多个字段,ART 在运行时,众多线程会依赖 ArtMethod,则因此可能导致错误状态。
-
正在 JIT 你修改的方法时
-
GC 时,GC 将会搜索栈,栈中有修改的 ArtMethod
-
正好其他线程调到了正在被修改的方法
-
其他线程发生栈回朔(异常),回朔到了正在修改的 ArtMethod
---
#### StopTheWord
那么我们需要暂停所有线程,并且等待 GC 完成
幸运的是,ART 等待调试器也需要这一操作,不仅仅是暂停所有线程,还需要等待 GC。
```
cpp
void
Dbg
::
SuspendVM
()
{
// Avoid a deadlock between GC and debugger where GC gets suspended during GC. b/25800335.
gc
::
ScopedGCCriticalSection
gcs
(
Thread
::
Current
(),
gc
::
kGcCauseDebugger
,
gc
::
kCollectorTypeDebugger
);
Runtime
::
Current
()
->
GetThreadList
()
->
SuspendAllForDebugger
();
}
void
Dbg
::
ResumeVM
()
{
Runtime
::
Current
()
->
GetThreadList
()
->
ResumeAllForDebugger
();
}
```
---
### 备份原方法
我们需要一个 "容器" 来备份原 ArtMethod。这里有两种方法:
-
New 出来
-
写一堆空方法作为 stub
这里我选择写 Stub,因为 New 有致命缺陷
---
#### New 的缺点
-
New 出来的 ArtMethod 不在 Linear 区,也就是说这个 "ArtMethod" 会被 Moving GC,那么每次调用原方法的时候,得去跳板中重新设置 ArtMethod 的地址。
-
虽然可以通过 dlsym 使用 ART 内部的函数 "art::LinearAlloc::Alloc" 在 Linear 区分配 "ArtMethod"
-
但是 ArtMethod 中的 declaring_class 是 GCRoot,是回 Moving GC 的,ART 并不知道他的存在,显然 GC 不会帮你更新假 "ArtMethod" 中的 declaring_class。
-
那么还是一样,调用前手动更新吧
---
### inline hook

\ No newline at end of file
doc/res/inline_flow.png
0 → 100644
View file @
c6dc9d27
59.1 KB
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment