首页
社区
课程
招聘
[原创]CTF2017 第十二题 kxuectf-apk 解题报告
2017-6-25 12:42 4168

[原创]CTF2017 第十二题 kxuectf-apk 解题报告

2017-6-25 12:42
4168

CTF2017 第十二题 kxuectf-apk

dex 部分

放入 JEB,截两段重要源码

this.button.setOnClickListener(new View$OnClickListener() {
            public void onClick(View v) {
			    // 点击事件,组装 ab.a(下面的算法)、ba.a(一个替换算法)、ca.a(base64)
				// 传入 jni 的 check 函数
                String v2 = uv.this.editText.getText().toString().trim();
                if(ua.check(ab.a(v2) + ba.a(v2) + ca.a(v2)) == 1) {
                    bc.showresult(uv.this.tv, true);
                }
                else {
                    bc.showresult(uv.this.tv, false);
                }
            }
        });
		
// 上面的 ab.a 算法
public static String encrypt(String input) {
        String v3 = "";
        int v2;
        for(v2 = 0; v2 < input.length(); ++v2) {
            int v0 = input.charAt(v2);
            if(v0 < 97 || v0 > 109) {
                if(v0 >= 65 && v0 <= 77) {
                    v3 = String.valueOf(v3) + (((char)(v0 + 13)));
                    goto label_21;
                }
                if(v0 >= 110 && v0 <= 122) {
                    v3 = String.valueOf(v3) + (((char)(v0 - 13)));
                    goto label_21;
                }
                if(v0 >= 78 && v0 <= 90) {
                    v3 = String.valueOf(v3) + (((char)(v0 - 13)));
                    goto label_21;
                }
                if(v0 >= 48 && v0 <= 57) {
                    v3 = String.valueOf(v3) + (((char)(v0 ^ 7)));
                    goto label_21;
                }
                v3 = String.valueOf(v3) + (((char)(v0 ^ 6)));
            }
            else {
                v3 = String.valueOf(v3) + (((char)(v0 + 13)));
            }
        label_21:
        }
        return v3;
    }

这个 ab.a 算法其实有多解,其实本题最后应该有4个解

K^ -> X
k~ -> x
@S -> F
M\ -> Z
`s -> f
m| -> z
L_ -> Y

附 python 编解码

from collections import defaultdict
def aba_(d):
    if 65 <= d <= 77:
        return chr(d + 13)
    elif 110 <= d <= 122:
        return chr(d - 13)
    elif 78 <= d <= 90:
        return chr(d - 13)
    elif 48 <= d <= 57:
        return chr(d ^ 7)
    elif 97 <= d <= 109:
        return chr(d + 13)
    return chr(d ^ 6)
def aba_enc(s):
    r = ""
    for c in s:
        r += aba_(ord(c))
    return r
def aba_dec(s):
    dt = defaultdict(str)
    for i in xrange(0x20, 0x7F):
        dt[aba_(i)] += chr(i)
    r = ""
    for c in s:
        t = dt[c]
        if len(t) == 1:
            r += t
        else:
            r += '[' + t + ']'
    return r

jni 部分

jni 有3个平台可供选择分析,我选择了 IDA 相容性较好的 armeabi-v7a 版本

反反调试

jni 部分有数种反调试手段,但是都可以用比较原始的方法去掉

  • 可以注入一个 so,修改相关函数的返回
  • 我是直接使用 keypatch 修改 so,替换原有 apk 安装

主程序逻辑

  1. 截取 dex 部分输入的组合字符前36字节
  2. 关于输入长度,程序没有并没有说明,只有大于120会失败,其实应该改成不等于120失败,就可以不用提示也能做出来,因为没有限定长度,只能靠猜测36字节就是 aba 加密结果才能作出该题,因为只有 aba 是36字节时,整体组合字符才会是 120
  3. 在36字节最后 padding 4,4,4,4,组成40字节,使用 FDB4685408CD564E 作为密钥,用变种的 DES 算法加密后,得到 desres(40字节)
  4. 将 654832EFBACD564E0F9B1D27 组合 desres[32:36] 成16字节密钥,用 RC6 算法加密 desres[:32] 得到 rc6res(32字节) 与程序内置的结果进行对比

解法步骤

需要使用调试来判断2个变种加密算法的修改细节,本文最后一部分会附上 diff,因为题目进行中公开了用户输入的前8位 kxuectf{ 和末位 },本文将在给出提示的前提下提供解法步骤

  1. aba_enc("kxuectf{") 得到 786b68727067737d
  2. DES 加密后得到 4cd9a3e6edfed105
  3. 枚举 desres[32:36] 部分,组合RC6密钥,解密 内置结果[:16],得到 desres[:16]
  4. 对比 desres[:8] 与 4cd9a3e6edfed105,相同则找到正确的 desres[32:36] 136a7e1f,以及 desres[:16]
  5. RC6解密 内置结果[16:] 解出剩下的 desres[16:32],DES 解密 desres[:32] 解出 abares[:32]
  6. 在可见字符范围内(aba输出可见字符),枚举 abares[32:36],padding 上[4,4,4,4]组合成 8 字节,用变种 DES 加密
  7. 对比 加密结果[:4] 与 136a7e1f,相同找到正确的 abares[32:36] 613e367b,组合成 abares(36字节)
  8. aba_dec(abares) 得到正确解,应该有4组,选了组没有奇怪标点的提交

算法源码

DES

set_key 位移部分不知道有bug还是啥,和标准不大一样,原型代码

--- Des.cpp	2017-06-22 21:29:47.000000000 +0800
+++ Des2.cpp	2017-06-25 12:19:36.000000000 +0800
@@ -6,7 +6,7 @@
 */
 //////////////////////////////////////////////////////////////////////////
 
-#include "memory.h"
+#include <string.h>
 #include "Des.h"
 
 //////////////////////////////////////////////////////////////////////////
@@ -163,7 +163,13 @@
             Xor(Ri, Li, 32);
             memcpy(Li, tmp, 32);
         }
+        memcpy(tmp, M, 32);
+        memcpy(M, M + 32, 32);
+        memcpy(M + 32, tmp, 32);
     }else{
+        memcpy(tmp, M, 32);
+        memcpy(M, M + 32, 32);
+        memcpy(M + 32, tmp, 32);
         for(int i=15; i>=0; --i) {
             memcpy(tmp, Li, 32);
             F_func(Li, (*pSubKey)[i]);
@@ -179,10 +185,23 @@
     static bool K[64], *KL=&K[0], *KR=&K[28];
     ByteToBit(K, Key, 64);
     Transform(K, K, PC1_Table, 56);
+    static bool KK[120], *K1=&KK[0], *K2=&KK[60];
+    memcpy(K1, K, 28); memcpy(K1 + 28, K, 28);
+    memcpy(K2, K + 28, 28); memcpy(K2 + 28, K + 28, 28);
+
+    int loop = 0;
     for(int i=0; i<16; ++i) {
-        RotateL(KL, 28, LOOP_Table[i]);
-        RotateL(KR, 28, LOOP_Table[i]);
-        Transform((*pSubKey)[i], K, PC2_Table, 48);
+        loop += LOOP_Table[i];
+        for (int j=0; j<48; ++j) {
+            int k = PC2_Table[j];
+            bool r;
+            if (k < 28) {
+                r = K1[loop + k - 1];
+            } else {
+                r = K1[32 + loop + k - 1];
+            }
+            SubKey[0][i][j] = r;
+        }
     }
 }
 void F_func(bool In[32], const bool Ki[48])
@@ -220,14 +239,18 @@
 }
 void ByteToBit(bool *Out, const char *In, int bits)
 {
+    int t = bits - 1;
+    if (t > 7) t = 7;
     for(int i=0; i<bits; ++i)
-        Out[i] = (In[i>>3]>>(i&7)) & 1;
+        Out[i] = (In[i>>3]>>(t-i%8))&1;
 }
 void BitToByte(char *Out, const bool *In, int bits)
 {
+    int t = bits - 1;
+    if (t > 7) t = 7;
     memset(Out, 0, bits>>3);
     for(int i=0; i<bits; ++i)
-        Out[i>>3] |= In[i]<<(i&7);
+        Out[i>>3] |= In[i]<<(t-i%8);
 }
 //////////////////////////////////////////////////////////////////////////
 // Code ends at Line 231

RC6

RC6 部分赛后 poyoten 发现应该是这个实现,我为了用 go 快速枚举,我是在 go-rc6 这个实现上修改的

// Package rc6 implements the RC6 cipher
/*
For more information, please see:
    https://en.wikipedia.org/wiki/RC6
    http://www.emc.com/emc-plus/rsa-labs/historical/rc6-block-cipher.htm
*/
package rc6
import (
	"crypto/cipher"
	"strconv"
)
const (
	rounds    = 20
	roundKeys = 2*rounds + 4
	keyWords  = 4
)
type rc6cipher struct {
	rk [roundKeys]uint32
	L  [4]uint32
}
func rotl32(k uint32, rot uint32) uint32 {
	return (k << rot) | (k >> (32 - rot))
}
func rotr32(k uint32, rot uint32) uint32 {
	return (k >> rot) | (k << (32 - rot))
}
type KeySizeError int
func (k KeySizeError) Error() string { return "rc6: invalid key size " + strconv.Itoa(int(k)) }
// New returns a cipher.Block implementing RC6.  The key argument must be 16 bytes.
func New(key []byte) (cipher.Block, error) {
	if l := len(key); l != 16 {
		return nil, KeySizeError(l)
	}
	c := &rc6cipher{}
	for i := 0; i < keyWords; i++ {
		c.L[i] = getUint32(key)
		key = key[4:]
	}
	return c, nil
}
func (c *rc6cipher) setrk() {
	copy(c.rk[:], skeytable)
	var A uint32
	var B uint32
	var i, j int
	for k := 0; k < 3*roundKeys; k++ {
		c.rk[i] = rotl32(c.rk[i]+(A+B), 3)
		A = c.rk[i]
		c.L[j] = rotl32(c.L[j]+(A+B), (A+B)&31)
		B = c.L[j]
		i = (i + 1) % roundKeys
		j = (j + 1) % keyWords
	}
}
func (c *rc6cipher) BlockSize() int { return 16 }
func (c *rc6cipher) Encrypt(dst, src []byte) {
	A := getUint32X(src)
	B := getUint32X(src[4:])
	C := getUint32X(src[8:])
	D := getUint32X(src[12:])
	c.setrk()
	B = B + c.rk[0]
	D = D + c.rk[1]
	for i := 1; i <= rounds; i++ {
		t := rotl32(B*(2*B+1), 5)
		u := rotl32(D*(2*D+1), 5)
		A = rotl32((A^t), u&31) + c.rk[2*i]
		C = rotl32((C^u), t&31) + c.rk[2*i+1]
		A, B, C, D = B, C, D, A
	}
	A = A + c.rk[2*rounds+2]
	C = C + c.rk[2*rounds+3]
	putUint32X(dst, A)
	putUint32X(dst[4:], B)
	putUint32X(dst[8:], C)
	putUint32X(dst[12:], D)
}
func (c *rc6cipher) Decrypt(dst, src []byte) {
	A := getUint32X(src)
	B := getUint32X(src[4:])
	C := getUint32X(src[8:])
	D := getUint32X(src[12:])
	c.setrk()
	C = C - c.rk[2*rounds+3]
	A = A - c.rk[2*rounds+2]
	for i := rounds; i >= 1; i-- {
		A, B, C, D = D, A, B, C
		u := rotl32(D*(2*D+1), 5)
		t := rotl32(B*(2*B+1), 5)
		C = rotr32((C-c.rk[2*i+1]), t&31) ^ u
		A = rotr32((A-c.rk[2*i]), u&31) ^ t
	}
	D = D - c.rk[1]
	B = B - c.rk[0]
	putUint32X(dst, A)
	putUint32X(dst[4:], B)
	putUint32X(dst[8:], C)
	putUint32X(dst[12:], D)
}
// avoid pulling in encoding/binary
func getUint32(b []byte) uint32 {
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
func getUint32X(b []byte) uint32 {
	return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
}
func putUint32(b []byte, v uint32) {
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
}
func putUint32X(b []byte, v uint32) {
	b[0] = byte(v >> 24)
	b[1] = byte(v >> 16)
	b[2] = byte(v >> 8)
	b[3] = byte(v)
}
// skeytable computed from
/*
Pw = uint32(0xb7e15163)
Qw = uint32(0x9e3779b9)
    T = 2*(R+2);
    S[0] = Pw;
    for (i = 1 ; i < T ; i++)  {
        S[i] = S[i-1] + Qw;
    }
*/
var skeytable = []uint32{
	0xb7e15163, 0x19a9d7aa, 0x7b725df1, 0xdd3ae438, 0x3f036a7f, 0xa0cbf0c6, 0x294770d, 0x645cfd54,
	0xc625839b, 0x27ee09e2, 0x89b69029, 0xeb7f1670, 0x4d479cb7, 0xaf1022fe, 0x10d8a945, 0x72a12f8c,
	0xd469b5d3, 0x36323c1a, 0x97fac261, 0xf9c348a8, 0x5b8bceef, 0xbd545536, 0x1f1cdb7d, 0x80e561c4,
	0xe2ade80b, 0x44766e52, 0xa63ef499, 0x8077ae0, 0x69d00127, 0xcb98876e, 0x2d610db5, 0x8f2993fc,
	0xf0f21a43, 0x52baa08a, 0xb48326d1, 0x164bad18, 0x7814335f, 0xd9dcb9a6, 0x3ba53fed, 0x9d6dc634,
	0xff364c7b, 0x60fed2c2, 0xc2c75909, 0x248fdf50,
}



[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞3
打赏
分享
最新回复 (1)
雪    币: 98
活跃值: (364)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
sherrydl 1 2017-6-26 16:43
2
0
输入长度才是坑。
游客
登录 | 注册 方可回帖
返回