当前位置:   article > 正文

Android逆向:某鹰直播Lua脚本解密_luar解密

luar解密

声明:案例分析仅供学习交流使用,勿用于任何非法用途。如学习者进一步逆向并对版权方造成损失,请自行承担法律后果,本人概不负责。

简介

Lua是一种小巧的脚本语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数。几乎在所有操作系统和平台上都可以编译、运行。市面上很多流行的手游就是采用Lua编写关键代码。

Lua、Luac、LuaJIT

Lua、Luac、LuaJIT是平时较为常见的Lua文件类型,其中lua是明文代码,直接用记事本就能打开,luac是lua编译后的字节码,文件头为0x1B 0x4C 0x75 0x61 0x51,lua虚拟机能够直接解析lua和luac脚本文件,而luaJIT是另一个lua的实现版本(不是原作者写的),JIT是指Just-In-Time(即时解析运行),luaJIT相比lua和luac更加高效,文件头是0x1B 0x4C 0x4A。
Luac文件头:
在这里插入图片描述
LuaJIT文件头:
在这里插入图片描述
 

Lua防护措施

一般有安全意识的厂商都不会直接把Lua脚本源码apk落盘,一般防护措施有下面几种:

  • 对称加密,在加载脚本之前解密:这种情况是指apk中的lua代码是加密过的,程序在加载Lua脚本时解密(关键函数luaL_loadbuffer ),解密后就能够获取lua源码。如果解密后获取的是luac字节码的话,也可以通过反编译得到lua源码,反编译主要用的工具有unluac和luadec51。
  • 将lua脚本编译成luaJIT字节码并加密打包:因为反编译的结果并不容易查看,所以这种情况能够较好的保护Lua源码。这个情况主要是先解密后反编译,反编译主要是通过luajit-decomp项目,它能够将luajit字节码反编译成伪lua代码。
  • 修改lua虚拟机中opcode的顺序:这种情况主要是修改lua虚拟机源码,再通过修改过的虚拟机将lua脚本编译成luac字节码,达到保护的目的。这种情况如果直接用上面的反编译工具是不能将luac反编译的。
     

逆向Lua源码的一般方案

要逆向获取Lua源码一般采用以下方案:

  • 静(动)态分析so:这种方法需要把解密的过程全部分析出来,主要是通过ida定位到luaL_loadbuffer(相关)函数,分析出解密的过程。
  • dump:通过ida动态调试so文件,然后是定位到luaL_loadbuffer地址,在加载必要的Lua脚本的地方下断点 ,接着针对每个Lua文件使用idc脚本 + dump拿到源码。
  • hookdump方案需要一个个文件断点再通过脚本拿到源码,如果采用hook方案则只需要跑一遍流程,hook函数自动拿到全部源码。
  • 分析lua虚拟机的opcode的顺序:这种方案主要针对修改lua虚拟机中opcode的顺序的防护。需要ida定位到虚拟机执行luac字节码的地方,然后对比原来lua虚拟机的执行过程,获取修改后的opcode顺序,最后还原lua脚本。
     

案例

破解流程

本次演示的案例是一款使用了Lua脚本作为关键代码的直播软件。首先抓包可以看到其每次都是动态下载Lua文件zip包:
在这里插入图片描述
看来是通过服务器下发带Lua文件信息的json,再通过其中的url下载(最新)zip包,下载到本地分析:
在这里插入图片描述
打开lua,发现都是加密的:
在这里插入图片描述
要解密lua就要从读取lua的地方入手,打开其apk结构,发现lib包下明显有负责读取lua的so库。
在这里插入图片描述
ida静态分析,发现这几个load函数应该是用来读取lua文件的。
在这里插入图片描述
查看luaL_loadEncryptedfile函数:
在这里插入图片描述
伪代码如下,后面的注释是根据函数名猜测添加的。

signed int __fastcall luaL_loadEncryptedfile(int a1, const char *a2)
{
  const char *v2; // r7
  size_t v3; // r4
  int v4; // r5
  int *v5; // r0
  char *v6; // r0
  void *v7; // r6
  char *v8; // r5
  int v9; // r4
  int v11; // [sp+Ch] [bp-8Ch]
  void *ptr; // [sp+10h] [bp-88h]
  int v13; // [sp+14h] [bp-84h]
  char v14; // [sp+18h] [bp-80h]
  size_t v15; // [sp+48h] [bp-50h]

  v11 = a1;
  v2 = a2;
  if ( stat(a2, &v14) < 0 )
    return -1;
  v3 = v15;
  printf("filesize; %d\n", v15);
  v4 = open(v2, 0);
  if ( !v4 )
  {
    v5 = _errno();
    v6 = strerror(*v5);
    printf("open file error, %s\n", v6);
    exit(0);
  }
  v7 = malloc(v3);
  memset(v7, 0, v3);
  read(v4, v7, v3); // 读取加密文件
  close(v4);
  v13 = 0;
  v8 = tinydes_generateKey();
  tinydes_decoding(&ptr, &v13, v7, v3, v8); // 解密文件
  printf("decoded: \n%s\n", ptr);
  v9 = luaL_loadbuffer(v11, ptr, v13); // buffer流载入解密文件
  free(v8);
  free(v7);
  free(ptr);
  return v9;
}
  • 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

首先查看tinydes_generateKey函数,不难发现,其中的key_map是破解关键。

void *tinydes_generateKey()
{
  const char **v0; // r4
  _BYTE *v1; // r6
  signed int v2; // r5
  const char *v3; // r7
  signed int v4; // ST04_4
  int v5; // r1
  void *v7; // [sp+0h] [bp-20h]

  v7 = malloc(0x11u);
  memset(v7, 0, 0x11u);
  v0 = key_map;  // 解密算法中的key
  v1 = v7;
  v2 = 3;
  do
  {
    v3 = *v0;
    ++v0;
    v4 = strlen(v3);
    *v1 = v3[v4 % v2];
    v5 = v4 % (v2 + 1);
    v2 += 2;
    v1[1] = v3[v5];
    v1 += 2;
  }
  while ( v2 != 19 );
  return v7;
}
  • 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

貌似key_map初始化了一些字符串常量,不过放在.data段,也存在中途被修改的可能,还需要动态调试一下。
在这里插入图片描述
tinydes_decoding,猜测参数应该包含解密的文件内容指针、key、文件长度

int __fastcall tinydes_decoding(char *s, _DWORD *a2, int a3, size_t a4, char *sa)
{
  char *v5; // r4
  size_t v6; // r5
  _DWORD *v7; // r7
  signed int v8; // r6
  void *v9; // r0
  int i; // r4
  int v11; // r2
  int v13; // [sp+0h] [bp-20h]
  int v14; // [sp+4h] [bp-1Ch]

  v5 = s;
  v6 = a4;
  v7 = a2;
  v14 = a3;
  v8 = strlen(sa);
  v9 = malloc(v6);
  *v5 = v9;
  memset(v9, 0, v6);
  v13 = *v5;
  for ( i = 0; i != v6; ++i )
  {
    v11 = sa[(i + v8) % v8];
    *(v13 + i) = (((v11 + *(v14 + i)) & 0xFFu) >> 3) | 32 * ((v11 + *(v14 + i)) & 7);
  }
  *v7 = i;
  return 0;
}
  • 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

开始动态调试。
在这里插入图片描述
在这里插入图片描述
找到函数入口。
在这里插入图片描述
tinydes_generateKey开始结束分别下断。
在这里插入图片描述
码表居然没被二次赋值过,那上面的流程用java写出来就是:

	/**
	 * lua码表
	 */
	private final static String[] luaKeyArray = { 
			"6f8f57715090da2632453988d9a1501b",
			"d95679752134a2d9eb61dbd7b91c4bcc", 
			"4b43b0aee35624cd95b910189b3dc231", 
			"e1671797c52e15f763380b45e841ec32",
			"e358efa489f58062f10dd7316b65649e", 
			"9e3669d19b675bd57058fd4664205d2a", 
			"9d4d6204ee943564637f06093236b181",
			"d88468fb83a6d5675fcd2bdcb8fa57bf", };

	/**
	 * 生成lua密匙
	 * 
	 * @param luaKeyArray
	 *            lua码表
	 * @return
	 */
	private static String generateLuaKey(String[] luaKeyArray) {
		StringBuilder keyBuilder = new StringBuilder();
		int cout = 3;
		final int arrlength = luaKeyArray.length;
		final int strLength = luaKeyArray[0].length();
		for (int i = 0; i < arrlength; i++) {
			keyBuilder.append(luaKeyArray[i].charAt(strLength % cout))
					.append(luaKeyArray[i].charAt((strLength % (cout + 1))));
			cout += 2;
		}
		return keyBuilder.toString();
	}

	/**
	 * 解密lua文件中的数据
	 * 
	 * @param encFile
	 * @return
	 */
	private static String desDecLuaFile(File encFile) {
		StringBuilder decBuilder = new StringBuilder();
		if (encFile == null || !encFile.exists() || encFile.length() <= 0) {
			return "无文件数据";
		}
		String key = generateLuaKey(luaKeyArray);
		int keyLength = key.length();
		byte[] encDate = FileUtils.binaryFileRead(encFile.getAbsolutePath());
		int[] encArray = ByteUtils.byteArrayToIntArray(encDate);
		int coutKey = 0;
		int coutDate = 0;
		char decDate = 0;
		for (int i = 0; i < encDate.length; i++) {
			coutKey = key.charAt(ByteUtils.loopPoint(keyLength, i));
			coutDate = encArray[i];
			decDate = (char) ((((coutKey + coutDate) & 0xFF) >> 3) | 32 * (coutKey + coutDate & 7));
			decBuilder.append(decDate);
		}
		return decBuilder.toString();
	}
 
 //------上面用到的工具类-----//
 	/**
	 * 循环指针下标
	 * 
	 * @param o
	 * @param i
	 * @return
	 */
	public static int loopPoint(int arrayLen, int i) {
		return (i + arrayLen) % arrayLen;
	}
 
 	/**
	 * byte[]转int[]
	 * 
	 * @param byteArray
	 * @return
	 */
	public static int[] byteArrayToIntArray(byte[] byteArray) {
		int[] intArray = new int[byteArray.length];
		for (int i = 0; i < intArray.length; i++) {
			intArray[i] = byteToInt(byteArray[i]);
		}
		return intArray;
	}
  • 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

 

流程梳理

在这里插入图片描述
最后附上Lua文件解密后的样子:
在这里插入图片描述

 

相关资料

浅析android手游lua脚本的加密与解密
Lua游戏逆向及破解方法介绍

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/481833
推荐阅读
相关标签
  

闽ICP备14008679号