0x00 前言

起因是这款手机app曾有一段时间风席卷高校,也看到很多同学的“社死”现场

image-20210330190340454

0x01 apk分析

首先apktool解包,在assets文件夹下

image-20210326232139705

发现了mc.mp3,就是O泡的广告音频,还有一些.lua文件,打开之后发现是加密的,不了解lua,但是很有可能是就是lua写的,这种脚本语言很可能需要被加载

分析java部分,jeb打开一份礼物.apk,为了方便可以修改AndroidManifest.xml中的application标签,加一个android:debuggable="true",这样重新用apktool编译回去就可以动态调试了,

但是这样的apk并不能直接安装,需要对其重打包并进行签名。

1
2
3
4
#管理员
keytool -genkey -alias key.keystore -keyalg RSA -validity 30000 -keystore key.keystore

jarsigner -verbose -keystore key.keystore -signedjar 123.apk 123.apk key.keystore

签完名正常安装,改一下开发者选项

image-20210326233338463

界面变为如下效果:

image-20210326233445700

找到启动类,跟进到这里

image-20210329223248785

java水平实在有限,逻辑搞不清楚,多次下断点都没命中

但发现有加载lua的代码,

image-20210330163439453

尝试抓包,发现这个程序并没有任何的网络封包传输 (fiddler)

image-20210330160100632

在jeb下,发现了大量的混淆包,还有一些百度API、腾讯API没被调用,甚至还有网上可以搜到Lua源项目的包,使用Androlua的库,可见该app是魔改过的

img

后来网上查到了Lua的Android项目,是国人开发的一个 lua 写安卓应用的框架

Java部分可能并不是应用的主体部分,重要操作可能会写在Lua中,

上图相关代码实际上并没有被调用,只是打包apk时封装进去的类,关键逻辑位于main.lua中。

lua脚本需要加载,而在加载之前肯定是要先解密的,所以只要找到解密函数就可以了

libluajava.so文件会使用luaL_loadbuffer或者luaL_loadbufferx函数对Lua脚本进行加载,通常解密也在这个位置,果不其然IDA定位到这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __fastcall luaL_loadbufferx(int a1, int array, size_t size, int path, int a5)
{
int v5; // r10
size_t v6; // r6
int v7; // r11
int v8; // r8
_BYTE *v9; // r0
int v10; // r1
signed int v11; // r2
_BYTE *v13; // [sp+8h] [bp-28h]
size_t v14; // [sp+Ch] [bp-24h]

v5 = a1;
v6 = size;
v7 = array;
v8 = path;
v13 = array;
v14 = size;
if ( *array == 0x1B && *(array + 1) != 0x4C )
{
v9 = malloc(size);
if ( v6 )
{
*v9 = 27;
if ( v6 != 1 )
{
v10 = 0;
v11 = 1;
do
{
v10 += v6;
v9[v11] = *(v7 + v11) ^ (v10
+ ((((-2139062143LL * v10) >> 32) + v10) >> 7)
+ ((((-2139062143LL * v10) >> 32) + v10) < 0));
++v11;
}
while ( v6 != v11 );
}
}
v13 = v9;
}
return j_lua_load(v5, sub_E0B6, &v13, v8, a5);
}

image-20210330160703541

参数名是改过的,if语句判断是否需要解密,否则直接执行j_lua_load加载文件,

找到了原函数然后了解各个参数的意义进行逆向即可,(其实不算逆向而是正向逻辑再走一遍就行),

或者IDA动调在return上面下个断点,当程序执行到这后dump出内存数据也行

1
2
3
4
5
6
7
>LUALIB_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t size,
const char *name, const char *mode) {
LoadS ls;
ls.s = buff;
ls.size = size;
return lua_load(L, getS, &ls, name, mode);
}

逆向脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from ctypes import *
import sys
def decrypt(filename):
s = open(filename, 'rb').read()
outfile = 'out.lua'
if s[0] == chr(0x1b) and s[1] != chr(0x4c):
rst = chr(0x1b)
size = len(s)
v10 = 0
for i in range(1, size):
v10 += size
v = (c_ulonglong(-2139062143 * v10).value >> 32) + v10
v1 = c_uint(v).value >> 7
v2 = c_int(v).value < 0
rst += chr(ord(s[i]) ^ (v10 + v1 + v2) & 0xff)
with open(outfile, 'wb') as f:
f.write(rst)
else:
pass
def foo():
# print len(sys.argv)
if len(sys.argv) == 2:
filename = sys.argv[1]
else:
filename = 'main.lua'
decrypt(filename)
if __name__ == '__main__':
foo()

(其中ctypes之前用过,又忘了,在这记一下)

然后解密三个lua

image-20210327000258919

还是乱码,于是搜索LuaS,了解到与Python生成pyc字节码一样,Lua程序也有自己的字节码格式luac。Lua程序在加载到内存中后,Lua虚拟机环境会将其编译为Luac字节码

需要用工具反编译

1
java -jar .\unluac_2021_03_19b.jar .\out.lua > main.lua

于是得到源码main.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require("import")
import("android.app.*")
import("android.os.*")
import("android.widget.*")
import("android.view.*")
import("android.view.View")
import("android.content.Context")
import("android.media.MediaPlayer") //播放器
import("android.media.AudioManager") //音量控制
import("com.androlua.Ticker")
activity.getSystemService(Context.AUDIO_SERVICE).setStreamVolume(AudioManager.STREAM_MUSIC, 15, AudioManager.FLAG_SHOW_UI) //调大音量
activity.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE)//隐藏系统导航栏,并进入沉浸模式(全屏)
m = MediaPlayer()
m.reset()
m.setDataSource(activity.getLuaDir() .. "/mc.mp3") //广告
m.prepare()
m.start()
m.setLooping(true)//循环
ti = Ticker() //计时器触发
ti.Period = 10 //间隔10
function ti.onTick()
activity.getSystemService(Context.AUDIO_SERVICE).setStreamVolume(AudioManager.STREAM_MUSIC, 15, AudioManager.FLAG_SHOW_UI)//同上
activity.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE)//隐藏navbar
end
ti.start()//开启ticker,每10ms执行一次上面的函数
function onKeyDown(A0_0, A1_1) //监听按键
if string.find(tostring(A1_1), "KEYCODE_BACK") ~= nil then //如果按键是返回键
activity.getSystemService(Context.AUDIO_SERVICE).setStreamVolume(AudioManager.STREAM_MUSIC, 15, AudioManager.FLAG_SHOW_UI)
end //相当于把返回键变成了音量最大键
return true
end

大体流程如下:

将系统音量调至最大

隐藏系统导航栏,并进入沉浸模式(全屏)

每10tick,重复以上步骤使得无法主动调低音量

循环播放音频文件mc.mp3,并劫持返回键

(仅当两次返回的时间间隔小于0秒是才会退出软件,否则就会一直播放音乐)

如果锁定你的home键,然后再实现开机自启动,这样的话,这种软件就可以变成流氓勒索软件了

0x02 小结

这样看来似乎程序并没什么危害,只是个恶搞软件

不要随便安装来历不明的软件,apk安装包实在是太容易被改包了。

img

reference: https://cloud.tencent.com/developer/article/1718949