APP加固dex解密流程分析

news2024/11/16 13:25:49

前言

在去年9月的时候,我就想研究一下apk的加固,在网上逛了一圈,感觉某加固不错,所以先选它啦,写了一个测试apk丢到某加固里面加固了一下

这里我用的是某加固的免费版(因为付费版太贵啦)

本来计划着去年分析完某加固的,但是总是抽不出一段完整的时间来所以就拖到了现在,终于最近因为过年赋闲在家,就花了几天分析了一下某加固,感觉这几天探索某加固的过程真是充满了惊喜和乐趣呢~

java层初步分析

包名:com.oacia.apk_protect

入口:com.stub.StubApp

我们从AndroidManifest.xml中可以得知,某加固的入口是com.stub.StubApp,所以我们就先进到apk的入口进行分析

image-20230930183634640

在这个入口类中,不仅有常规的onCreate()函数,还有一个函数值得注意,他就是attachBaseContext(Context context)

image-20230930183713453

Application 的onCreateattachBaseContext是 Application 的两个回调方法,通常我们会在其中做一些初始化操作,attachBaseContext 在onCreate之前执行

其中出现的字符串经过了加密混淆操作,加密函数如下,算法是将所有的字符与16进行异或

image-20230930183731411

我们写个jeb脚本把加密字符串解密来方便后续的静态分析

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

# coding=utf-8

from com.pnfsoftware.jeb.client.api import IScript, IconType, ButtonGroupType

from com.pnfsoftware.jeb.core import RuntimeProjectUtil

from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit

from com.pnfsoftware.jeb.core.units.code import ICodeUnit, ICodeItem

from com.pnfsoftware.jeb.core.output.text import ITextDocument

from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit, IJavaStaticField, IJavaNewArray, IJavaConstant, IJavaCall, IJavaField, IJavaMethod, IJavaClass

from com.pnfsoftware.jeb.core.events import JebEvent, J

from com.pnfsoftware.jeb.core.util import DecompilerHelper

# 解密字符串函数的类名以及方法名

methodName = ['Lcom/qihoo/util/a;''a']

class dec_str_某加固jiagu(IScript):

    def run(self, ctx):

        print('start deal with strings')

        self.ctx = ctx

        engctx = ctx.getEnginesContext()

        if not engctx:

            print('Back-end engines not initialized')

            return

        projects = engctx.getProjects()

        if not projects:

            print('There is no opened project')

            return

        units = RuntimeProjectUtil.findUnitsByType(projects[0], IJavaSourceUnit, False)

        for unit in units:

            javaClass = unit.getClassElement()

            print('[+] decrypt:' + javaClass.getName())

            self.cstbuilder = unit.getFactories().getConstantFactory()

            self.processClass(javaClass)

            unit.notifyListeners(JebEvent(J.UnitChange))

        print('Done.')

    def processClass(self, javaClass):

        if javaClass.getName() == methodName[0]:

            return

        for method in javaClass.getMethods():

            block = method.getBody()

            = 0

            while i < block.size():

                stm = block.get(i)

                self.checkElement(block, stm)

                += 1

    def checkElement(self, parent, e):

        try:

            if isinstance(e, IJavaCall):

                mmethod = e.getMethod()

                mname = mmethod.getName()

                msig = mmethod.getSignature()

                if mname == methodName[1and methodName[0in msig:

                    = []

                    for arg in e.getArguments():

                        if isinstance(arg, IJavaConstant):

                            v.append(arg.getString())

                    if len(v) == 1:

                        decstr = self.decryptstring(v[0])

                        parent.replaceSubElement(e, self.cstbuilder.createString(decstr))

            for subelt in e.getSubElements():

                if isinstance(subelt, IJavaClass) or isinstance(subelt, IJavaField) or isinstance(subelt, IJavaMethod):

                    continue

                self.checkElement(e, subelt)

        except:

            print('error')

    def decryptstring(self, string):

        src = []

        for index, char in enumerate(string):

            src.append(chr(ord(char) ^ 16))

        return ''.join(src).decode('unicode_escape')

解密后的效果如下

image-20230930182440462

我们往下进行分析,可以知道attachBaseContext的第一个作用是根据目标手机的架构加载libjiagu_xxx.so如图

image-20230930184207278

这些so在assets目录下

在加载完libjiagu_xxx.so之后,还调用了DtcLoader类进行初始化,这里使用的jadx的反编译结果,因为jeb没有反编译出DtcLoader.init();方法来

image-20230930184605448

DtcLoader类如图所示

image-20230930184748578

DtcLoader类被加载到JVM中时,会去加载libjgdtc.so,如果加载失败,则会尝试从/data/app/com.oacia.apk_protect/lib/arm64/libjgdtc.so或者/data/data/com.oacia.apk_protect/lib/libjgdtc.so中去加载这个so

但是当我们去进入到这两个目录进行寻找时,却发现没有这个libjgdtc.so存在

image-20230930190258638

所以我们的分析重点是在libjiagu.so中,这里我选取了arm64架构的so文件libjiagu_a64.so进行分析

壳ELF导入导出表修复

我们使用ida分析libjiagu_a64.so,发现导入表和导出表都没有内容,既然是这种情况,那么应该是在so装载进内存时导入导出表才会去进行相应的链接操作

image-20231118145804113

所以我们可以先用frida把这个so给dump下来

首先我们在手机上运行一下frida server

1

2

3

4

PS D:\frida> adb shell

blueline:/ $ su

blueline:/ # cd /data/local/tmp

blueline:/data/local/tmp # ./fs -l 0.0.0.0:1234

随后做一下端口转发

1

adb forward tcp:1234 tcp:1234

frida命令行语句如下

1

frida -H 127.0.0.1:1234 -l .\hook.js -f "com.oacia.apk_protect"

注入如下脚本

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

function my_hook_dlopen(soName = '') {

    Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),

        {

            onEnter: function (args) {

                var pathptr = args[0];

                if (pathptr !== undefined && pathptr != null) {

                    var path = ptr(pathptr).readCString();

                    if (path.indexOf(soName) >= 0) {

                        this.is_can_hook = true;

                    }

                }

            },

            onLeave: function (retval) {

                if (this.is_can_hook) {

                    dump_so("libjiagu_64.so");

                }

            }

        }

    );

}

function dump_so(so_name) {

    var libso = Process.getModuleByName(so_name);

    console.log("[name]:", libso.name);

    console.log("[base]:", libso.base);

    console.log("[size]:", ptr(libso.size));

    console.log("[path]:", libso.path);

    var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";

    var file_handle = new File(file_path, "wb");

    if (file_handle && file_handle != null) {

        Memory.protect(ptr(libso.base), libso.size, 'rwx');

        var libso_buffer = ptr(libso.base).readByteArray(libso.size);

        file_handle.write(libso_buffer);

        file_handle.flush();

        file_handle.close();

        console.log("[dump]:", file_path);

    }

}

setImmediate(my_hook_dlopen("libjiagu_64.so"));

image-20231118150855726

随后我们使用SoFixer修复一下这个so,这里的-m参数即这个so在内存中的base基地址

1

.\SoFixer-Windows-64.exe -s .\libjiagu_64.so_0x74a2845000_0x274000.so -o .\libjiagu_64_0x74a2845000_0x274000_fix.so -m 0x74a2845000 -d

修复完成后,导入表和导出表就恢复了

image-20231118151321228

加固壳反调试初步分析

首先我们去hook一下打开so的函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

function hook_dlopen() {

    Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),

        {

            onEnter: function (args) {

                var pathptr = args[0];

                if (pathptr !== undefined && pathptr != null) {

                    var path = ptr(pathptr).readCString();

                    console.log("load " + path);

                }

            }

        }

    );

}

setImmediate(hook_dlopen)

日志如下,所以反调试是在libjiagu_64.so

1

2

3

load libstats_jni.so

load /data/app/~~P6meiEqXSQZrP2ChUgVgOg==/com.oacia.apk_protect-ezyVSLdtBZmLTZejgPlSoQ==/oat/arm64/base.odex

load /data/data/com.oacia.apk_protect/.jiagu/libjiagu_64.so

然后去hook打开文件的函数open

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

function my_hook_dlopen(soName = '') {

    Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),

        {

            onEnter: function (args) {

                var pathptr = args[0];

                if (pathptr !== undefined && pathptr != null) {

                    var path = ptr(pathptr).readCString();

                    if (path.indexOf(soName) >= 0) {

                        this.is_can_hook = true;

                    }

                }

            },

            onLeave: function (retval) {

                if (this.is_can_hook) {

                    hook_open();

                }

            }

        }

    );

}

function hook_open(){

    var pth = Module.findExportByName(null,"open");

  Interceptor.attach(ptr(pth),{

      onEnter:function(args){

          this.filename = args[0];

          console.log("",this.filename.readCString())

      },onLeave:function(retval){

      }

  })

}

setImmediate(my_hook_dlopen,"libjiagu");

日志如下

image-20231119122024522

这里我们发现了/proc/self/maps,这是常见的反调试,要绕过这个检测,我们可以备份一个正常的maps文件,然后用frida去hook open函数,如果匹配到字符串maps,就将字符串重定向到我们备份的maps文件

首先我们正常打开一次加壳的apk,然后使用下列命令备份maps

1

cp /proc/self/maps /data/data/com.oacia.apk_protect/maps

然后我们注入如下frida脚本

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

function my_hook_dlopen(soName = '') {

    Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),

        {

            onEnter: function (args) {

                var pathptr = args[0];

                if (pathptr !== undefined && pathptr != null) {

                    var path = ptr(pathptr).readCString();

                    if (path.indexOf(soName) >= 0) {

                        this.is_can_hook = true;

                    }

                }

            },

            onLeave: function (retval) {

                if (this.is_can_hook) {

                    hook_proc_self_maps();

                }

            }

        }

    );

}

function hook_proc_self_maps() {

    const openPtr = Module.getExportByName(null'open');

    const open = new NativeFunction(openPtr, 'int', ['pointer''int']);

    var fakePath = "/data/data/com.oacia.apk_protect/maps";

    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {

        var pathname = Memory.readUtf8String(pathnameptr);

        console.log("open",pathname);

        if (pathname.indexOf("maps") >= 0) {

            console.log("find",pathname,",redirect to",fakePath);

            var filename = Memory.allocUtf8String(fakePath);

            return open(filename, flag);

        }

        var fd = open(pathnameptr, flag);

        return fd;

    }, 'int', ['pointer''int']));

}

setImmediate(my_hook_dlopen,"libjiagu");

但是当注入这段脚本后,进程由于非法内存访问而退出了,这说明某加固不仅读取maps文件,并且会尝试访问maps文件中所记录的文件或内存映射.这里由于frida注入后重启apk,但是备份的maps文件中记录的是先前的映射起始地址(这块内存在关闭apk后就被抹去了),所以当壳尝试访问其中的映射时产生了非法内存访问从而让进程崩溃

image-20240212225108205

这里我的解决方式是将上述frida代码中的fakePath赋值为一个不存在的文件例如/data/data/com.oacia.apk_protect/maps_nonexistent,来让壳没有内容可以访问

修改完fakePath后重新注入代码,这个打印出来的日志可以说非常有意思,我们来看一下,相比hook open之前的日志,我们成功的让某加固的壳释放出了dex

image-20240213225015953

然而这个壳似乎是又发现了些什么异常,随后赶紧让app退出了,但是由于退出的太过仓促,它甚至还没有来得及把dex从文件夹中删除

image-20231117143717038

用010editor打开classes.dex,发现前几位并不是dex的魔术头,说明这个dex还没有被解密,不过现在我们只需要分析dex如何被壳从内存中释放出来的过程就可以了~

image-20231117143828767

如何可以定位到是什么位置调用了open函数来打开classes.dex呢?

很简单,打印一下堆栈就可以了

假如我们使用常规的frida打印堆栈代码,即使用DebugSymbol.fromAddress函数来判断地址所在的so的位置,那么进程是会报错退出的

1

console.log('RegisterNatives called from:\\n' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\\n') + '\\n');

所以这里DebugSymbol.fromAddress所实现的逻辑需要自己编写,即下方的addr_in_so函数

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

44

45

46

47

48

49

50

function addr_in_so(addr){

    var process_Obj_Module_Arr = Process.enumerateModules();

    for(var i = 0; i < process_Obj_Module_Arr.length; i++) {

        if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){

            console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));

        }

    }

}

function hook_proc_self_maps() {

    const openPtr = Module.getExportByName(null'open');

    const open = new NativeFunction(openPtr, 'int', ['pointer''int']);

    var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent";

    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {

        var pathname = Memory.readUtf8String(pathnameptr);

        console.log("open",pathname);//,Process.getCurrentThreadId()

        if (pathname.indexOf("maps") >= 0) {

            console.log("find",pathname+", redirect to",fakePath);

            var filename = Memory.allocUtf8String(fakePath);

            return open(filename, flag);

        }

        if (pathname.indexOf("dex") >= 0) {

            Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);

        }

        var fd = open(pathnameptr, flag);

        return fd;

    }, 'int', ['pointer''int']));

}

function my_hook_dlopen(soName='') {

    Interceptor.attach(Module.findExportByName(null"android_dlopen_ext"),

        {

            onEnter: function (args) {

                var pathptr = args[0];

                if (pathptr !== undefined && pathptr != null) {

                    var path = ptr(pathptr).readCString();

                    //console.log(path);

                    if (path.indexOf(soName) >= 0) {

                        this.is_can_hook = true;

                    }

                }

            },

            onLeave: function (retval) {

                if (this.is_can_hook) {

                    hook_proc_self_maps();

                }

            }

        }

    );

}

setImmediate(my_hook_dlopen,'libjiagu');

于是我们得到了释放三个dex文件的堆栈回溯

  • classes.dex

    image-20240213225949584

  • classes2.dex

    image-20240213230022534

  • classes3.dex

    image-20240213230048818

这里我们发现classes.dexclasses2.dex的堆栈回溯完全相同,并且classes3.dex的前半部分和前两个dex的堆栈一样,随后进程便又退出了

通过对堆栈的分析,我们可以发现三个dex应该是在一个循环中被依次加载的

接下来我们便跳转到堆栈所打印的偏移来进一步分析下

然而当我们跳转到堆栈回溯中的libjiagu_64.so的偏移0x19b780或者0x134598时,却发现这些地址的值都是0

image-20240213232138840

image-20240213232208410

我们很快就能想到这里用到的技术应该是先将一块内存标记为可写可执行,随后将字节码填充进去,所以说,我们只需要在壳打开dex时,将此时的libjiagu_64.so从内存中dump下来就可以了

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

function dump_so(so_name) {

    var libso = Process.getModuleByName(so_name);

    console.log("[name]:", libso.name);

    console.log("[base]:", libso.base);

    console.log("[size]:", ptr(libso.size));

    console.log("[path]:", libso.path);

    var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";

    var file_handle = new File(file_path, "wb");

    if (file_handle && file_handle != null) {

        Memory.protect(ptr(libso.base), libso.size, 'rwx');

        var libso_buffer = ptr(libso.base).readByteArray(libso.size);

        file_handle.write(libso_buffer);

        file_handle.flush();

        file_handle.close();

        console.log("[dump]:", file_path);

    }

}

var dump_once = false;//因为会打开三次dex,所以这里我们仅dump打开第一次dex时的libjiagu_64.so

function hook_proc_self_maps() {

    const openPtr = Module.getExportByName(null'open');

    const open = new NativeFunction(openPtr, 'int', ['pointer''int']);

    var fakePath = "/data/data/com.oacia.apk_protect/maps_nonexistent";

    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {

        var pathname = Memory.readUtf8String(pathnameptr);

        console.log("open",pathname);//,Process.getCurrentThreadId()

        if (pathname.indexOf("maps") >= 0) {

            console.log("find",pathname+", redirect to",fakePath);

            var filename = Memory.allocUtf8String(fakePath);

            return open(filename, flag);

        }

        if (pathname.indexOf("dex") >= 0) {

            if(!dump_once){

                dump_once = true;

                dump_so("libjiagu_64.so");

            }

        }

        var fd = open(pathnameptr, flag);

        return fd;

    }, 'int', ['pointer''int']));

}

然后再去用SoFixer修复这个dump下来的so

1

.\SoFixer-Windows-64.exe -s .\libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex.so -o .\libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex_fix.so -m 0x7a69829000 -d

再次来到偏移0x19B780处,可以发现这块空内存已经被填充了数据

image-20240213233913165

接下来我们想知道的是究竟是从什么地方开始被填充了新的数据,所以我们可以用WinMerge来让填充和未填充数据的so进行比较看看,结果却有了惊人的发现,被填充的数据是从0xe7000开始的,它的开头竟然是ELF文件的魔数头!?这有意思了,那么就是一个so里面藏了另外一个so咯~

image-20240213234737987

我们写个python脚本,把这个ELF从0x0e7000开始后面的所有字节都复制到新的文件里面

1

2

3

4

with open('libjiagu_64.so_0x7a69829000_0x274000_open_classes.dex.so','rb') as f:

    s=f.read()

with open('libjiagu_0xe7000.so','wb') as f:

    f.write(s[0xe7000::])

但是当把这个elf提取出来之后拿010editor看却发现program header table被加密了

image-20240220220722659

这就导致ida根本就无法进行正常的分析

image-20240217212952888

image-20240217213007622

主ELF解密流程分析

壳elf加载主elf,并且program header还被加密了,感觉这种形式很像是 自实现linker加固so

对于这种加固方式,壳elf在代码中自己实现了解析ELF文件的函数,并将解析结果赋值到soinfo结构体中,随后调用dlopen进行手动加载

来到ida里面在导入表对dlopen进行交叉引用,我们看到dlopen有5个交叉引用

image-20240218174655222

看到第二个交叉引用,来到sub_3C94函数,这个for循环看起来像是在用符号表通过dlopen加载依赖项

image-20240218174801778

向上面翻翻代码,看到这个switch就知道找对地方了,这里应该就是自实现linker来加载so的

image-20240218175140788

因为这和AOSP源码(android-platform\bionic\linker\linker.cpp)中的预链接(soinfo::prelink_image)这部分的操作极为的相似

image-20240218175438786

那接下来就在ida中导入soinfo相关的符号就可以啦

在ida中依次点击View->Open subviews->Local Types,然后按下键盘上的Insert将下面的结构体添加到对话框中

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

//IMPORTANT

//ELF64启用该宏

#define __LP64__  1

//ELF32启用该宏

//#define __work_around_b_24465209__  1

/*

//https://android.googlesource.com/platform/bionic/+/master/linker/Android.bp

架构为32位 定义__work_around_b_24465209__宏

arch: {

        arm: {cflags: ["-D__work_around_b_24465209__"],},

        x86: {cflags: ["-D__work_around_b_24465209__"],},

    }

*/

//android-platform\bionic\libc\include\link.h

#if defined(__LP64__)

#define ElfW(type) Elf64_ ## type

#else

#define ElfW(type) Elf32_ ## type

#endif

//android-platform\bionic\linker\linker_common_types.h

// Android uses RELA for LP64.

#if defined(__LP64__)

#define USE_RELA 1

#endif

//android-platform\bionic\libc\kernel\uapi\asm-generic\int-ll64.h

//__signed__-->signed

typedef signed char __s8;

typedef unsigned char __u8;

typedef signed short __s16;

typedef unsigned short __u16;

typedef signed int __s32;

typedef unsigned int __u32;

typedef signed long long __s64;

typedef unsigned long long __u64;

//A12-src\msm-google\include\uapi\linux\elf.h

/* 32-bit ELF base types. */

typedef __u32   Elf32_Addr;

typedef __u16   Elf32_Half;

typedef __u32   Elf32_Off;

typedef __s32   Elf32_Sword;

typedef __u32   Elf32_Word;

/* 64-bit ELF base types. */

typedef __u64   Elf64_Addr;

typedef __u16   Elf64_Half;

typedef __s16   Elf64_SHalf;

typedef __u64   Elf64_Off;

typedef __s32   Elf64_Sword;

typedef __u32   Elf64_Word;

typedef __u64   Elf64_Xword;

typedef __s64   Elf64_Sxword;

typedef struct dynamic{

  Elf32_Sword d_tag;

  union{

    Elf32_Sword d_val;

    Elf32_Addr  d_ptr;

  } d_un;

} Elf32_Dyn;

typedef struct {

  Elf64_Sxword d_tag;       /* entry tag value */

  union {

    Elf64_Xword d_val;

    Elf64_Addr d_ptr;

  } d_un;

} Elf64_Dyn;

typedef struct elf32_rel {

  Elf32_Addr    r_offset;

  Elf32_Word    r_info;

} Elf32_Rel;

typedef struct elf64_rel {

  Elf64_Addr r_offset;  /* Location at which to apply the action */

  Elf64_Xword r_info;   /* index and type of relocation */

} Elf64_Rel;

typedef struct elf32_rela{

  Elf32_Addr    r_offset;

  Elf32_Word    r_info;

  Elf32_Sword   r_addend;

} Elf32_Rela;

typedef struct elf64_rela {

  Elf64_Addr r_offset;  /* Location at which to apply the action */

  Elf64_Xword r_info;   /* index and type of relocation */

  Elf64_Sxword r_addend;    /* Constant addend used to compute value */

} Elf64_Rela;

typedef struct elf32_sym{

  Elf32_Word    st_name;

  Elf32_Addr    st_value;

  Elf32_Word    st_size;

  unsigned char st_info;

  unsigned char st_other;

  Elf32_Half    st_shndx;

} Elf32_Sym;

typedef struct elf64_sym {

  Elf64_Word st_name;       /* Symbol name, index in string tbl */

  unsigned char st_info;    /* Type and binding attributes */

  unsigned char st_other;   /* No defined meaning, 0 */

  Elf64_Half st_shndx;      /* Associated section index */

  Elf64_Addr st_value;      /* Value of the symbol */

  Elf64_Xword st_size;      /* Associated symbol size */

} Elf64_Sym;

#define EI_NIDENT   16

typedef struct elf32_hdr{

  unsigned char e_ident[EI_NIDENT];

  Elf32_Half    e_type;

  Elf32_Half    e_machine;

  Elf32_Word    e_version;

  Elf32_Addr    e_entry;  /* Entry point */

  Elf32_Off e_phoff;

  Elf32_Off e_shoff;

  Elf32_Word    e_flags;

  Elf32_Half    e_ehsize;

  Elf32_Half    e_phentsize;

  Elf32_Half    e_phnum;

  Elf32_Half    e_shentsize;

  Elf32_Half    e_shnum;

  Elf32_Half    e_shstrndx;

} Elf32_Ehdr;

typedef struct elf64_hdr {

  unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */

  Elf64_Half e_type;

  Elf64_Half e_machine;

  Elf64_Word e_version;

  Elf64_Addr e_entry;       /* Entry point virtual address */

  Elf64_Off e_phoff;        /* Program header table file offset */

  Elf64_Off e_shoff;        /* Section header table file offset */

  Elf64_Word e_flags;

  Elf64_Half e_ehsize;

  Elf64_Half e_phentsize;

  Elf64_Half e_phnum;

  Elf64_Half e_shentsize;

  Elf64_Half e_shnum;

  Elf64_Half e_shstrndx;

} Elf64_Ehdr;

/* These constants define the permissions on sections in the program

   header, p_flags. */

#define PF_R        0x4

#define PF_W        0x2

#define PF_X        0x1

typedef struct elf32_phdr{

  Elf32_Word    p_type;

  Elf32_Off p_offset;

  Elf32_Addr    p_vaddr;

  Elf32_Addr    p_paddr;

  Elf32_Word    p_filesz;

  Elf32_Word    p_memsz;

  Elf32_Word    p_flags;

  Elf32_Word    p_align;

} Elf32_Phdr;

typedef struct elf64_phdr {

  Elf64_Word p_type;

  Elf64_Word p_flags;

  Elf64_Off p_offset;       /* Segment file offset */

  Elf64_Addr p_vaddr;       /* Segment virtual address */

  Elf64_Addr p_paddr;       /* Segment physical address */

  Elf64_Xword p_filesz;     /* Segment size in file */

  Elf64_Xword p_memsz;      /* Segment size in memory */

  Elf64_Xword p_align;      /* Segment alignment, file & memory */

} Elf64_Phdr;

typedef struct elf32_shdr {

  Elf32_Word    sh_name;

  Elf32_Word    sh_type;

  Elf32_Word    sh_flags;

  Elf32_Addr    sh_addr;

  Elf32_Off sh_offset;

  Elf32_Word    sh_size;

  Elf32_Word    sh_link;

  Elf32_Word    sh_info;

  Elf32_Word    sh_addralign;

  Elf32_Word    sh_entsize;

} Elf32_Shdr;

typedef struct elf64_shdr {

  Elf64_Word sh_name;       /* Section name, index in string tbl */

  Elf64_Word sh_type;       /* Type of section */

  Elf64_Xword sh_flags;     /* Miscellaneous section attributes */

  Elf64_Addr sh_addr;       /* Section virtual addr at execution */

  Elf64_Off sh_offset;      /* Section file offset */

  Elf64_Xword sh_size;      /* Size of section in bytes */

  Elf64_Word sh_link;       /* Index of another section */

  Elf64_Word sh_info;       /* Additional section information */

  Elf64_Xword sh_addralign; /* Section alignment */

  Elf64_Xword sh_entsize;   /* Entry size if section holds table */

} Elf64_Shdr;

//android-platform\bionic\linker\linker_soinfo.h

typedef void (*linker_dtor_function_t)();

typedef void (*linker_ctor_function_t)(intchar**, char**);

#if defined(__work_around_b_24465209__)

#define SOINFO_NAME_LEN 128

#endif

struct soinfo {

#if defined(__work_around_b_24465209__)

  char old_name_[SOINFO_NAME_LEN];

#endif

  const ElfW(Phdr)* phdr;

  size_t phnum;

#if defined(__work_around_b_24465209__)

  ElfW(Addr) unused0; // DO NOT USE, maintained for compatibility.

#endif

  ElfW(Addr) base;

  size_t size;

#if defined(__work_around_b_24465209__)

  uint32_t unused1;  // DO NOT USE, maintained for compatibility.

#endif

  ElfW(Dyn)* dynamic;

#if defined(__work_around_b_24465209__)

  uint32_t unused2; // DO NOT USE, maintained for compatibility

  uint32_t unused3; // DO NOT USE, maintained for compatibility

#endif

  soinfo* next;

  uint32_t flags_;

  const char* strtab_;

  ElfW(Sym)* symtab_;

  size_t nbucket_;

  size_t nchain_;

  uint32_t* bucket_;

  uint32_t* chain_;

#if !defined(__LP64__)

  ElfW(Addr)** unused4; // DO NOT USE, maintained for compatibility

#endif

#if defined(USE_RELA)

  ElfW(Rela)* plt_rela_;

  size_t plt_rela_count_;

  ElfW(Rela)* rela_;

  size_t rela_count_;

#else

  ElfW(Rel)* plt_rel_;

  size_t plt_rel_count_;

  ElfW(Rel)* rel_;

  size_t rel_count_;

#endif

  linker_ctor_function_t* preinit_array_;

  size_t preinit_array_count_;

  linker_ctor_function_t* init_array_;

  size_t init_array_count_;

  linker_dtor_function_t* fini_array_;

  size_t fini_array_count_;

  linker_ctor_function_t init_func_;

  linker_dtor_function_t fini_func_;

/*

#if defined(__arm__)

  // ARM EABI section used for stack unwinding.

  uint32_t* ARM_exidx;

  size_t ARM_exidx_count;

#endif

  size_t ref_count_;

//怎么找不到link_map这个类型的声明...

  link_map link_map_head;

  bool constructors_called;

  // When you read a virtual address from the ELF file, add this

  // value to get the corresponding address in the process' address space.

  ElfW(Addr) load_bias;

#if !defined(__LP64__)

  bool has_text_relocations;

#endif

  bool has_DT_SYMBOLIC;

*/

};

导入完成后按下Y键,将a1定义为soinfo*

image-20240218175803731

然后就可以看到这些符号了,但是看这些符号总感觉有些不太对劲,这里不应该出现a1[1]或者a1[2],所以我猜测这个soinfo有被魔改的痕迹

image-20240219144353027

虽然这个soinfo可能有被魔改了,我们还是从sub_3C94这个预链接相关函数入手好了,交叉引用发现sub_3C94是被sub_49F0调用

随后我们来到sub_49F0内调用sub_3C94函数的位置,向下看,进入sub_4918函数中

image-20240219181823744

sub_4918中调用了sub_5E6C,我们进入sub_5E6C

image-20240219181928308

这个函数中出现了0x38这个数字,0x38是这个循环的步长

image-20240219184405967

0x38这个数字有什么特殊的含义吗?当然有了!!

我们把刚刚提取出来的elf用010editor打开,看到elf_headerphentsize这个字段,这个字段的含义是一个Program header table的长度,它正正好好也是0x38

image-20240219184824589

所以说在sub_5E6C中变量v5的类型应该是Elf64_Phdr *,我们直接重定义类型

image-20240219185005955

既然知道了真正的program header table就是在这个位置的,那我们直接在这个地方把program header table整个给dump下来不就行了

所以我们直接去hooksub_5E6C的三个传入的值

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

function hook_5E6C(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x5E6C), {

        // fd, buff, len

        onEnter: function (args) {

            console.log(hexdump(args[0], {

              offset: 0,// 相对偏移

              length: 0x38*0x6+0x20,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(args[1])

            console.log(args[2])

            console.log(`base = ${module.base}`)

        },

        onLeave: function (ret) {

        }

    });

}

image-20240219185724473

上面的第一个hexdump就是program header table,我们可以用cyberchefhexdump转成数组的形式

image-20240219190012572

0x6则对应着phnum,这表示共有6个program header table

0x793ca38000表示这个主ELF的基址,因为这个主ELF的位置在壳ELF基址的偏移0xe7000处,而最下面这行也已经打印出了壳ELF的基址为0x793c951000,0x793ca38000==0x793c951000+0xe7000等式成立

至此为止,我们拿到了解密之后的program header table,同时我们也知道了sub_5E6C传入的三个参数分别是phdr,phnum以及base

但是phdr成员命名是在soinfo偏移的0x0的位置

image-20240219191502430

那假如a1的类型就是soinfo*,为什么在sub_4918里面调用sub_5E6C传入的是偏移是232呢?

image-20240219191628154

所以soinfo*必定有被魔改,同时我们也可以在soinfo前填充一个大小为232的char类型数组看看是什么情况

image-20240219191808113

很好,这验证了我们对于soinfo*被魔改的猜测,因为在一切正常的情况之下,函数的调用应该是sub_5E6C(a1->phdr, a1->phnum, a1->base)才对

image-20240219191839316

但是我很想知道这个壳ELF究竟是如何被解密出来的,那么首先来看看主ELF的函数调用链是什么样子的吧~

我写了一个ida插件来实现这个过程stalker_trace_so

在IDA中使用Edit->Plugins->stalker_trace_so后,在so所在的目录下会生成一个js脚本,我们用frida注入到apk中即可,需要注意的是so_name需要改成libjiagu_64.so

image-20231118152818874

打印出来的完整日志如下

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

call1:JNI_OnLoad

call2:j_interpreter_wrap_int64_t

call3:interpreter_wrap_int64_t 

call4:getenv

call5:sub_13908

call6:inotify_add_watch

call7:sub_11220

call8:fopen

call9:sub_9DD8

call10:sub_E3E0

call11:strtol

call12:feof

call13:raise

call14:memset

call15:sub_C918

call16:sub_9988

call17:sub_9964

call18:sub_9AC4

call19:j_ffi_prep_cif

call20:ffi_prep_cif

call21:j_ffi_prep_cif_machdep

call22:ffi_prep_cif_machdep

call23:j_ffi_call

call24:ffi_call

call25:sub_1674C

call26:j_ffi_call_SYSV

call27:ffi_call_SYSV

call28:sub_167BC

call29:sub_1647C

call30:sub_163DC

call31:sub_9900

call32:sub_94BC

call33:inotify_init

call34:fmod

call35:strncpy

call36:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi

call37:sub_9E58

call38:sub_999C

call39:sub_10964

call40:j_lseek_1

call41:lseek

call42:sub_96E0

call43:sub_8000

call44:dlopen

call45:sub_60E0

call46:sub_6544

call47:sub_4B54

call48:sub_6128

call49:_ZN9__arm_c_19__arm_c_0Ev

call50:sub_A3EC

call51:sub_99CC

call52:sub_9944

call53:sub_6484

call54:sub_6590

call55:prctl

call56:sub_6698

call57:sub_9FFC

call58:j_lseek_3

call59:j_lseek_2

call60:j_lseek_0

call61:sub_9A90

call62:sub_5F20

call63:sub_6044

call64:sub_3574

call65:uncompress

call66:sub_49F0

call67:sub_5400

call68:sub_5478

call69:sub_5B08

call70:sub_5650

call71:sub_580C

call72:open

call73:atoi

call74:sub_3C94

call75:strncmp

call76:sub_4918

call77:sub_4000

call78:sub_41B4

call79:sub_35AC

call80:sigaction

call81:sub_5E6C

call82:sub_5444

call83:sub_633C

call84:sub_8130

call85:sub_4C70

call86:sub_825C

call87:sub_8B50

call88:sub_8ED4

call89:sub_8430

call90:interpreter_wrap_int64_t_bridge

call91:sub_9D60

call92:sub_166C4

call93:memcpy

call94:_Z9__arm_a_2PcmS_Rii

call95:j_ffi_prep_cif_var

call96:ffi_prep_cif_var

我们以sub_3C94为起点开始分析,因为这是我们通过dlopen交叉引用找到的自实现linker加固so的一个功能函数

sub_3C94不断按下X查看交叉引用,得到如下的调用关系sub_4B54->sub_49F0->sub_3C94

sub_4B54可能被sub_8000sub_8C74调用

image-20240219195550519

我们将stalker_trace_so打印出来的内容中,提取关键的部分拿过来看看,说明sub_3B54是被sub_8000调用的

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

call43:sub_8000 <--

call44:dlopen

call45:sub_60E0

call46:sub_6544

call47:sub_4B54 <--

call48:sub_6128

call49:_ZN9__arm_c_19__arm_c_0Ev

call50:sub_A3EC

call51:sub_99CC

call52:sub_9944

call53:sub_6484

call54:sub_6590

call55:prctl

call56:sub_6698

call57:sub_9FFC

call58:j_lseek_3

call59:j_lseek_2

call60:j_lseek_0

call61:sub_9A90

call62:sub_5F20

call63:sub_6044

call64:sub_3574

call65:uncompress

call66:sub_49F0 <--

call67:sub_5400

call68:sub_5478

call69:sub_5B08

call70:sub_5650

call71:sub_580C

call72:open

call73:atoi

call74:sub_3C94 <--

sub_8000的函数长这个样子,请记住第25行0xB8010这个数字,后面会派上用场的

image-20240219200830705

跟着函数调用链一处一处的在IDA中跳转到相应的地址进行查看,在call62:sub_5F20我们发现了有意思的代码

这个函数,一眼RC4呀

image-20240219200139731

用frida去hook一下这个函数看看RC4的密钥是什么

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

function hook_5f20_guess_rc4(){//像是RC4的样子,hook看看

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x5f20), {

        // fd, buff, len

        onEnter: function (args) {

            console.log(hexdump(args[0], {

              offset: 0,// 相对偏移

              length: 0x10,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(args[1])

            console.log(hexdump(args[2], {

              offset: 0,// 相对偏移

              length: 256,//dump 的大小

              header: true,

              ansi: true

            }));

        },

        onLeave: function (ret) {

        }

    });

}

image-20240219200302071

所以密钥就是这个咯

1

key = b"vUV4#\x91#SVt"

继续跟着函数调用链走,在call63:sub_6044我们发现了RC4的解密函数

image-20240219200510607

hook一下call63:sub_6044看看到底给什么数据解密了

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

var rc4_enc_text_addr,rc4_enc_size;

function hook_rc4_enc(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x6044), {

        // fd, buff, len

        onEnter: function (args) {

            rc4_enc_text_addr = args[0];

            rc4_enc_size = args[1];

            console.log(hexdump(args[0], {

              offset: 0,// 相对偏移

              length: 0x30,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(args[1])

        },

        onLeave: function (ret) {

            console.log(hexdump(rc4_enc_text_addr, {

              offset: 0,// 相对偏移

              length: 0x30,//dump 的大小

              header: true,

              ansi: true

            }));

        }

    });

}

image-20240219200703480

这个函数的第二个参数是0xb8010,感觉是解密的数据的长度的样子,而且这个数字,有没有感觉在哪里见过呢?

没错,这个数字刚刚就出现在sub_8000

image-20240219201000793

v5[0]的值是qword_2E270,这个数组也是01 18 25 e7开头的

image-20240219201131482

继续跟着调用链走,接下来是调用call65:uncompress,进行解压缩操作

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

function hook_uncompress_res(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(Module.findExportByName(null"uncompress"), {

        onEnter: function (args) {

            console.log("hook uncompress")

            console.log(hexdump(args[2], {

              offset: 0,// 相对偏移

              length: 0x30,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(args[3])

            dump_memory(args[2],args[3],`uncompress_${args[2]}_${args[3]}`)

        },

        onLeave: function (ret) {

        }

    });

}

我们发现解压缩的数据,前面四个字节b9 0e 1a 00没有包含在解压缩的字节之内

image-20240219202037098

现在既然我们已经知道了主ELF在壳ELF中的位置,以及解密的算法,那我们直接从解压apk,找到里面的assets/libjiagu_a64.so,不就能直接把壳ELF解密出来咯

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

import zlib

import struct

def RC4(data, key):

    = list(range(256))

    = 0

    out = []

    # KSA Phase

    for in range(256):

        = (j + S[i] + key[i % len(key)]) % 256

        S[i], S[j] = S[j], S[i]

    # PRGA Phase

    = = 0

    for ch in data:

        = (i + 1% 256

        = (j + S[i]) % 256

        S[i], S[j] = S[j], S[i]

        out.append(ch ^ S[(S[i] + S[j]) % 256])

    return out

def RC4decrypt(ciphertext, key):

    return RC4(ciphertext, key)

wrap_elf_start = 0x1e270

wrap_elf_size = 0xb8010

key = b"vUV4#\x91#SVt"

with open('com.oacia.apk_protect/assets/libjiagu_a64.so','rb') as f:

    wrap_elf = f.read()

# 对密文进行解密

dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start+wrap_elf_size], key)

dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))

with open('wrap_elf','wb') as f:

    f.write(dec_elf)

解密完成后,我们发现0x1a0eb9应该表示解压缩之后数据的大小

image-20240220214503336

image-20240220131405321

wrap_elf的前半部分是一大堆莫名其妙有很多D3的东西,但是看到中间还是发现了壳ELF的身影

image-20240219203139130

我们以.ELF为标志将这两部分分离一下

1

2

3

4

5

6

7

8

9

10

11

with open('wrap_elf''rb') as f:

    wrap_elf = f.read()

ELF_magic = bytes([0x7F0x450x4C0x46])

for in range(len(wrap_elf) - len(ELF_magic) + 1):

    if wrap_elf[i:i + len(ELF_magic)] == ELF_magic:

        print(hex(i))

        with open('wrap_elf_part1''wb') as f:

            f.write(wrap_elf[0:i])

        with open('wrap_elf_part2''wb') as f:

            f.write(wrap_elf[i::])

        break

跟着函数调用链来到call69:sub_5B08,这里又出现了0x38,并且word_38跳转过去的值为6

image-20240220000932490

这正好和phentsizephnum的值相对应

image-20240220001104708

所以可想而知,这又是一个关键点了,往下看一下代码,发现了循环异或,那我们不妨用frida把v4的值hook下来看看是什么

image-20240220001342895

v4的值出现了那么多的d3

image-20240220001528743

而这就是wrap_elf的前半部分那一大堆我们看不懂的字节

image-20240220012621743

接下来用来解密的循环就是一个arm64的neon运算

image-20240220003908902

官网可以找到vdupq_n_s8和veorq_s8,根据函数描述可以知道这里用向量运算,把向量中的每一个元素都异或了0xd3

image-20240220004233903

image-20240220004029770

sub_5B08进行分析之后,我们便可以知道wrap_elf_part1的读取方式是第一个字节表示被异或的数字,这里是0xD3,后面的四个字节表示一个段的长度,随后读取指定长度的字节并异或,之后再读取四个字节获取到下一个段的长度,以此类推,直到读取到文件末尾

image-20240220012939262

sub_5B08的最后,因为v31,v19,v43,v7代表对应的数据组的长度,所以这里共有四个数据组,而为了表示每一个数据组的长度共需占用4*4=16字节,并且文件开头还有1位的异或值,于是这些长度加起来,*(a1 + 0x98)的偏移就来到了主ELF的魔术头.ELF的位置了

image-20240220013039190

image-20240220013502056

我们可以在sub_5B08中为变量a1定义一个结构体,成员分别表示数据组的1,2,3,4这四个部分,这样我们就知道这四个部分分别被用到什么地方了

1

2

3

4

5

6

7

8

9

10

11

12

struct deal_extra

{

  char blank[72];

  int phnum;

  int *extra_part1;

  int phdr_size;

  char blank2[36];

  int *extra_part2;

  int *extra_part3;

  int *extra_part4;

  int *main_elf;

};

image-20240220023743626

接下来再捋一下函数的调用链sub_49F0->sub_5478(&v16, a1, v4)->sub_5B08(a1, a2, a3),在sub_5B08中,我们把a1的类型定义成了deal_extra,所以理所应当的,我们也把sub_49F0中的变量v16的类型定义为deal_extra

sub_49F0中我们发现成员extra_part赋值给了变量v7,所以我们也为v7建立一个结构体让v7的偏移可以对应这些变量

image-20240220024254105

1

2

3

4

5

6

7

8

9

10

11

12

struct deal_extra_B

{

  char blank[232];

  int *extra_part1;

  char blank1[8];

  int phnum;

  int *extra_part4;

  char blank2[24];

  int *extra_part2;

  char blank3[8];

  int *extra_part3;

};

image-20240220024130734

这样做有什么意义呢?

我们发现变量v7分别被传入到了sub_3C94sub_4918中,我们分别进去看看

image-20240220024433454

sub_3C94中解析了extra_part4,显而易见,这个switch是用来处理动态链接库的,即extra_part4对应.dynamic

image-20240220024522541

image-20240220024831366

sub_4918中,extra_part2extra_part3被传入到sub_4000

image-20240220024916186

而这个函数中的switch是用来处理重定位的,因为重定位主要有基址重定位和符号重定位,这两个的值分别是0x4030x402

所以extra_part2extra_part3分别对应着.rela.plt(402重定位)和.rela.dyn(403重定位)

image-20240220025025814

image-20240220145850194

而之后extra_part1被传入到了sub_5E6C

image-20240220025453629

而来到sub_5E6C也来到了我们最开始分析的起点(兜兜转转又回来了),所以extra_part1表示program header table

image-20240220025540021

至此为止,四个数据组所对应的段都分析完成

  • 数据组1表示program header table
  • 数据组2表示.rela.plt
  • 数据组3表示.rela.dyn
  • 数据组4表示.dynamic

所以接下来,写个脚本把这四个数据组给分离成单独的文件咯

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

import copy

import zlib

def RC4(data, key):

    = list(range(256))

    = 0

    out = []

    # KSA Phase

    for in range(256):

        = (j + S[i] + key[i % len(key)]) % 256

        S[i], S[j] = S[j], S[i]

    # PRGA Phase

    = = 0

    for ch in data:

        = (i + 1% 256

        = (j + S[i]) % 256

        S[i], S[j] = S[j], S[i]

        out.append(ch ^ S[(S[i] + S[j]) % 256])

    return out

def RC4decrypt(ciphertext, key):

    return RC4(ciphertext, key)

wrap_elf_start = 0x1e270

wrap_elf_size = 0xb8010

key = b"vUV4#\x91#SVt"

with open('com.oacia.apk_protect/assets/libjiagu_a64.so''rb') as f:

    wrap_elf = f.read()

# 对密文进行解密

dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start + wrap_elf_size], key)

dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))

with open('wrap_elf''wb') as f:

    f.write(dec_elf)

class part:

    def __init__(self):

        self.name = ""

        self.value = b''

        self.offset = 0

        self.size = 0

index = 1

extra_part = [part() for in range(7)]

seg = ["phdr"".rela.plt"".rela.dyn"".dynamic"]

v_xor = dec_elf[0]

for in range(4):

    size = int.from_bytes(dec_elf[index:index + 4], 'little')

    index += 4

    extra_part[i + 1].name = seg[i]

    extra_part[i + 1].value = bytes(map(lambda x: x ^ v_xor, dec_elf[index:index + size]))

    extra_part[i + 1].size = size

    index += size

for in extra_part:

    if p.value!=b'':

        filename = f"libjiagu.so_{hex(p.size)}_{p.name}"

        print(f"[{p.name}] get {filename}, size: {hex(p.size)}")

        with open(filename,'wb') as f:

            f.write(p.value)

于是我们得到了这四个文件

image-20240220220013192

image-20240220220022117

主ELF导入导出表修复

需要被修复的主ELF是我们在从assets/libjiagu_a64.so利用RC4decompress解密出来的文件的后半部分那个ELF

可以写个python脚本分离出后面的ELF

1

2

3

4

5

6

7

8

with open('wrap_elf''rb') as f:

    wrap_elf = f.read()

ELF_magic = bytes([0x7F0x450x4C0x46])

for in range(len(wrap_elf) - len(ELF_magic) + 1):

    if wrap_elf[i:i + len(ELF_magic)] == ELF_magic:

        with open('libjiagu_0xe7000.so''wb') as f:

            f.write(wrap_elf[i::])

        break

现在我们拿到了主ELF的四个重要的数据段,分别是phdr,.rela.plt,.rela.dyn,.dynamic,那么接下来需要做的工作就是修复主ELF的导入导出表了,不然导入导出函数都看不见怎么逆嘞~

在使用自实现linker加固so时,phdr,.rela.plt,.rela.dyn,.dynamic这四个段是从待加固的so中提取出来,然后加密存储到其他位置,原来的位置会使用无关字节直接覆盖

等到需要为加固的so进行预链接和重定位的工作时,才将这些段解密并通过自己实现的预链接和重定位代码,让待加固的so可以正确的被壳so加载出来

我们进行修复的方法其实就藏在这句话中原来的位置会使用无关字节直接覆盖,我们可以将分离出来的这四个段再塞回到原来的位置

自实现linker加固so的加固方案既然都把那四个段加密存到其他地方了,那怎么不直接把原来的四个段直接删除而是用无关字节覆盖呢?

因为直接把段删除掉的话,会影响了一整个ELF文件的布局,偏移就会变得和原先不一样,然后产生各种奇奇怪怪的问题

在010editor中,按下ctrl+shift+C可以复制整块内存,按下ctrl+shift+V可以粘贴整块内存

  1. 修复program header table

    复制libjiagu.so_0x150_phdr的所有字节,然后来到libjiagu_0xe7000.so中选中struct program_header_table粘贴

    image-20240220223148864

    随后按下F5刷新模板

  2. 修复.dynamic

    program header table(RW_) Dynamic Segmentp_offset指向.dynamic段的位置
     

    image-20240220223253986


    跳转到该位置,复制libjiagu.so_0x1b0_.dynamic的内容并粘贴到这个位置

  3. 修复重定位表
    我们需要通过.dynamic段的d_tag字段来找到重定位表的位置,下面是AOSP中d_tag的宏定义

    image-20240220172348458

所有的d_tag标志对应的含义可以在ORACLE 链接程序和库指南 中找到

对于我们修复主ELF比较重要的tag

d_tag含义
DT_JMPREL0x17.rela.plt在文件中的偏移
DT_PLTRELSZ0x2.rela.plt的大小
DT_RELA0x7.rela.dyn在文件中的偏移
DT_RELASZ0x8.rela.dyn的大小

我们可以在.dynamic中发现这些tag以及对应的值

image-20240221131839098

看看这两个大小分别是0x16500x25188,这不就和我们刚刚分离出来的文件大小一模一样嘛,说明我们离修复完成不远了

image-20240220224638674

然后就是和之前一样,跳转到.rela.plt.rela.dyn的对应地址,然后把这些段本来的数据粘贴进去

现在我们就修复好啦,拿ida打开主ELF看看,满满的都是符号!

image-20240221131944227

随便找个导入函数交叉引用看看,一切正常(●'◡'●)

image-20240221132044506

为了方便起见,我们可以将主ELF的基址定义成在其在壳ELF的偏移0xe7000方便后续的分析

主DEX解密流程初步分析

还记得在加固壳反调试初步分析中,我们拿到了未解密的dex嘛

image-20240220231727044

那么接下来有个问题就是,这个未解密的dex究竟藏在了apk的什么地方呢?

我将未加固的apk解压出来,然后用7zip压缩其中的dex,发现大小依然有2.8MB

image-20240220231901275

随后我将经过某加固之后的apk解压出来,按大小对文件进行排序之后发现,最大的文件就只有这个壳classes.dex,而别的文件甚至连1MB都没到,总不可能压缩率可以高到这种地步吧

image-20240220232029056

所以我们打开classes.dex看看,在这个classes.dex的末尾,果然藏着一大堆的数据

image-20240220232313676

而末尾的数据是由71 68 00 01和我们之前看到的加密的dex一模一样

接下来我们继续用stalker_trace_so去看看补充上主ELF的函数地址以及名称之后的函数调用链是什么样子的,首先在主ELF中运行插件Edit->Plugins->stalker_trace_so

之后同样的,我们需要将so_name改成libjiagu_64.so,特别注意的是,这里我们需要把壳ELF的func_addrfunc_name给复制过来,同时使用concat方法将主ELF和壳ELF的函数地址和函数名拼接成一个新的数组

image-20240221141755126

之前替换/proc/self/maps来实现初步反调试的js函数hook_proc_self_maps也需要同时执行

输出结果如下,KEkeELF标志表示壳ELF,mainELF表示主ELF,(为什么是KEke,只是为了对齐看着舒服:))

要判断调用的函数在哪个ELF里面,在trace_so()里面稍作修改判断一下范围可以了

image-20240221142103021

打印出来的结果如下

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

(KEkeELF)call1:JNI_OnLoad

(KEkeELF)call2:j_interpreter_wrap_int64_t

(KEkeELF)call3:interpreter_wrap_int64_t 

(KEkeELF)call4:getenv                   

(KEkeELF)call5:sub_13908                

(KEkeELF)call6:inotify_add_watch        

(KEkeELF)call7:sub_11220

(KEkeELF)call8:fopen

(KEkeELF)call9:sub_9DD8

(KEkeELF)call10:sub_E3E0

(KEkeELF)call11:strtol

(KEkeELF)call12:feof

(KEkeELF)call13:raise

(KEkeELF)call14:memset

(KEkeELF)call15:sub_C918

(KEkeELF)call16:sub_9988

(KEkeELF)call17:sub_9964

(KEkeELF)call18:sub_9AC4

(KEkeELF)call19:j_ffi_prep_cif

(KEkeELF)call20:ffi_prep_cif

(KEkeELF)call21:j_ffi_prep_cif_machdep

(KEkeELF)call22:ffi_prep_cif_machdep

(KEkeELF)call23:j_ffi_call

(KEkeELF)call24:ffi_call

(KEkeELF)call25:sub_1674C

(KEkeELF)call26:j_ffi_call_SYSV

(KEkeELF)call27:ffi_call_SYSV

(KEkeELF)call28:sub_167BC

(KEkeELF)call29:sub_1647C

(KEkeELF)call30:sub_163DC

(KEkeELF)call31:sub_9900

(KEkeELF)call32:sub_94BC

(KEkeELF)call33:inotify_init

(KEkeELF)call34:fmod

(KEkeELF)call35:strncpy

(KEkeELF)call36:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi

(KEkeELF)call37:sub_9E58

(KEkeELF)call38:sub_999C

(KEkeELF)call39:sub_10964

(KEkeELF)call40:j_lseek_1

(KEkeELF)call41:lseek

(KEkeELF)call42:sub_96E0

(KEkeELF)call43:sub_8000

(KEkeELF)call44:dlopen

(KEkeELF)call45:sub_60E0

(KEkeELF)call46:sub_6544

(KEkeELF)call47:sub_4B54

(KEkeELF)call48:sub_6128

(KEkeELF)call49:_ZN9__arm_c_19__arm_c_0Ev

(KEkeELF)call50:sub_A3EC

(KEkeELF)call51:sub_99CC

(KEkeELF)call52:sub_9944

(KEkeELF)call53:sub_6484

(KEkeELF)call54:sub_6590

(KEkeELF)call55:prctl

(KEkeELF)call56:sub_6698

(KEkeELF)call57:sub_9FFC

(KEkeELF)call58:j_lseek_3

(KEkeELF)call59:j_lseek_2

(KEkeELF)call60:j_lseek_0

(KEkeELF)call61:sub_9A90

(KEkeELF)call62:sub_5F20

(KEkeELF)call63:sub_6044

(KEkeELF)call64:sub_3574

(KEkeELF)call65:uncompress

(KEkeELF)call66:sub_49F0

(KEkeELF)call67:sub_5400

(KEkeELF)call68:sub_5478

(KEkeELF)call69:sub_5B08

(KEkeELF)call70:sub_5650

(KEkeELF)call71:sub_580C

(KEkeELF)call72:open

(KEkeELF)call73:atoi

(KEkeELF)call74:sub_3C94

(KEkeELF)call75:strncmp

(KEkeELF)call76:sub_4918

(KEkeELF)call77:sub_4000

(KEkeELF)call78:sub_41B4

(KEkeELF)call79:sub_35AC

(KEkeELF)call80:sigaction

(KEkeELF)call81:sub_5E6C

(KEkeELF)call82:sub_5444

(mainELF)call83:sub_11603C

(mainELF)call84:j__Znwm

(mainELF)call85:_Znwm

(mainELF)call86:malloc

(mainELF)call87:__cxa_atexit

(mainELF)call88:sub_1160B4

(mainELF)call89:sub_1160C4

(mainELF)call90:strlen

(mainELF)call91:memcpy

(mainELF)call92:sub_1161FC

(mainELF)call93:sub_1164AC

(mainELF)call94:sub_1164D8

(mainELF)call95:sub_116528

(mainELF)call96:sub_1165C8

(mainELF)call97:sub_1A32C0

(mainELF)call98:sub_1A3150

(mainELF)call99:sub_1A3204

(mainELF)call100:sub_1166FC

(mainELF)call101:sub_116728

(mainELF)call102:sub_116750

(mainELF)call103:sub_116830

(mainELF)call104:sub_116BA0

(KEkeELF)call105:sub_633C

(KEkeELF)call106:sub_8130

(KEkeELF)call107:sub_4C70

(KEkeELF)call108:sub_825C

(KEkeELF)call109:sub_8B50

(KEkeELF)call110:sub_8ED4

(KEkeELF)call111:sub_8430

(mainELF)call112:JNI_OnLoad

(mainELF)call113:j_interpreter_wrap_int64_t

(mainELF)call114:interpreter_wrap_int64_t

(KEkeELF)call115:interpreter_wrap_int64_t_bridge

(KEkeELF)call116:sub_9D60

(mainELF)call117:sub_1B3F0C

(mainELF)call118:gettimeofday

(mainELF)call119:sub_11BD9C

(mainELF)call120:sub_1182D8

(mainELF)call121:sub_123970

(mainELF)call122:sub_1B6448

(mainELF)call123:getenv

(mainELF)call124:sub_11F130

(mainELF)call125:sub_12047C

(mainELF)call126:j__ZdlPv

(mainELF)call127:_ZdlPv

(mainELF)call128:free

(mainELF)call129:sub_1427E8

(mainELF)call130:dlopen

(mainELF)call131:sub_11BDA8

(mainELF)call132:sub_11BE58

(mainELF)call133:sub_11F69C

(mainELF)call134:sub_117BE0

(mainELF)call135:sub_117CA0

(mainELF)call136:fopen

(mainELF)call137:sub_117E90

(mainELF)call138:sub_14285C

(mainELF)call139:sub_1429CC

(mainELF)call140:sub_11C1AC

(mainELF)call141:sub_11C1B4

(mainELF)call142:sub_11C210

(KEkeELF)call143:sub_166C4

(KEkeELF)call144:memcpy

(mainELF)call145:sub_123324

(mainELF)call146:sub_1205A0

(mainELF)call147:sub_11F768

(mainELF)call148:memcmp

(mainELF)call149:opendir

(mainELF)call150:closedir

(mainELF)call151:sub_11859C

(mainELF)call152:sub_11C268

(mainELF)call153:sub_11C300

(mainELF)call154:sub_117B68

(mainELF)call155:sub_1186B8

(mainELF)call156:sub_143964

(mainELF)call157:sub_1B66A8

(mainELF)call158:pthread_mutex_lock

(mainELF)call159:sub_142EA0

(mainELF)call160:sub_143A38

(mainELF)call161:sub_11CF8C

(mainELF)call162:sub_131D58

(mainELF)call163:sub_1B66D0

(mainELF)call164:pthread_mutex_unlock

(mainELF)call165:sub_1178E8

(mainELF)call166:sub_13D70C

(mainELF)call167:sub_19F984

(mainELF)call168:sub_11F1C8

(mainELF)call169:atoi

(mainELF)call170:sub_12D2F8

(mainELF)call171:sub_17ABE8

(mainELF)call172:sub_172660

(mainELF)call173:sub_13BFF0

(mainELF)call174:sub_172AA4

(mainELF)call175:sub_13BD80

(mainELF)call176:sub_13BE2C

(mainELF)call177:sub_13BE4C

(mainELF)call178:memmove

(mainELF)call179:sub_13BE64

(mainELF)call180:sub_172D78

(mainELF)call181:sub_13E510

(mainELF)call182:sub_1926F0

(mainELF)call183:sub_13DB7C

(mainELF)call184:sub_1B7A08

(mainELF)call185:sub_1B7ABC

(mainELF)call186:pthread_cond_broadcast

(mainELF)call187:sub_12FA34

(mainELF)call188:sub_120664

(mainELF)call189:sub_1332B8

(mainELF)call190:sub_13E0F8

(mainELF)call191:sub_12743C

(mainELF)call192:sub_124C68

(mainELF)call193:sub_125DC4

(mainELF)call194:sub_124510

(mainELF)call195:sub_126888

(mainELF)call196:strdup

(mainELF)call197:sub_126920

(mainELF)call198:sub_122180

(mainELF)call199:sub_11BC1C

(mainELF)call200:sub_13DF34

(mainELF)call201:getpid

(mainELF)call202:memset

(mainELF)call203:snprintf

(mainELF)call204:sub_124FA0

(mainELF)call205:sub_1B6498

(mainELF)call206:sub_1A0C88

(mainELF)call207:sub_217444

(mainELF)call208:sub_2175E0

(mainELF)call209:read

(mainELF)call210:strncmp

(mainELF)call211:close

(mainELF)call212:sub_1B578C

(mainELF)call213:j___self_lseek

(mainELF)call214:__self_lseek

(mainELF)call215:sub_1B586C

(mainELF)call216:j_j___read_self

(mainELF)call217:j___read_self

(mainELF)call218:__read_self

(mainELF)call219:sub_1B6528

(mainELF)call220:sub_1B6578

(mainELF)call221:mmap

(mainELF)call222:sub_1B5B50

(mainELF)call223:calloc

(mainELF)call224:memchr

(mainELF)call225:sub_1B5D04

(mainELF)call226:sub_1B5EC4

(mainELF)call227:sub_1B6270

(mainELF)call228:sub_1B6180

(mainELF)call229:sub_1B6678

(mainELF)call230:inflateInit2_

(mainELF)call231:inflate

(mainELF)call232:inflateEnd

(mainELF)call233:sub_1B6540

(mainELF)call234:munmap

(mainELF)call235:sub_1B56F8

(mainELF)call236:sub_19BC9C

(mainELF)call237:sub_19CCD4

(mainELF)call238:sub_12D470

(mainELF)call239:sub_142FE0

(mainELF)call240:sub_143008

(mainELF)call241:sub_142ABC

(mainELF)call242:sub_143848

(mainELF)call243:sub_143B48

(mainELF)call244:sub_143088

(mainELF)call245:sub_1222D0

(mainELF)call246:sub_14316C

(mainELF)call247:sub_142954

(KEkeELF)call248:_Z9__arm_a_2PcmS_Rii

(mainELF)call249:sub_142894

(mainELF)call250:sub_1428BC

(mainELF)call251:sub_127DCC

(mainELF)call252:sub_14292C

(mainELF)call253:sub_121B78

(mainELF)call254:sub_121BE0

(mainELF)call255:sub_123CE8

(mainELF)call256:sub_123BC0

(mainELF)call257:sub_11959C

(mainELF)call258:sub_1AC170

(mainELF)call259:pthread_create

(mainELF)call260:sub_1AC210

(mainELF)call261:sub_1B5DE4

(mainELF)call262:sub_1B60E8

(mainELF)call263:sub_19F7C4

(mainELF)call264:sub_1B2DC8

(mainELF)call265:sub_1B1CE8

(mainELF)call266:sub_1B0974

(mainELF)call267:sub_1AFE6C

(mainELF)call268:sub_126ED8

(mainELF)call269:sub_1AFE8C

(mainELF)call270:sub_1AFE90

(mainELF)call271:sub_1AB87C

(mainELF)call272:sub_1B26D4

(mainELF)call273:sub_1B26F4

(mainELF)call274:sub_1B27C8

(KEkeELF)call275:j_ffi_prep_cif_var

(KEkeELF)call276:ffi_prep_cif_var

(mainELF)call277:sub_1AAF48

(mainELF)call278:sub_1AAF54

(mainELF)call279:sub_2162D4

(mainELF)call280:sub_1B2898

(mainELF)call281:sub_1B2918

(mainELF)call282:sub_1ABE90

(mainELF)call283:sub_13E0EC

(mainELF)call284:sub_124900

(mainELF)call285:sub_1A0C34

(mainELF)call286:sub_217188

(mainELF)call287:j_strcmp

(mainELF)call288:strcmp

(mainELF)call289:sub_194514

(mainELF)call290:sub_1A2380

(mainELF)call291:sub_1A23CC

(mainELF)call292:sub_1A2718

(mainELF)call293:sub_1A2A94

(mainELF)call294:sub_1A25E0

(mainELF)call295:sub_1A2984

分析输出的结果,我们发现了三个有趣的函数inflateInit2_,inflate,inflateEnd,这不是zlib用来解压缩的函数嘛~

1

2

3

(mainELF)call230:inflateInit2_

(mainELF)call231:inflate

(mainELF)call232:inflateEnd

inflateInit2_交叉引用,发现有两个函数调用了它

image-20240221160528463

那么要怎么知道是哪一个函数先调用的inflateInit2_呢?向上看看函数调用链就行了

于是我们发现是sub_1B6270调用了inflateInit2_

1

2

3

4

5

6

(mainELF)call227:sub_1B6270 <--

(mainELF)call228:sub_1B6180

(mainELF)call229:sub_1B6678

(mainELF)call230:inflateInit2_

(mainELF)call231:inflate

(mainELF)call232:inflateEnd

我们来到sub_1B6270,先到GitHub - madler/zlib: A massively spiffy yet delicately unobtrusive compression library.把zlib.h中的z_stream_s,导入的方法和之前一样

1

2

3

4

5

6

7

8

9

10

11

12

13

#  define z_const const

typedef unsigned char  Byte;  /* 8 bits */

typedef unsigned int   uInt;  /* 16 bits or more */

typedef unsigned long  uLong; /* 32 bits or more */

typedef struct z_stream_s {

    z_const Bytef *next_in;     /* next input byte */

    uInt     avail_in;  /* number of bytes available at next_in */

    uLong    total_in;  /* total number of input bytes read so far */

    Bytef    *next_out; /* next output byte will go here */

    uInt     avail_out; /* remaining free space at next_out */

    uLong    total_out; /* total number of bytes output so far */

} z_stream;

重定义s的类型为z_stream,这四个字段的含义如下

  • s.next_in: 压缩数据
  • s.avail_in:压缩数据的长度
  • s.next_out: 解压后的数据
  • s.avail_out: 解压后数据的长度

image-20240221162607623

各个成员的偏移如图所示

image-20240221162856663

随后我们用frida去hook一下inflate函数看看解压缩之后的数据是什么

这里有个技巧,就是如何可以hook到主ELF中的函数,,因为在壳ELF加载进内存时,主ELF还没有被加载,所以假如在壳ELF通过android_dlopen_ext打开时我们进行hook,是会hook失败的

那么如何才能获取到主ELF的hook时机呢?我们可以通过统计外部函数的调用次数来判断是否已经加载了主ELF,例如我这里,我通过zlib_count统计外部函数inflate调用次数,因为在壳ELF会使用uncompress调用一次inflate,所以当第二次调用inflate,我们就知道这肯定是主ELF调用的,所以我们也可以在这个位置放心大胆的hook了

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

function dump_memory(start,size,filename) {

    var file_path = "/data/data/com.oacia.apk_protect/" + filename;

    var file_handle = new File(file_path, "wb");

    if (file_handle && file_handle != null) {

        var libso_buffer = start.readByteArray(size.toUInt32());

        file_handle.write(libso_buffer);

        file_handle.flush();

        file_handle.close();

        console.log("[dump]:", file_path);

    }

}

function hook_zlib_result(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x1B63F0), {

        // fd, buff, len

        onEnter: function (args) {

            console.log("inflate result")

            console.log(hexdump(next_in, {

              offset: 0,// 相对偏移

              length: 0x50,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(hexdump(next_out, {

              offset: 0,// 相对偏移

              length: 0x50,//dump 的大小

              header: true,

              ansi: true

            }));

            dump_memory(next_out,avail_out,"dex001")

        },

        onLeave: function (ret) {

        }

    });

}

var zlib_count=0;

var next_in,avail_in,next_out,avail_out;

function hook_zlib(){

    Interceptor.attach(Module.findExportByName(null"inflate"), {

        // fd, buff, len

        onEnter: function (args) {

            zlib_count+=1

            if(zlib_count>1){

                hook_zlib_result();

            }

            next_in=ptr(args[0].add(0x0).readS64());

            avail_in=ptr(args[0].add(0x8).readS64());

            next_out=ptr(args[0].add(0x18).readS64());

            avail_out=ptr(args[0].add(0x20).readS64());

            console.log(hexdump(next_in, {

              offset: 0,// 相对偏移

              length: 0x50,//dump 的大小

              header: true,

              ansi: true

            }));

            console.log(args[1]);

        },

        onLeave: function (ret) {

        }

    });

}

解压缩之后的输出如下,在输出的文件头,我们发现了dex035,所以我们把这块内存dump下来看看,使用上方的dump_memory(start,size,filename)函数即可

image-20240221170453897

把这个解压缩之后的dex拖入到jadx里面,却发现这个类名怎么和壳DEX的类名一模一样,通过校验哈希发现dump下来的dex和壳dex其实是同一个文件

image-20240221170627481

我们在之前的分析中知道壳dex的末尾附带了一大串的加密数据,所以通过将这个解压缩得到了这个dex,就说明马上要进行加密主DEX的解密操作了

解压缩的函数是sub_1B6270,接下来我们继续通过stalker_trace_so打印出来的内容,并利用交叉引用来追踪该函数的调用链

就比如说对于函数sub_1B6270,它有两个交叉引用

image-20240221172325310

通过stalker_trace_so打印出来的函数调用链,我们发现是sub_1A0C88sub_1B6270之前调用,所以函数的调用关系就是sub_1A0C88->sub_1B6270,以此类推

1

2

(mainELF)call206:sub_1A0C88

(mainELF)call227:sub_1B6270

所以一路跟过来之后,函数的调用链为sub_1332B8->sub_124FA0->sub_1A0C88->sub_1B6270->inflate,sub_1332B8函数之后就没有交叉引用了

1

2

3

4

5

6

7

(mainELF)call189:sub_1332B8

(mainELF)call204:sub_124FA0

(mainELF)call206:sub_1A0C88

(mainELF)call227:sub_1B6270

(mainELF)call230:inflateInit2_

(mainELF)call231:inflate

(mainELF)call232:inflateEnd

在这个函数中,我们发现了apk@classes.dex,而它的作用,正是为了找到已加载到内存且优化后的壳dex

image-20240221172717168


加固壳反调试初步分析的后半部分,我们打印出了加固壳打开dex的堆栈回溯,现在我们直接跳转到相对应的地方看看

image-20240220225331054

我们到0x19b780看看,看起来是一个标准的打开并写入文件的函数

image-20240221133619261

随后对该函数进行交叉引用,我们发现sub_1332B8竟然调用了它,就在刚刚我们就分析出这个函数中可是同时也执行了从内存中获取壳dex的操作的呢

image-20240221173657553

我们对这两处调用都hook一下看看是什么情况,打印的结果如下,说明这两处调用都打开了dex,sub_1332B8中的前一个调用打开了classes.dex,后一个调用打开了classes2.dexclasses3.dex,而classes.dex文件中的内容就是加密的主dex

image-20240221174419522

在创建完classes2.dexclasses3.dex,通过hook发现调用在调用sub_128D44之后进程就退出了

image-20240221180350649

我们去hook一下sub_128D44这个函数,发现传入的参数v8正是加密的主DEX

image-20240221184130805

sub_128D44函数是这个样子的,并且在壳ELF加载时启动stalker_trace_sotrace_so()函数所打印出的结果中,并没有这个函数的调用被打印出来

image-20240221180415665

这该怎么办呢?

很简单,在调用sub_128D44的位置再去调用一次trace_so()函数从现在的位置开始打印函数的调用链不就行咯:)

image-20240221180705025

函数调用关系如下,我们发现再mainELF调用完sub_128D44之后,通过一系列操作又回到了壳ELF中,最终调用raise导致进程退出

image-20240221185357390

然而,当我跳转到最后调用的几个函数时,可以说函数复杂到让人咋舌

image-20240222005607295

image-20240222005639325

这么复杂,是给人分析的吗!?所以我便卡在这里了很久

我想了想现在摆在面前的有两条路,是和一眼望不到尽头的这俩函数死磕到底,还是选择把某加固的反调试搞定?

我选择后者,因为明显搞定反调试要比把这两个函数分析明白要稍微简单一点

加固壳反调试深入分析

加固壳反调试初步分析中,我曾尝试过dbus,TracerPid,readlink,strstr都没有明显的效果,只有hook open函数让我看到了些许的曙光,那么现在应该还有一种非常重要的反调试手段没有用到,那就是pthread_create反调试

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

function check_pthread_create() {

    var pthread_create_addr = Module.findExportByName(null'pthread_create');

    var pthread_create = new NativeFunction(pthread_create_addr, "int", ["pointer""pointer""pointer""pointer"]);

    Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) {

        var so_name = Process.findModuleByAddress(parg2).name;

        var so_path = Process.findModuleByAddress(parg2).path;

        var so_base = Module.getBaseAddress(so_name);

        var offset = parg2 - so_base;

        var PC = 0;

        if ((so_name.indexOf("jiagu") > -1)) {

            console.log("======")

            console.log("find thread func offset", so_name, offset.toString(16));

            Thread.backtrace(this.context, Backtracer.ACCURATE).map(addr_in_so);

            var check_list = []//1769036,1771844

            if (check_list.indexOf(offset)!==-1) {

                console.log("check bypass")

            else {

                PC = pthread_create(parg0, parg1, parg2, parg3);

            }

        else {

            PC = pthread_create(parg0, parg1, parg2, parg3);

        }

        return PC;

    }, "int", ["pointer""pointer""pointer""pointer"]))

}

function addr_in_so(addr){

    var process_Obj_Module_Arr = Process.enumerateModules();

    for(var i = 0; i < process_Obj_Module_Arr.length; i++) {

        if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){

            console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));

        }

    }

}

注入代码之后,pthread_create的调用都指向了同一个地址0x17710

image-20240222010902622

我们跳转到这个地址之后却发现为什么会没有pthread_create呢??

image-20240222011044885

看了一眼这个代码所在的函数的名称ffi_call_SYSV

image-20240222011143427

hmmm,看来是用libffi动态调用函数呀

image-20240222011351754

直接到libffi的github仓库看一眼ffi_call_SYSV的源码

一进去注释都写得清清楚楚了

image-20240222012001816

利用注释就可以知道每行汇编都代表什么了,所以BLR X24表示去动态调用函数,而前面的X0,X2,X4,X6是用来传参的

image-20240222012543471

我们hook一下x0看看有没有什么敏感的字符串

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

function anti_frida_check(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x1770C), {

        onEnter: function (args) {

            try{

                console.log(this.context.x0.readCString())

            }

            catch (e){

            }

        },

        onLeave: function (ret) {

        }

    });

}

然而神奇的是,我仅仅去hook并打印x0字符串,其他什么事情都不干,apk竟然神奇的进去了,只不过会没有响应,感觉距离成功不远了呢

image-20240222013454838

有点意思,筛选一下看看有没有什么敏感的字符串好咯

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

function anti_frida_check(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x1770C), {

        onEnter: function (args) {

            try{

                var s = this.context.x0.readCString();

                if (s.indexOf('frida')!==-1 ||

                    s.indexOf('gum-js-loop')!==-1 ||

                    s.indexOf('gmain')!==-1 ||

                    s.indexOf('linjector')!==-1 ||

                    s.indexOf('/proc/')!==-1){

                    console.log(s)

                }

            }

            catch (e){

            }

        },

        onLeave: function (ret) {

        }

    });

}

竟然还真有,那就把这些字符串全部替换成无意义的字符串看看咯

image-20240222013944879

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

function anti_frida_check(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x1770C), {

        onEnter: function (args) {

            try{

                var s = this.context.x6.readCString();

                if (s.indexOf('frida')!==-1 ||

                    s.indexOf('gum-js-loop')!==-1 ||

                    s.indexOf('gmain')!==-1 ||

                    s.indexOf('linjector')!==-1 ||

                    s.indexOf('/proc/')!==-1){

                    console.log(s)

                    Memory.protect(this.context.x0, Process.pointerSize, 'rwx');

                    var replace_str=""

                    for(var i=0;i<s.length;i++){

                        replace_str+="0"

                    }

                    this.context.x0.writeUtf8String(replace_str);

                }

            }

            catch (e){

            }

        },

        onLeave: function (ret) {

        }

    });

}

然而这样做进程却一个劲的崩溃!!

没事,寄存器x0用不了,还有x2,x4,x6没替换过呢!我一个一个的试过去,终于,当我将寄存器改成x6时,进程终于不再崩溃成功的进入了apk!

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

function anti_frida_check(){

    var module = Process.findModuleByName("libjiagu_64.so");

    Interceptor.attach(module.base.add(0x1770C), {

        onEnter: function (args) {

            try{

                var s = this.context.x6.readCString();

                if (s.indexOf('frida')!==-1 ||

                    s.indexOf('gum-js-loop')!==-1 ||

                    s.indexOf('gmain')!==-1 ||

                    s.indexOf('linjector')!==-1 ||

                    s.indexOf('/proc/')!==-1){

                    //console.log(s)

                    Memory.protect(this.context.x0, Process.pointerSize, 'rwx');

                    var replace_str=""

                    for(var i=0;i<s.length;i++){

                        replace_str+="0"

                    }

                    this.context.x0.writeUtf8String(replace_str);

                }

            }

            catch (e){

            }

        },

        onLeave: function (ret) {

        }

    });

}

image-20240222015211639

看一眼检测的字符串,怎么全是/memfd:frida-agent-64.so

image-20240222015139528

主DEX加载流程分析

回到这个卡了我们很久位置,现在过了反调试之后这里的代码终于可以继续执行下去了

image-20240222015651090

向下看找到这一个函数

image-20240222030003433

在这个函数中的字符串全部都是加密的,颇有种此地无银三百两的感觉,我们把字符串解密后发现了DexFileLoader相关的字符串,说明这个函数肯定和加载dex有某种关联

image-20240222030104410

我们hook一下这个函数,发现这个函数共调用了三次,而且传入的值都是已经解密了的dex,classes.dex,classes2.dex,classes3.dex分别通过这个函数加载

classes.dex

image-20240222030239507

classes2.dex

image-20240222030345127

classes3.dex

image-20240222030355010

把这三个dex给dump下来看看,于是我们得到了这三个文件

image-20240222030924824

把最大的那个dex拖到jadx分析里面,发现这正是我们要找的主DEX

其他函数都很正常,唯独onCreate函数变成了native声明,要是有同样分析到这里的朋友可以去研究研究onCreate函数对应的native本地函数究竟在什么地方,相信有了本文的铺垫,对于进行后续onCreate函数的分析应该是有所帮助的吧~

而除此之外的别的类和直接反编译未加固的apk的类是一样的

image-20240222031222988

总结

某加固和常规的加固方案类似,都是壳DEX->壳ELF->主ELF->主DEX这样的过程,其中壳ELF解密主ELF所用到的算法是RC4uncompress,壳ELF加载主ELF所用到的技术是自实现linker加固so

以前我就听说某加固是有VMP的,但是我这一路下来都没遇见,我想VMP应该是在DEX的解密中所用到的,但是以我现在的水平或许还不足以对抗VMP,所以等未来学会了Static Analysis之后再回过头来研究这个样本中的VMP吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1502236.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux系统Docker部署DbGate并结合内网穿透实现公网管理本地数据库

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-66GkyG9g7oNq7tl8 {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

TCP包头

TCP包头: 1.序号:发送端发送数据包的编号 2.确认号:已经确认接收到的数据的编号(只有当ACK为1时,确认号才有用) TCP为什么安全可靠: 1.在通信前建立三次握手连接 SYN SYNACK ACK SYN是TCP包头的一个字段 tcp.port 端口号 抓包数据 2.在通信过程中通过序…

docker安装ES、LogStash、Kibana

文章目录 一、安装Elasticsearch1. 安装Elasticsearch2. 安装IK分词器3. elasticsearch-head 监控的插件4. 配置跨域 二、安装LogStash三、安装kibana四、SpringBoot集成LogStash&#xff0c;将日志输出到ES中五、 启动项目&#xff0c;监控项目运行 提示&#xff1a;以下是本篇…

C++:模版进阶 | Priority_queue的模拟实现

创作不易&#xff0c;感谢三连支持 一、非类型模版参数 模板参数分类为类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&…

python基础——输入与输出【input 和 print】

&#x1f4dd;前言&#xff1a; 上一篇文章python基础——入门必备知识中讲解了一些关于python的基础知识&#xff0c;可以让我们更好的理解程序代码中内容的含义&#xff0c;不至于一头雾水。今天我就来介绍一下&#xff0c;python中两个常见的输入和输出语句 input 和 print …

AI探索实践12 - Typescript开发AI应用4:大模型响应数据的格式化输出

大家好&#xff0c;我是feng&#xff0c;感谢你阅读我的博文&#xff0c;如果你也关注AI应用开发&#xff0c;欢迎关注公众号和我一起​探索。如果文章对你有所启发&#xff0c;请为我点赞&#xff01; 一、重点回顾 在介绍本文之前的文章中&#xff0c;我们先来回顾一下使用L…

Leetcode 剑指 Offer II 068.搜索插入位置

题目难度: 简单 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个排序的整数数组 nums 和一个整数目标值 target &#xf…

粘包与拆包

优质博文&#xff1a;IT-BLOG-CN 一、粘包出现的原因 服务端与客户端没有约定好要使用的数据结构。Socket Client实际是将数据包发送到一个缓存buffer中&#xff0c;通过buffer刷到数据链路层。因服务端接收数据包时&#xff0c;不能断定数据包1何时结束&#xff0c;就有可能出…

MySQL 篇-快速了解事务、索引

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 事务概述 1.1 事务四大特性(ACID) 2.0 索引概述 2.1 关于 “索引一定要创建在主键上&#xff1f;” 的问题 2.2 索引操作语法 2.3 索引结构 1.0 事务概述 事务是…

【ElasticSearch】es索引、映射、文档基本操作复杂查询

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的ElasticSearch专栏&#xff0c;本篇博客由B战尚硅谷的ElasticSearch视频总结而来&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f306; 内容速览 1 es数据格…

[Electron]中的BrowserView

Electron中BrowserView BrowserView 被用来让 BrowserWindow 嵌入更多的 web 内容。 它就像一个子窗口&#xff0c;除了它的位置是相对于父窗口。 这意味着可以替代webview标签. 示例 const { app, BrowserView, BrowserWindow } require(electron) ​ app.whenReady().the…

[服务器]RTSP服务与ffmpeg推送-简单搭建-Windows与Linux

文章目录 下载地址rtsp服务-mediamtx推流工具-ffmpegVLC播放器 Linux下载安装解压启动 Windows下载安装解压启动 VLC查看视频串流window查看本地摄像头 公司来了个临时需求&#xff0c;正好自己一直想搞一下&#xff0c;例如在VR Chat上放自己的视频[滑稽]&#xff0c;所以简单…

Matlab|2机5节点牛拉法(含报告)

目录 主要内容 下载链接 主要内容 采用牛拉法计算2机5节点的潮流计算程序&#xff0c;程序迭代稳定&#xff0c;运行可靠&#xff0c;含报告资料。 下载链接

.NET高级面试指南专题十五【 原型模式介绍,Clone要这样去用】

介绍&#xff1a; 原型模式是一种创建型设计模式&#xff0c;其主要目的是通过克隆现有对象来创建新对象&#xff0c;而不是通过实例化新的对象。这种模式在需要创建相似对象时非常有用&#xff0c;尤其是当对象的创建过程比较昂贵或复杂时。 实现原理&#xff1a; 原型模式通过…

探索Web中的颜色选择:不同取色方法的实现

在Web开发中&#xff0c;提供用户选择颜色的功能是很常见的需求。无论是为了个性化UI主题&#xff0c;还是为了图像编辑工具&#xff0c;一个直观且易用的取色器都是必不可少的。本文将介绍几种在Web应用中实现取色功能的方法&#xff0c;从简单的HTML输入到利用现代API的高级技…

Kafka MQ 主题和分区

Kafka MQ 主题和分区 Kafka 的消息通过 主题 进行分类。主题就好比数据库的表&#xff0c;或者文件系统里的文件夹。主题可以被分为若干个 分区 &#xff0c;一个分区就是一个提交日志。消息以追加的方式写入分区&#xff0c;然 后以先入先出的顺序读取。要注意&#xff0c;由…

OPC UA 学习:文件传输

本博文是OPC 10000-20: UA Part 20: File Transfer 的学习笔记。 OPC UA的客户端需要读写服务器端的文件&#xff0c;OPCUA 规范中&#xff0c;是通过文件模型实现的。客户端通过调用文件模型中的方法来处理文件。 在控制系统中&#xff0c;需要下载配置文件&#xff0c;工艺文…

掌握java中继承

目录 1.概念&#xff1a; 2.使用&#xff1a; 3.super关键字 4.子类构造方法 5.super和this关键字 6.初始化时代码块的执行顺序 7.继承的方式 8.final关键字 1.概念&#xff1a; 是面向对象程序设计代码可以重复使用的重要手段&#xff0c;允许程序员在保持原有类特性的…

java注释的详尽解析

一、什么是注解 (1).注解的作用 ①&#xff1a;注解一般用于对程序的说明&#xff0c;就像注释一样&#xff0c;但是区别是注释是给人看的&#xff0c;但是注解是给程序看的。 ②&#xff1a;让编译器进行编译检查的作用&#xff0c;比如下边这个Override注解是重写的意思&am…

漏洞复现-蓝凌LandrayOA系列

蓝凌OA系列 &#x1f52a; 是否利用过 优先级从高到低 发现日期从近到远 公司团队名_产品名_大版本号_特定小版本号_接口文件名_漏洞类型发现日期.载荷格式LandrayOA_Custom_SSRF_JNDI漏洞 LandrayOA_sysSearchMain_Rce漏洞 LandrayOA_Custom_FileRead漏洞