改机系列-字符串打印
字符串打印有时候可以辅助我们分析日志和应用开发的bug,在涉及不同的引擎中(比如游戏unity引擎中可以通过Il2CppString* il2cpp_string_new(const char* str),方法创建属于游戏自己类型的字符串,安卓中创建一个字符串new String("Hello, World!")还有传入不同的参数来创建java字符串)今天我们主要说下安卓中的字符串在java虚拟机中的处理。
学习目录
1.安卓中字符串的探索
2.监控字符串改机
安卓中字符串的探索
- • 对于逆向安全研究人员,部分应用我们可以通过hook string 打印堆栈来快速定位,创建字符串的地方,那么创建字符串的方法都在虚拟机做了什么处理,我们先从java层创建一个字符串开始说起,进入java中的类发现都是废弃的方法和空实现。
String a1 = new String("asd");
这是因为在Android运行时环境中,所有对这个构造函数的调用都被拦截并重定向到StringFactory。这是为了优化性能和内存使用。由于字符串在Java中是不可变的,所以复制字符串通常是不必要的。相反,Android的StringFactory可以重用已有的字符串实例,从而节省内存。我们抛弃了利用“new”关键字随意制造对象的方法,改用这个StringFactory类来构建并共享字符串元,外部需要字符串直接向StringFactory索要即可,感觉它还是用了一定的设计思想在里边,看安卓源码中一定要考虑到设计思想和代码的实现思路,这对我们学习帮助和印象的提升非常大,那么,我们从安卓源码中寻找看看虚拟机是怎么处理的。
创建字符串的代码走到汇编的解释器中,执行MterpInvokeDirect
// Hack for String init:
//
// Rewrite invoke-x java.lang.String.<init>(this, a, b, c, ...) into:
// invoke-x StringFactory(a, b, c, ...)
// by effectively dropping the first virtual register from the invoke.
//
// (at this point the ArtMethod has already been replaced,
// so we just need to fix-up the arguments)
//
// Note that FindMethodFromCode in entrypoint_utils-inl.h was also special-cased
// to handle the compiler optimization of replacing `this` with null without
// throwing NullPointerException.
extern "C" size_t MterpInvokeDirect(Thread* self,
ShadowFrame* shadow_frame,
uint16_t* dex_pc_ptr,
uint16_t inst_data)
REQUIRES_SHARED(Locks::mutator_lock_) {
JValue* result_register = shadow_frame->GetResultRegister();
const Instruction* inst = Instruction::At(dex_pc_ptr);
return DoInvoke<kDirect, /*is_range=*/ false, /*do_access_check=*/ false, /*is_mterp=*/ true>(
self, *shadow_frame, inst, inst_data, result_register) ? 1u : 0u;
}
这里进入到了DoInvoke,最终又会调用DoCallCommon
static ALWAYS_INLINE bool DoInvoke(Thread* self,
ShadowFrame& shadow_frame,
const Instruction* inst,
uint16_t inst_data,
JValue* result)
REQUIRES_SHARED(Locks::mutator_lock_) {
// Make sure to check for async exceptions before anything else.
if (is_mterp && self->UseMterp()) {
DCHECK(!self->ObserveAsyncException());
} else if (UNLIKELY(self->ObserveAsyncException())) {
return false;
}
const uint32_t method_idx = (is_range) ? inst->VRegB_3rc() : inst->VRegB_35c();
const uint32_t vregC = (is_range) ? inst->VRegC_3rc() : inst->VRegC_35c();
ArtMethod* sf_method = shadow_frame.GetMethod();
...
return DoCallCommon<is_range, do_assignability_check>(
called_method, self, shadow_frame,
result, number_of_inputs, arg, vregC);
}
在这里就有关于创建字符串相关的操作
static inline bool DoCallCommon(ArtMethod* called_method,
Thread* self,
ShadowFrame& shadow_frame,
JValue* result,
uint16_t number_of_inputs,
uint32_t (&arg)[Instruction::kMaxVarArgRegs],
uint32_t vregC) {
bool string_init = false;
// Replace calls to String.<init> with equivalent StringFactory call.
if (UNLIKELY(called_method->GetDeclaringClass()->IsStringClass()
&& called_method->IsConstructor())) {
called_method = WellKnownClasses::StringInitToStringFactory(called_method);
string_init = true;
}
关键方法 ArtMethod* WellKnownClasses::StringInitToStringFactory
ArtMethod* WellKnownClasses::StringInitToStringFactory(ArtMethod* string_init) {
#define TO_STRING_FACTORY(init_runtime_name, init_signature, new_runtime_name,
new_java_name, new_signature, entry_point_name)
DCHECK((init_runtime_name) != nullptr);
if (string_init == (init_runtime_name)) {
DCHECK((new_runtime_name) != nullptr);
return (new_runtime_name);
}
STRING_INIT_LIST(TO_STRING_FACTORY)
#undef TO_STRING_FACTORY
LOG(FATAL) << "Could not find StringFactory method for String.<init>";
UNREACHABLE();
}
#define STRING_INIT_LIST(V)
V(java_lang_String_init, "()V", newEmptyString, "newEmptyString", "()Ljava/lang/String;", NewEmptyString)
V(java_lang_String_init_B, "([B)V", newStringFromBytes_B, "newStringFromBytes", "([B)Ljava/lang/String;", NewStringFromBytes_B)
V(java_lang_String_init_BI, "([BI)V", newStringFromBytes_BI, "newStringFromBytes", "([BI)Ljava/lang/String;", NewStringFromBytes_BI)
V(java_lang_String_init_BII, "([BII)V", newStringFromBytes_BII, "newStringFromBytes", "([BII)Ljava/lang/String;", NewStringFromBytes_BII)
V(java_lang_String_init_BIII, "([BIII)V", newStringFromBytes_BIII, "newStringFromBytes", "([BIII)Ljava/lang/String;", NewStringFromBytes_BIII)
V(java_lang_String_init_BIIString, "([BIILjava/lang/String;)V", newStringFromBytes_BIIString, "newStringFromBytes", "([BIILjava/lang/String;)Ljava/lang/String;", NewStringFromBytes_BIIString)
V(java_lang_String_init_BString, "([BLjava/lang/String;)V", newStringFromBytes_BString, "newStringFromBytes", "([BLjava/lang/String;)Ljava/lang/String;", NewStringFromBytes_BString)
V(java_lang_String_init_BIICharset, "([BIILjava/nio/charset/Charset;)V", newStringFromBytes_BIICharset, "newStringFromBytes", "([BIILjava/nio/charset/Charset;)Ljava/lang/String;", NewStringFromBytes_BIICharset)
V(java_lang_String_init_BCharset, "([BLjava/nio/charset/Charset;)V", newStringFromBytes_BCharset, "newStringFromBytes", "([BLjava/nio/charset/Charset;)Ljava/lang/String;", NewStringFromBytes_BCharset)
V(java_lang_String_init_C, "([C)V", newStringFromChars_C, "newStringFromChars", "([C)Ljava/lang/String;", NewStringFromChars_C)
V(java_lang_String_init_CII, "([CII)V", newStringFromChars_CII, "newStringFromChars", "([CII)Ljava/lang/String;", NewStringFromChars_CII)
V(java_lang_String_init_IIC, "(II[C)V", newStringFromChars_IIC, "newStringFromChars", "(II[C)Ljava/lang/String;", NewStringFromChars_IIC)
V(java_lang_String_init_String, "(Ljava/lang/String;)V", newStringFromString, "newStringFromString", "(Ljava/lang/String;)Ljava/lang/String;", NewStringFromString)
V(java_lang_String_init_StringBuffer, "(Ljava/lang/StringBuffer;)V", newStringFromStringBuffer, "newStringFromStringBuffer", "(Ljava/lang/StringBuffer;)Ljava/lang/String;", NewStringFromStringBuffer)
V(java_lang_String_init_III, "([III)V", newStringFromCodePoints, "newStringFromCodePoints", "([III)Ljava/lang/String;", NewStringFromCodePoints)
V(java_lang_String_init_StringBuilder, "(Ljava/lang/StringBuilder;)V", newStringFromStringBuilder, "newStringFromStringBuilder", "(Ljava/lang/StringBuilder;)Ljava/lang/String;", NewStringFromStringBuilder)
这个TO_STRING_FACTORY函数宏套宏,当 STRING_INIT_LIST(TO_STRING_FACTORY) 被预处理器处理时,会生成一系列的代码,每一行都是 TO_STRING_FACTORY 宏的一个实例,使用 STRING_INIT_LIST 定义中的一组参数 例如,第一行 V(java_lang_String_init, "()V", newEmptyString, "newEmptyString", "()Ljava/lang/String;", NewEmptyString) 将会生成以下的代码:
DCHECK((java_lang_String_init) != nullptr);
if (string_init == (java_lang_String_init)) {
DCHECK((newEmptyString) != nullptr);
return (newEmptyString);
}
这里说下在 ART 中,EnterInterpreterFromDeoptimize 是用来处理反优化(Deoptimization)的情况的。反优化是指在某些情况下,已经被 JIT(Just-In-Time)编译器优化过的代码需要被转回到解释执行的情况。也会涉及到判断是不是字符串初始化函数的。
static inline bool IsStringInit(const DexFile* dex_file, uint32_t method_idx)
REQUIRES_SHARED(Locks::mutator_lock_) {
const dex::MethodId& method_id = dex_file->GetMethodId(method_idx);
const char* class_name = dex_file->StringByTypeIdx(method_id.class_idx_);
const char* method_name = dex_file->GetMethodName(method_id);
// Instead of calling ResolveMethod() which has suspend point and can trigger
// GC, look up the method symbolically.
// Compare method's class name and method name against string init.
// It's ok since it's not allowed to create your own java/lang/String.
// TODO: verify that assumption.
if ((strcmp(class_name, "Ljava/lang/String;") == 0) &&
(strcmp(method_name, "<init>") == 0)) {
return true;
}
return false;
}
在类的加载链接中也会设计到字符串的初始化,FinishInit方法在ClassLinker的初始化过程中被调用,以完成其初始化。不过这些是artruntime初始化时候做的,应该是挖坑占座,涉及到具体的处理还得是执行过程中的行为。最终,我们发现都会走到java_lang_StringFactory.cc和java.lang.StringFactory.java这些类中,
public static String newStringFromBytes(byte[] data) {
String result = newStringFromBytes(data, 0, data.length);
if (!result.isEmpty()) {
System.out.println("[" + TAG + "] Returned value: " + result);
logNewEmptyStringStackTrace();
}
return result;
}
public static String newStringFromBytes(byte[] data, int high) {
String result = newStringFromBytes(data, high, 0, data.length);
if (!result.isEmpty()) {
System.out.println("[" + TAG + "] Returned value: " + result);
logNewEmptyStringStackTrace();
}
return result;
}
而jni中创建字符串
jclass stringClass = env->FindClass("java/lang/String");
if (stringClass == NULL) {
return NULL;
}
jmethodID midInit = env->GetMethodID(stringClass, "<init>", "([C)V");
if (midInit == NULL) {
return NULL;
}
jcharArray charArray = env->NewCharArray( 5);
jchar chars[5] = {'H', 'e', 'l', 'l', 'o'};
env->SetCharArrayRegion( charArray, 0, 5, chars);
jstring newString = (jstring)env->NewObject(stringClass, midInit, charArray);
std::string hello = "Hello from C++";
const char* cStr = env->GetStringUTFChars(newString, 0);
__android_log_print(4,"qqq","%s", cStr);
return env->NewStringUTF(hello.c_str());
这里我们就要从NewObject说起
static jobject NewObject(JNIEnv* env, jclass java_class, jmethodID mid, ...) {
va_list args;
va_start(args, mid);
ScopedVAArgs free_args_later(&args);
CHECK_NON_NULL_ARGUMENT(java_class);
CHECK_NON_NULL_ARGUMENT(mid);
jobject result = NewObjectV(env, java_class, mid, args);
return result;
}
static jobject NewObjectV(JNIEnv* env, jclass java_class, jmethodID mid, va_list args) {
CHECK_NON_NULL_ARGUMENT(java_class);
CHECK_NON_NULL_ARGUMENT(mid);
ScopedObjectAccess soa(env);
ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(),
soa.Decode<mirror::Class>(java_class));
if (c == nullptr) {
return nullptr;
}
if (c->IsStringClass()) {
// Replace calls to String.<init> with equivalent StringFactory call.
jmethodID sf_mid = jni::EncodeArtMethod<kEnableIndexIds>(
WellKnownClasses::StringInitToStringFactory(jni::DecodeArtMethod(mid)));
return CallStaticObjectMethodV(env, WellKnownClasses::java_lang_StringFactory, sf_mid, args);
}
ObjPtr<mirror::Object> result = c->AllocObject(soa.Self());
if (result == nullptr) {
return nullptr;
}
jobject local_result = soa.AddLocalReference<jobject>(result);
CallNonvirtualVoidMethodV(env, local_result, java_class, mid, args);
if (soa.Self()->IsExceptionPending()) {
return nullptr;
}
return local_result;
}
最终执行到CallNonvirtualVoidMethodV到InvokeWithVarArgs这里又涉及到了字符串的创建相关处理
bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();
if (is_string_init) {
// LOG(ERROR)<<"STRING BEFORE REPLACE "<<method->PrettyMethod();
// Replace calls to String.<init> with equivalent StringFactory call.
method = WellKnownClasses::StringInitToStringFactory(method);
// LOG(ERROR)<<"STRING AFTER REPLACE "<<method->PrettyMethod();
}
ObjPtr
也是最终用到了StringInitToStringFactory这个方法。至此java和jni反射创建字符串的流程看完了,然后还有涉及 std::string下的字符串创建,这里大家可以自行探索,理论上能想通的,都可以进行实现的。
监控字符串改机
根据以上我们可以在java_lang_StringFactory.cc和java.lang.StringFactory.java这些类中,写入一些堆栈代码,当有app函数执行这些方法的时候,我们可以将其堆栈打印出来,当然也可以使用Frida hook ,用啥都无所谓了,效果图演示,会有一些日志影响我们分析,我们可以进行过滤即可。
思路总结:
字符串在安卓虚拟机的处理流程
1.java层字符串创建处理执行流程
2.jni反射字符串创建处理执行流程
3.在关键类中添加自己的堆栈代码。
原文始发于微信公众号(小瑶在工地):改机系列-字符串打印
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论