淘宝UA反混淆之常量替换(201605_64.js)
目前对淘宝UA反混淆还要进行常量替换。在esprima和C#之间徘徊了许久,最终我选择了用C#自行编写代码来实现常量替换。虽然用C#大致实现了这个功能,但是代码执行的效率,通用性,正确率应该是远不如esprima的。如果你也跟我一样正在徘徊,那我建议你使用etools。今天记录用C#实现淘宝UA反混淆步骤中的常量替换,给大家做一个反面教材吧。本文算法读取的js文件已完成去数组下标和函数反混淆。
常量替换的基本思路
首先我们要确定js文件中常量和它们的作用域,然后要找出常量表达式(可以被常量替换的表达式),根据常量表达式所在位置找到相应作用域的常量并进行替换。
下面的代码以及描述中,我将一对{}当成一个作用域或者函数,尽管并不一定所有的{}都是函数,这里我们先做这样一个约定,以下的描述中的“作用域”指的是一组花括号包含的范围;而代码注释中的”函数“一般也是一组花括号包含的范围。
根据以上基本思路,我定义类FunctionInfo来记录每一个作用域的信息,这些信息包含:
a. 左( { )右( } )括号在整个js文件所组成的字符串中的序号;
b. 每一组{}的父级{}在作用域列表中的序号,父级仅记录一个;
c. 每一组{}的子级{}在作用域列表中的序号列表,某些作用域中可能会有多个并列的子级{};
d. 每一组{}的常量列表,该列表不包含其子集中的常量。该常量列表中的数据类型为ConstantInfo,包含常量名称及其相应的值。
下面开始分步介绍C#实现常量替换的实现方法。
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 |
//用于记录js文件中function所包含区域的信息:所包含区域的字符序号范围和所在区域定义的常量集 public class FunctionInfo { //每个花括号的左右括号在字符串中的位置 private Point _funRange; public Point funRange { get { return _funRange; } set { _funRange = value; } } //记录每一个花括号的上一级花括号在配对括号列表中的序号 private int _fatherIndex; public int fatherIndex { get { return _fatherIndex; } set { _fatherIndex = value; } } //记录每一个花括号的下一次花括号的序号列表,一个花括号下可能有多个子花括号并列 private ArrayList _sonList; public ArrayList sonList { get { return _sonList; } set { _sonList = value; } } //记录当前花括号下的常量列表 private ArrayList _constantList; public ArrayList constantList { get { return _constantList; } set { _constantList = value; } } } public class ConstantInfo//常量信息记录 { private string _name; public string name { get { return _name; } set { _name = value; } } private string _value; public string value { get { return _value; } set { _value = value; } } } |
寻找作用域列表
用正则表达式找到所有的花括号{},根据花括号出现的顺序用stack进行配对查找,代码如下所示。当前代码是有缺陷的,并未考虑单只存在的左括号或右括号,幸运的是64.js中正好所有的花括号都是成对出现的。我觉得这个方案可以改善的方向是:js文件中用来表示函数范围的花括号一定是成对出现的,不成对出现的一般是js代码中出现的字符串中包含的单只左或右括号,所以应该改善正则表达式,让它能准确的匹配一个函数的开始和结束。这个问题留给有兴趣的同学继续。
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 |
/// <summary> /// 在输入的js代码中找到花括号的配对 /// </summary> /// <param name="codeStr">js文件的代码字符串</param> /// <returns>返回配对的括号在代码字符串中的位置,最外层的括号在最后面</returns> static private ArrayList getBraceList(string codeStr) { //用正则找出js文件中所有的{和} Regex reg1 = new Regex("[{}]"); MatchCollection braceMatch = reg1.Matches(codeStr); int braceCount = braceMatch.Count; //获取括号对{}在js文件中所在的字符索引号 var braceStack = new Stack<int>(); ArrayList braceList = new ArrayList(); GroupCollection tmpColl = braceMatch[0].Groups; for (int i = 0; i < braceCount; i++) { Point bracePoint = new Point(); tmpColl = braceMatch[i].Groups; if (tmpColl[0].Value == "{") { braceStack.Push(tmpColl[0].Index); } else { if (braceStack.Any()) { bracePoint.X = braceStack.Pop(); bracePoint.Y = tmpColl[0].Index; braceList.Add(bracePoint); } else { //return "brace is single!"; } } } return braceList; } |
获取每组作用域的信息
1. 寻找父作用域
根据stack的作用规则,在作用域列表最后面的,一定是顶级父作用域;在列表最前面的,那就是最顶级子作用域。根据此原则,寻找父作用域时,从作用域列表的第一个序号开始遍历每一个作用域左右括号所在的序号,找到的第一个能将当前作用域包含的作用域作为其父作用域,当前作用域查找父作用域动作结束,开始查找下一个作用域的查找。
2. 寻找子作用域
在每一个作用域的父作用域确定后,子作用域即信手拈来。从作用域列表的最后一个序号开始遍历每一个作用域的父作用域在作用域列表中的序号,父作用域序号与当前查询序号相等的都是当前查询序号的子作用域。
3. 获取当前作用域的常量列表
从作用域列表的第一个序号开始遍历每一个作用域,为了不将其子作用域的常量囊括进来,首先将当前作用域的子作用域的字符串用空字符串替换,替换完成后,根据格式化之后的常量定义的规律来查找常量定义,并按照名值对存入列表中。
由notepad++格式化之后的js文件中常量定义时有下图所示特征,每一个常量定义单独成行;但for循环中的常量定义并不会独立成行,按照此规则写的正则表达式无法匹配for循环中的常量定义,当然还有可能有其他我没有发现的无法匹配的类别。
获取作用域信息的代码如下:
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 |
/// <summary> /// 根据js代码中花括号对的位置关系来查找函数之间的父子关系 /// </summary> /// <param name="codeStr">js文件的代码字符串</param> /// <param name="braceList">js文件的匹配的花括号在字符串中位置列表</param> /// <returns>返回配对的括号在代码字符串中的位置,最外层的括号在最后面</returns> static private ArrayList getRelationFun(string codeStr, ArrayList braceList) { int i = 0, j = 0, k = 0;//循环用变量 int posLeft = 0; int posRight = 0; int posLeftIn = 0; int posRightIn = 0; int funCount = braceList.Count; ArrayList funInfoList = new ArrayList(); //寻找一级父函数,函数序号从小到大开始查找,父函数仅记录一个 for (i = 0; i < funCount; i++) { FunctionInfo funInfo = new FunctionInfo(); posLeft = ((Point)braceList[i]).X; posRight = ((Point)braceList[i]).Y; for (j = i + 1; j < funCount; j++) { posLeftIn = ((Point)braceList[j]).X; posRightIn = ((Point)braceList[j]).Y; if ((posLeftIn < posLeft) && (posRightIn > posRight)) { funInfo.fatherIndex = j; break;//仅记录一级父函数,记录该父函数在函数列表中的序号 } } if (j == funCount) { funInfo.fatherIndex = -1; } funInfo.funRange = (Point)braceList[i]; funInfoList.Add(funInfo); } //寻找一级子函数,函数序号从大到小开始查找,一级子函数可能会有多个并列 for (i = (funCount - 1); i > 0; i--) { ArrayList funSon = new ArrayList(); for (j = i - 1; j >= 0; j--) { if (((FunctionInfo)funInfoList[j]).fatherIndex == i) { funSon.Add(j); } } ((FunctionInfo)funInfoList[i]).sonList = funSon; } //获取每一个花括号中定义的常量 string strCon = codeStr; int sonFunCount = 0; string tmpStr = ""; //获取每一个function范围内的常量定义 Regex reg1 = new Regex(@"\b(\w+)\s=\s[\""\'](.*?)[\""\'][,;]\r\n"); for (i = 0; i < funCount; i++) { ArrayList conList = new ArrayList(); ArrayList funSon = ((FunctionInfo)funInfoList[i]).sonList; ArrayList strList = new ArrayList(); posLeft = ((Point)braceList[i]).X; posRight = ((Point)braceList[i]).Y; strCon = codeStr.Substring(posLeft, posRight - posLeft); if (funSon != null) { sonFunCount = funSon.Count; for (j = 0; j < sonFunCount; j++) { posLeftIn = ((Point)braceList[(int)funSon[j]]).X; posRightIn = ((Point)braceList[(int)funSon[j]]).Y; tmpStr = codeStr.Substring(posLeftIn, posRightIn - posLeftIn); strList.Add(tmpStr); } for (j = 0; j < sonFunCount; j++ ) { strCon = strCon.Replace(((string)(strList[j])), ""); } MatchCollection constantMatch = reg1.Matches(strCon); int constantCount = constantMatch.Count; GroupCollection tmpColl = null; for (j = 0; j < constantCount; j ++ ) { ConstantInfo conInfo = new ConstantInfo(); tmpColl = constantMatch[j].Groups; conInfo.name = tmpColl[1].Value; conInfo.value = tmpColl[2].Value; conList.Add(conInfo); } ((FunctionInfo)funInfoList[i]).constantList = conList; } } return funInfoList; } |
找到常量表达式并替换
当前代码中认为如下图所示类似的形式为常量表达式,代码中的正则表达式也会忽略另外一部分常量表达式。
将常量表达式用” + “进行分割,得到变量后,对变量进行逐一替换。替换时,在常量列表中倒序查找,这里认为离常量表达式最近的常量是有效的(常量定义中会出现同一个作用域内相同变量被多次复制),当前作用域内找不到的,到其父作用域进行查找,以此类推,实现代码如下:
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 |
static private string replaceConstant(string codeStr) { ArrayList braceList = getBraceList(codeStr); ArrayList funInfoList = getRelationFun(codeStr, braceList); //获取文件中的常量表达式 Regex reg1 = new Regex(@"\[((\w+\s\+\s){1,}\w+)\]"); MatchCollection expressionMatch = reg1.Matches(codeStr); int exCount = expressionMatch.Count; string[] strArray = null; int eCount = 0; int eIndex = 0; string tmpXstr = ""; string tmpStr = ""; string content = codeStr; int braceLCount = braceList.Count; int i = 0, j = 0, k = 0;//循环用变量 //替换常量处理 //替换原则:1. 先查找常量表达式所在的括号对中所包含的常量定义,若找到则替换,若找不到,则进入第二步 // 2. 在包含常量表达式的上一级括号对,且所在序号小于常量表达式字符串中寻找匹配常量,找不到再找上一级 GroupCollection tmpColl = null; for (i = 0; i < exCount; i++) { tmpColl = expressionMatch[i].Groups; //当前表达式在字符串中的位置 eIndex = tmpColl[0].Index; tmpStr = tmpColl[1].Value; //将当前表达式分割为常量数组 strArray = tmpStr.Split(new[] { " + " }, StringSplitOptions.None); eCount = strArray.Length; tmpStr = ""; //判断当前常量表达式所在的函数序号 for (j = 0; j < braceLCount; j ++ ) { Point tmpPoint = (Point)braceList[j]; if ((tmpPoint.X < eIndex) && (tmpPoint.Y > eIndex)) { break; } } //先在当前表达式所在函数区域的常量定义列表中查找,所找不到,就到父函数中的常量定义中查找 for (k = 0; k < eCount; k ++ ) { //当找到相应变量时,beOK为true bool beOK = false; int sIndex = j;//每一个字符开始查找的子函数均为j int m = 0;//循环变量 tmpXstr = strArray[k]; do { ArrayList tmpConList = ((FunctionInfo)funInfoList[sIndex]).constantList; int tmpCount = tmpConList.Count; for (m = tmpCount - 1; m >= 0; m -- ) { ConstantInfo tmpInfo = (ConstantInfo)(tmpConList[m]); if (tmpXstr == tmpInfo.name) { tmpStr += tmpInfo.value; beOK = true; break; } } if (!beOK) { sIndex = ((FunctionInfo)funInfoList[sIndex]).fatherIndex; } } while (!beOK); } content = content.Replace(tmpColl[1].Value, tmpStr); } return content; } |
当前常量替换方法的缺陷
本文中C#实现的淘宝UA反混淆的方法的缺陷: 每一次淘宝UA更新都需要去人工查找函数作用域,常量定义,常量表达式的特征,若观察不仔细或正则表达式写的不够全面,则很可能出现过匹配或者漏匹配。所以要实现js文件的反混淆,还是要首选node和etools系列工具,一劳永逸。