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
3ea18dde
Commit
3ea18dde
authored
Apr 02, 2019
by
swift_gan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update doc
parent
300a651b
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
329 additions
and
9 deletions
+329
-9
doc.md
doc/doc.md
+329
-2
pc_relate.png
doc/res/pc_relate.png
+0
-0
HookStubManager.java
...swift/sandhook/xposedcompat/hookstub/HookStubManager.java
+0
-6
HookerDexMaker.java
...swift/sandhook/xposedcompat/methodgen/HookerDexMaker.java
+0
-1
No files found.
doc/doc.md
View file @
3ea18dde
...
...
@@ -16,8 +16,8 @@ Swift Gan
-
简介
-
ART Invoke 指令生成
-
基本实现
-
Xposed 支持
-
指令检查
-
Xposed 支持
-
inline 处理
-
Android Q
-
Demo
...
...
@@ -876,6 +876,7 @@ ART 的判断逻辑
那么我们需要暂停所有线程,并且等待 GC 完成
幸运的是,ART 等待调试器也需要这一操作,不仅仅是暂停所有线程,还需要等待 GC。
至于是否会影响性能这点不用担心,实测是 nm 级的
```
cpp
void
Dbg
::
SuspendVM
()
{
...
...
@@ -963,6 +964,7 @@ SandHook 支持两种 Hook "截获" 方案,inline 以及入口替换
-
如果是 inline,由于原入口已经被塞入跳板,所以我们需要另外一块 call origin 的跳板
-
禁止 backup JIT
-
如果原方法非静态方法,要保证其是 private
-
调用原方法,只需要反射调用 backup 方法即可
---
...
...
@@ -1140,4 +1142,329 @@ FUNCTION_END(CALL_ORIGIN_TRAMPOLINE)
-
当 ART 发现你的方法已经被编译的时候,就不会走 CodeEntry
-
ArtInterpreterToInterpreterBridge 直接解释 CodeItem
----
## 指令检查
-
Code 长度检查
-
Thumb 指令
-
指令对齐
-
PC 寄存器相关指令
---
### 指令长度
如果我们需要 inline 一个已经编译的方法,我们就必须知道该方法 Code 的长度能否放下我们的跳转指令,否则就会破坏其他 Code。
#### 获取指令长度
某个方法的 Code 在 Code Cache 中的布局为 CodeHeader + Code, 其中 CodeHeader 中存有 Code 的长度。
```
cpp
uint32_t
vmap_table_offset_
=
0u
;
uint32_t
method_info_offset_
=
0u
;
QuickMethodFrameInfo
frame_info_
;
// The code size in bytes. The highest bit is used to signify if the compiled
// code with the method header has should_deoptimize flag.
uint32_t
code_size_
=
0u
;
// The actual code.
uint8_t
code_
[
0
];
```
---
#### 获取指令长度
<font
size=
5
>
JitCompile->CommitCode->CommitCodeInternal
</font>
```
cpp
size_t
alignment
=
GetInstructionSetAlignment
(
kRuntimeISA
);
size_t
header_size
=
RoundUp
(
sizeof
(
OatQuickMethodHeader
),
alignment
);
size_t
total_size
=
header_size
+
code_size
;
OatQuickMethodHeader
*
method_header
=
nullptr
;
uint8_t
*
code_ptr
=
nullptr
;
uint8_t
*
memory
=
nullptr
;
{
...
{
....
code_ptr
=
memory
+
header_size
;
std
::
copy
(
code
,
code
+
code_size
,
code_ptr
);
method_header
=
OatQuickMethodHeader
::
FromCodePointer
(
code_ptr
);
new
(
method_header
)
OatQuickMethodHeader
(
(
stack_map
!=
nullptr
)
?
code_ptr
-
stack_map
:
0u
,
(
method_info
!=
nullptr
)
?
code_ptr
-
method_info
:
0u
,
frame_size_in_bytes
,
core_spill_mask
,
fp_spill_mask
,
code_size
);
FlushInstructionCache
(
reinterpret_cast
<
char
*>
(
code_ptr
),
reinterpret_cast
<
char
*>
(
code_ptr
+
code_size
));
```
---
#### 获取指令长度
那么可得出 CodeEntry - 4 就是存放 Code Size 的地址
---
### Thumb 指令
-
ARM32 模式根据 PC 来区别是 ARM 指令还是 Thumb 指令。
-
故 inline 的跳板也需要根据这两种模式进行分别实现
-
并且在跳转的时候要注意入口地址符合要求
```
cpp
bool
isThumbCode
(
Size
codeAddr
)
{
return
(
codeAddr
&
0x1
)
==
0x1
;
}
```
---
### 指令对齐
-
ART 使用 Thumb2 作为 Thumb 编译实现,Thumb2 指令又分为 Thumb16 和 Thumb32 指令
-
例如 mov 是 16 位指令,ldr 是 32 位指令
-
那么问题来了,我们一级跳板是 4 字节对齐的,原始指令是 2 字节对齐
-
所以在备份原指令的时候一定要注意指令的完整性
---
### PC 寄存器相关指令
先看一个 Java 函数:
```
java
public
void
setCloseOnTouchOutsideIfNotSet
(
boolean
close
)
{
if
(!
mSetCloseOnTouchOutside
)
{
mCloseOnTouchOutside
=
close
;
mSetCloseOnTouchOutside
=
true
;
}
}
```
---
#### 汇编代码

虽然这种情况不多,但是检查是必要的,如果我们发现这种指令,直接转用入口替换即可。
----
## Xposed 兼容
-
为何需要兼容
-
可选方案
-
实现
-
性能优化
---
### 为何要兼容 Xposed?
-
Xposed 已经拥有众多优秀的开源模块
-
Xposed 特殊的(Callback)分发模式可支持多个模块同时 Hook 同一个函数
-
原版 Xposed 需要 Root 并且替换定制 ART,并且 8.0 已经停止更新
---
### 可选方案
我们目前的方案需要手写一个签名与原方法类似的 Hook 方法,而 Xposed API 则使用 Callback,所以我们需要运行期间动态生成方法。
-
libffi 动态生成 Native 方法,将 origin 方法注册为 JNI 方法(Frida)
-
DexMaker 生成 Java 方法,性能略差
-
或者自己根据调用约定从栈捞参数(epic),兼容性存疑
---
### DexMaker
最终我选择了 DexMaker
-
需要兼容前面的注解 API
-
性能可接受,使用缓存只需要生成一次
-
只需要完成参数打包的工作,生成的代码及其有限
-
稳定不会有兼容问题
---
### 性能优化
为了优化第一次生成 Hook 方法的性能缺陷,采取了一种折中的方法,既可以不需要从栈中解析参数,也可以不用动态生成方法。
-
依然是写一堆 Stub,不过这次 Stub 是 Hook 方法
-
为了兼容更多的参数情况(Object 类型以及基本类型(浮点除外)),将所有参数设为 long(32位为 int),返回值类似
-
这样如果是基本类型参数,则可以简单转换得到值,Object 参数收到的则是内存地址
-
参数多的 stub 可以兼容较少参数的情况
---
写了一个 python 脚本以自动生成
```
java
//stub of arg size 3, index 13
public
static
long
stub_hook_13
(
long
a0
,
long
a1
,
long
a2
)
throws
Throwable
{
return
hookBridge
(
getMethodId
(
3
,
13
),
null
,
a0
,
a1
,
a2
);
}
```
---
#### 参数转换
```
java
public
static
Object
addressToObject64
(
Class
objectType
,
long
address
)
{
if
(
objectType
==
null
)
return
null
;
if
(
objectType
.
isPrimitive
())
{
if
(
objectType
==
int
.
class
)
{
return
(
int
)
address
;
}
else
if
(
objectType
==
long
.
class
)
{
return
address
;
}
else
if
(
objectType
==
short
.
class
)
{
return
(
short
)
address
;
}
else
if
(
objectType
==
byte
.
class
)
{
return
(
byte
)
address
;
}
else
if
(
objectType
==
char
.
class
)
{
return
(
char
)
address
;
}
else
if
(
objectType
==
boolean
.
class
)
{
return
address
!=
0
;
}
else
{
throw
new
RuntimeException
(
"unknown type: "
+
objectType
.
toString
());
}
}
else
{
return
SandHook
.
getObject
(
address
);
}
}
```
---
#### 对象与地址互转
-
一切的基础在于,当一个对象在栈上时,是不会 Moving GC 的,保证了对象地址在传递中的有效性
-
地址 -> 对象, 使用 ART 内部的 AddWeakGlobalRef
-
对象 -> 地址, 直接使用 Java 层的 Unsafe 类
---
#### 结果
-
如此,几乎 9 成以上的函数 Hook 都走内部 Stub 的方式,Hook 耗时在 1ms 以内
-
DexMaker 方式第一次大约需要 80ms 左右,第二次直接加载约为 3 - 5ms,其实也能接受
----
## Inline 处理
-
ART Inline 优化
-
阻止 JIT Inline
-
阻止 dex2oat Inline(Profile)
-
系统类中 Inline 的如何处理
---
### ART Inline 优化
ART 的 inline 类似其他语言的编译器优化,在 Runtime(JIT) 或者 dex2oat 期间, ART 将 “invoke 字节码指令” 替换成 callee 的方法体。
往往被 inline 的都是较为简单的方法。
---
### 阻止 JIT 期间的 Inline
观察 JIT Inline 的条件:
当被 inline 方法的 code units 大于设置的阈值的时候,方法 Inline 失败。
这个阈值是 CompilerOptions -> inline_max_code_units_
```
cpp
const
CompilerOptions
&
compiler_options
=
compiler_driver_
->
GetCompilerOptions
();
if
((
compiler_options
.
GetInlineDepthLimit
()
==
0
)
||
(
compiler_options
.
GetInlineMaxCodeUnits
()
==
0
))
{
return
;
}
bool
should_inline
=
(
compiler_options
.
GetInlineDepthLimit
()
>
0
)
&&
(
compiler_options
.
GetInlineMaxCodeUnits
()
>
0
);
if
(
!
should_inline
)
{
return
;
}
size_t
inline_max_code_units
=
compiler_driver_
->
GetCompilerOptions
().
GetInlineMaxCodeUnits
();
if
(
code_item
->
insns_size_in_code_units_
>
inline_max_code_units
)
{
VLOG
(
compiler
)
<<
"Method "
<<
PrettyMethod
(
method
)
<<
" is too big to inline: "
<<
code_item
->
insns_size_in_code_units_
<<
" > "
<<
inline_max_code_units
;
return
false
;
}
```
---
经过搜索,CompilerOptions 一般与 JitCompiler 绑定:
```
cpp
class JitCompiler {
public
:
static JitCompiler* Create();
virtual ~JitCompiler();
..............
private
:
std::unique_ptr<CompilerOptions> compiler_options_;
............
}
;
}
// namespace jit
}
// namespace art
```
---
ART 的 JitCompiler 为全局单例:
```
cpp
// JIT compiler
static
void
*
jit_compiler_handle_
;
jit
->
dump_info_on_shutdown_
=
options
->
DumpJitInfoOnShutdown
();
if
(
jit_compiler_handle_
==
nullptr
&&
!
LoadCompiler
(
error_msg
))
{
return
nullptr
;
}
jit_compiler_handle_
=
(
jit_load_
)(
&
will_generate_debug_symbols
);
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
;
}
```
---
#### 结论
ok,那么我们就得到了 “static void
*
jit_compiler_handle_” 的 C++ 符号 “_ZN3art3jit3Jit20jit_compiler_handle_E“
最后修改里面的值就可以了。
---
### 阻止 dex2oat Inline
除了 JIT 期间的内联
doc/res/pc_relate.png
0 → 100644
View file @
3ea18dde
227 KB
xposedcompat/src/main/java/com/swift/sandhook/xposedcompat/hookstub/HookStubManager.java
View file @
3ea18dde
...
...
@@ -13,15 +13,12 @@ import java.lang.reflect.Constructor;
import
java.lang.reflect.Member
;
import
java.lang.reflect.Method
;
import
java.lang.reflect.Modifier
;
import
java.util.Map
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
de.robv.android.xposed.XC_MethodHook
;
import
de.robv.android.xposed.XposedBridge
;
import
de.robv.android.xposed.XposedHelpers
;
import
static
de
.
robv
.
android
.
xposed
.
XposedBridge
.
sHookedMethodCallbacks
;
public
class
HookStubManager
{
public
static
volatile
boolean
is64Bit
;
...
...
@@ -42,9 +39,6 @@ public class HookStubManager {
public
static
HookMethodEntity
[]
hookMethodEntities
;
public
static
XposedBridge
.
AdditionalHookInfo
[]
additionalHookInfos
;
private
static
final
Map
<
Member
,
XposedBridge
.
CopyOnWriteSortedSet
<
XC_MethodHook
>>
hookCallbacks
=
sHookedMethodCallbacks
;
static
{
is64Bit
=
SandHook
.
is64Bit
();
Class
stubClass
=
is64Bit
?
MethodHookerStubs64
.
class
:
MethodHookerStubs32
.
class
;
...
...
xposedcompat/src/main/java/com/swift/sandhook/xposedcompat/methodgen/HookerDexMaker.java
View file @
3ea18dde
package
com
.
swift
.
sandhook
.
xposedcompat
.
methodgen
;
import
android.text.TextUtils
;
import
android.util.Log
;
import
com.android.dx.BinaryOp
;
import
com.android.dx.Code
;
...
...
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