昨天发的那篇标题有点小问题,又懒得重新推,就这样吧。
. . . * . * ☄️. * . * . 🦉 .* . * . 🌟 .* . * . . .
读题-确定方向
这道题笔者个人认为比较有代表性,代表性在有那么一些些android知识,有点迷惑性,而破题的思路又非常多,适合花样拷打~
首先GDA一把梭,无加壳无混淆。直接看向主Activity,发现其注册了一个广播(Receiver),对象为Send_to_Activity:
跟过来,这里去读了传递Intent里的String msg,然后根据它的值跳转到对应的Activity。
跟到ThisIsTheRealOne,发现这里使用的是native函数:
而且这个native函数要在点击Button之后才能调用,调用后发送一个广播:
但是从前文来看,这个广播根本没有落脚点(广播了,但是没有后续了,因为唯一一个Receiver只会进入else然后不展示msg),并且一开始的MainActivity也不会导向任何一个Activity。也就是说出题人构造了一个不可能实现的调用,那么破题思路就在于主动调用Activity或者更直接的调用对应的算flag函数。
方法一 ActivityManager+Frida Hook
Hook是最常见的思路,毕竟flag已经在运行中算出来了,但没展示而已。
先用am直接拉起ThisIsTheRealOne,然后点击BROADCAST INTENT:
拉起语句为:
am
start-activity -n com.example.hellojni/com.example.application.IsThisTheRealOne
点击之后并没有输出flag,因为onClick算出flag之后将其作为Intent返回了Send_to_Activity。
这也简单,我们只需要Hook到sendBroadcast或者putExtra即可(不要怀疑,通常Hook越底层越方便,Hook Android自带函数最好^ ^)。
var
intent=Java.use(
'android.content.Intent'
);
intent.putExtra.overload(
'java.lang.String'
,
'java.lang.String'
).implementation=
function
(
name,stri
)
{
console
.log(
"进入Hook"
);
console
.log(name);
console
.log(stri);
}
Hook成功之后使用am依次拉起三个Activity,即可获得flag:
flag为CTF{IDontHaveABadjokeSorry}。
方法二 Java+Unidbg还原调用环境
根据前面对源码的分析,我们知道,三个Activity分别都会调用一个native方法算flag,而native方法传的参又经过了一个自建方法的加密(Utilities.doBoth):
而Unidbg实际上也是在Java环境运行的(不过一般的JDK环境和Android SDK环境肯定是有些不同的,更不要说不同JDK之间都会打架)。也就是说我们可以直接复制这个类到Unidbg的环境中进行调用~这也多亏了GDA反编译的Java代码一般都十分甚至九分可读,照搬之后需要改的地方并不多。
这是改好的工具类代码,扔进随便一个Java工程里导个包就行:
import
java.lang.Object;
import
java.lang.String;
import
java.security.MessageDigest;
import
java.lang.StringBuilder;
import
java.lang.Byte;
import
org.apache.commons.codec.binary.Base64;
import
java.util.HashMap;
import
java.lang.Integer;
import
java.lang.Character;
import
java.util.Map;
public
class
Utilities
//
class
@000019
from
classes
.
dex
{
public
static
String
customEncodeValue
(String input)
{
MessageDigest md =
null
;
String output =
""
;
byte
[] input_bytes = input.getBytes();
try
{
md = MessageDigest.getInstance(
"SHA-224"
);
}
catch
(java.security.NoSuchAlgorithmException e5){
}
md.update(input_bytes,
0
, input_bytes.length);
byte
[] hash_bytes = md.digest();
for
(
int
i =
0
; i < hash_bytes.length; i++) {
Object[] objArray =
new
Object[]{Byte.valueOf(hash_bytes[i])};
StringBuilder sb=
new
StringBuilder(
""
);
output = sb.append(output).append(String.format(
"%02x"
, objArray)).toString();
}
return
new
Base64().encodeToString(output.getBytes());
}
public
static
String
doBoth
(String input)
{
return
Utilities.translate(Utilities.customEncodeValue(input));
}
public
static
String
translate
(String input)
{
input = input.replace(
'='
,
'?'
);
char
[] inputchars = input.toCharArray();
Map table =
new
HashMap();
int
[] ointArray = {
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
0
};
char
[] uocharArray = {
'W'
,
'h'
,
'a'
,
't'
,
'i'
,
's'
,
'd'
,
'o'
,
'n'
,
'e'
};
for
(
int
i =
0
; i <
10
; i++) {
table.put(Integer.valueOf(ointArray[i]), Character.valueOf(uocharArray[i]));
}
int
i =
0
;
while
(i < inputchars.length) {
int
charcode = inputchars[i];
if
(charcode >
'/'
&& charcode <
':'
) {
charcode = charcode -
48
;
inputchars[i] = ((Character)table.get(charcode)).charValue();
}
i++;
}
return
new
String(inputchars);
}
}
在解决工具类的问题之后,我们需要找到这几个依托Android R类(把它当成Android一种取资源的方法就行,如果读者开发过类似Android和Unity的工程就会明白)获取的参数。
我们以IsThisTheRealOne为例(做一道题三遍已经很烦了,不要让我做一个思路三遍):
a为R.string.(0x7f...)+一个固定字符串。0x7f...是这个string ID对应的十六进制ID。要讲这些ID怎么映射到代码里的str再映射到实际的Object比较困难,但是想找它们很容易:全局搜索就行。
搜到的对应名为str3,然后我们折回resources.asrc,找到values/strings.xml(所有的R.strings都存在这个文件里):
str3为TRytfrgooq|F{i-JovFBungFk,则a为TRytfrgooq|F{i-JovFBungFk\VlphgQbwvj~HuDgaeTzuSt.@Lex^~。
b为Utilities.doBoth(SendAnIntentApplication)
c这里有点说道,因为是匿名类,前面this.getClass().getName()应该是com.example.application.IsThisTheRealOne$1,后面去掉两位是com.example.application.IsThisTheRealOne,则c为Utilities.doBoth(com.example.application.IsThisTheRealOne)。
然后打开IDA直接看这个native函数的偏移量,为0x07ac:
unidbg梭哈一把,代码如下:
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName(
"com.augusttheodor.ctf"
)
.addBackendFactory(
new
Unicorn2Factory(
true
))
.build();
// 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory();
// 模拟器的内存操作接口
memory.setLibraryResolver(
new
AndroidResolver(
23
));
// 设置系统类库解析
VM vm = emulator.createDalvikVM();
// 创建Android虚拟机
new
AndroidModule(emulator,vm).register(memory);
DalvikModule dm = vm.loadLibrary(
new
File(
"\lib\arm64-v8a\libhello-jni.so"
),
false
);
dm.callJNI_OnLoad(emulator);
// 手动执行JNI_OnLoad函数
Module module = dm.getModule();
DvmClass dvc = vm.resolveClass(
"com/example/application/ThisIsTheRealOne"
);
//System.out.print(Utilities.customEncodeValue("123"));
List<Object> argss =
new
ArrayList<>(
10
);
argss.
add
(vm.getJNIEnv());
argss.
add
(vm.addLocalObject(
new
StringObject(vm,
""
)));
argss.
add
(vm.addLocalObject(
new
StringObject(vm,
"TRytfrgooq|F{i-JovFBungFk\VlphgQbwvj~HuDgaeTzuSt.@Lex^~"
)));
argss.
add
(vm.addLocalObject(
new
StringObject(vm, Utilities.doBoth(
"SendAnIntentApplication"
))));
argss.
add
(vm.addLocalObject(
new
StringObject(vm, Utilities.doBoth(
"com.example.application.IsThisTheRealOne"
))));
Number result=module.callFunction(emulator,
0x07ac
,argss.toArray());
String resText=vm.getObject(result.intValue()).getValue().toString();
System.
out
.println(
"result is "
+resText);
运行,成功得到flag:
方法三 修改APP内容直接显示flag
这个其实最简单,不过看上去没什么人会用,也许是因为这个方法要求做题人要对Android有一定了解。俗话说,治不了反汇编还治不了smali嘛,smali可比读反汇编舒服多了,真的。
使用apktool反编译APP:
java
-jar
.
apktool_2
.7
.0
.jar
d
2
bb0718ee4324207b2d2d1ffdd42f4e7
.apk
在修改Smali代码的过程中可以使用jeb打开APP在旁辅助,比如这样:
要达到我们的目标,需要做两步。我们可以先用Java来写出要修改的步骤,然后再改成smali。
首先我们需要吊起IsThisTheRealOne这个Activity,这一步不需要Java来做,可以直接修改AndroidManifest.xml达成。
其次,我们需要使得调用perhapsThis获得的返回显示出来。Android环境不同于Java,System.Out.println是无效的。但我们可以通过调用其他组件的setText方法来展示输出。不过目前所在的onClick处于匿名类中,调用外部组件不太方便(实际上,我也不太了解怎么在smali中使用this),所以我会使用Toast发送一个弹框。
思路完整之后就直接开始修改代码。
先修改AndroidManifest.xml,移除MainActivity的intent-filter然后加到IsThisTheRealOne下:
<
manifest
xmlns:android
=
"http://schemas.android.com/apk/res/android"
package
=
"com.example.hellojni"
platformBuildVersionCode
=
"22"
platformBuildVersionName
=
"5.1.1-1819727"
>
<
permission
android:description
=
"@string/android.permission._msg"
android:name
=
"ctf.permission._MSG"
android:protectionLevel
=
"signature"
/>
<
permission
android:description
=
"@string/android.permission._msg"
android:name
=
"ctf.permission._SEND"
/>
<
application
android:icon
=
"@mipmap/ic_launcher"
android:label
=
"CTF Application"
>
<
activity
android:label
=
"Main Activity"
android:name
=
"com.example.application.MainActivity"
>
</
activity
>
<
activity
android:label
=
"Activity: Is This The Real One"
android:name
=
"com.example.application.IsThisTheRealOne"
>
<
intent-filter
>
<
action
android:name
=
"android.intent.action.MAIN"
/>
<
category
android:name
=
"android.intent.category.LAUNCHER"
/>
</
intent-filter
>
</
activity
>
<
activity
android:label
=
"This Is The Real One"
android:name
=
"com.example.application.ThisIsTheRealOne"
/>
<
activity
android:label
=
"Definitely Not This One"
android:name
=
"com.example.application.DefinitelyNotThisOne"
/>
<
receiver
android:exported
=
"true"
android:name
=
"com.example.application.Send_to_Activity"
/>
</
application
>
</
manifest
>
然后修改IsThisTheRealOne的smali代码,这是我加上的部分,加在perhapsThis返回之后:
invoke-
virtual
{p1}, Landroid/view/View;->getContext()Landroid/content/Context;
move-result-
object
v9
const
/
4
v3,
0x1
invoke-
static
{p1, v6, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-
object
v3
invoke-
virtual
{v3}, Landroid/widget/Toast;->show()V
然后移除原本的加入Intent->发送broadcast步骤,也就是我这里红框的部分(因为我上面的代码使用了寄存器v3,删除是防止下面因为v3报错,如果希望留着就自己改一改寄存器):
修改后使用apktool重新打包:
java
-jar
.
apktool_2
.7
.0
.jar
b
.2
bb0718ee4324207b2d2d1ffdd42f4e7
使用NP管理器签名重新安装,进入后点击按钮即返回flag:
. . . * . * 🌟 * . * . . .
原文始发于微信公众号(重生之成为赛博女保安):一题三解花式拷打Android CTF:攻防世界#ill-intentions
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论