吾爱破解 - LCG - LSG |安卓破解|病毒分析|破解软件|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 10523|回复: 81

[Android 原创] 娜迦14年挑战赛第二阶段四道题题解

  [复制链接]
发表于 2017-10-2 16:33 | 显示全部楼层
本帖最后由 wnagzihxain 于 2017-10-2 16:36 编辑

附件在这,里面有所有题目,解题时的文件等,还简单录了一个视频讲了大概的情况:http://pan.baidu.com/s/1pKB8b5x

NAGA & PIOWIND 2014 APP应用攻防竞赛第二阶段第一题题解

Java层用于传字符串,输入用户名和密码到Native层校验

protected void onCreate(Bundle arg3) {
    super.onCreate(arg3);
    this.setContentView(2130903040);
    this.txt_name = this.findViewById(2131165184);
    this.txt_passwd = this.findViewById(2131165185);
    this.btn_login = this.findViewById(2131165186);
    this.btn_reset = this.findViewById(2131165187);
    this.txt_result = this.findViewById(2131165188);
    this.btn_login.setOnClickListener(new View$OnClickListener() {
        public void onClick(View arg7) {
            String v0 = MainActivity.this.txt_name.getText().toString();
            String v1 = MainActivity.this.txt_passwd.getText().toString();
            if("".equals(v0)) {
                System.out.println("name is null or \'\'");
                MainActivity.this.txt_result.setText("账户为空");
            }
            else if("".equals(v1)) {
                System.out.println("passwd is null or \'\'");
                MainActivity.this.txt_result.setText("密码为空");
            }
            else {
                System.out.println("name:" + v0);
                System.out.println("passwd:" + v1);
                System.out.println("Please treat me gently, you have to go a long way.");
                MainActivity.this.txt_result.setText(MainActivity.this.crackme(v0, v1));
            }
        }
    });
    this.btn_reset.setOnClickListener(new View$OnClickListener() {
        public void onClick(View arg3) {
            MainActivity.this.txt_name.setText("");
            MainActivity.this.txt_passwd.setText("");
            MainActivity.this.txt_result.setText("");
        }
    });
}

使用IDA查看so,发现加密了

1.png

动态调试把解密后的so文件dump出来

先查看加载的内存基址

2.png

dump脚本如下,地址需要根据自己的调试环境确定

auto fp, dex_addr, end_addr;  
fp = fopen("E:\\libcrackme.so", "wb");  
for(dex_addr = 0xA35C8000; dex_addr < 0xA3609000; dex_addr++)  
    fputc(Byte(dex_addr), fp);  

然后再打开,可以看到代码已经还原了

3.png

先传入用户名和密码,然后转为char *类型的字符串,,接着调用两个函数sub_536C()sub_597C()

LOAD:00005B50             ; jstring __fastcall Java_com_crackme_MainActivity_crackme(JNIEnv *env, int a2, jstring a3, jstring a4)
LOAD:00005B50             EXPORT Java_com_crackme_MainActivity_crackme
LOAD:00005B50             Java_com_crackme_MainActivity_crackme
LOAD:00005B50 F8 B5       PUSH            {R3-R7,LR}
LOAD:00005B52             ; 7:   v4 = a4;
LOAD:00005B52 1E 1C       MOVS            R6, R3  ; R6 = R3 = password
LOAD:00005B54             ; 9:   v6 = (*env)->GetStringUTFChars(env, a3, 0);
LOAD:00005B54 03 68       LDR             R3, [R0] ; R3 = [R0] = *env
LOAD:00005B56 A9 25 AD 00 MOVS            R5, #0x2A4 ; R5 = 0x2A4
LOAD:00005B5A 5B 59       LDR             R3, [R3,R5] ; R3 = GetStringUTFChars()
LOAD:00005B5C 11 1C       MOVS            R1, R2  ; R1 = R2 = username
LOAD:00005B5E             ; 8:   vEnv = env;
LOAD:00005B5E 00 22       MOVS            R2, #0  ; R2 = 0
LOAD:00005B60 04 1C       MOVS            R4, R0  ; R4 = R0 = env
LOAD:00005B62 98 47       BLX             R3      ; R0 = (*env)->GetStringUTFChars(env, username, 0)
LOAD:00005B64 23 68       LDR             R3, [R4] ; R3 = [R4] = *env
LOAD:00005B66 07 1C       MOVS            R7, R0  ; R7 = R0 = szUserName
LOAD:00005B68             ; 10:   v7 = (*vEnv)->GetStringUTFChars(vEnv, v4, 0);
LOAD:00005B68 31 1C       MOVS            R1, R6  ; R1 = R6 = Password
LOAD:00005B6A 5B 59       LDR             R3, [R3,R5] ; R3 = GetStringUTFChars()
LOAD:00005B6C 20 1C       MOVS            R0, R4  ; R0 = R4 = env
LOAD:00005B6E 00 22       MOVS            R2, #0  ; R2 = 0
LOAD:00005B70 98 47       BLX             R3      ; R0 = (*env)->GetStringUTFChars(env, password, 0)
LOAD:00005B72             ; 11:   sub_536C("Failure", v6, v7);
LOAD:00005B72 09 4D       LDR             R5, =(dword_15220 - 0x5B7C) ; R5 = pFailure - 0x5B7C
LOAD:00005B74 39 1C       MOVS            R1, R7  ; R1 = szUserName
LOAD:00005B76 02 1C       MOVS            R2, R0  ; R2 = R0 = szPassword
LOAD:00005B78 7D 44       ADD             R5, PC ; dword_15220
LOAD:00005B7A 04 35       ADDS            R5, #4  ; R5 = pFailure
LOAD:00005B7C 28 1C       MOVS            R0, R5  ; R0 = R5 = pFailure
LOAD:00005B7E FF F7 F5 FB BL              sub_536C ; R0 = sub_536C("Failure", szUserName, szPassword)
LOAD:00005B82             ; 12:   sub_597C((int)"Failure");
LOAD:00005B82 28 1C       MOVS            R0, R5  ; R0 = R5 = pFailure
LOAD:00005B84 FF F7 FA FE BL              sub_597C ; R0 = sub_597C(pFailure)
LOAD:00005B88             ; 13:   return (*vEnv)->NewStringUTF(vEnv, "Failure");
LOAD:00005B88 22 68       LDR             R2, [R4] ; R2 = [R4] = *env
LOAD:00005B8A A7 23 9B 00 MOVS            R3, #0x29C ; R3 = 0x29C
LOAD:00005B8E 29 1C       MOVS            R1, R5  ; R1 = R5 = pFailure
LOAD:00005B90 D3 58       LDR             R3, [R2,R3] ; R3 = NewStringUTF()
LOAD:00005B92 20 1C       MOVS            R0, R4  ; R0 = R4 = env
LOAD:00005B94 98 47       BLX             R3      ; R0 = (*env)->NewStringUTF(env, "Failure")
LOAD:00005B96 F8 BD       POP             {R3-R7,PC}
LOAD:00005B96             ; End of function Java_com_crackme_MainActivity_crackm

跟入sub_536C("Failure", szUserName, szPassword)

这个函数比较简单

4.png

先传进来一个字符串指针,这个指针非常重要,后续的栈变量要使用这个字符串指针作为基址来寻找

LOAD:0000536C             ; const JNINativeInterface *__fastcall malloc_Heap(int a1, const char *a2, const char *a3)
LOAD:0000536C             malloc_Heap
LOAD:0000536C
LOAD:0000536C             n= -0x24
LOAD:0000536C             len_UserName= -0x20
LOAD:0000536C             len_Password= -0x1C
LOAD:0000536C
LOAD:0000536C F0 B5       PUSH            {R4-R7,LR}
LOAD:0000536E 85 B0       SUB             SP, SP, #0x14 ; 抬高栈顶
LOAD:00005370             ; 15:   v3 = a3;
LOAD:00005370 16 1C       MOVS            R6, R2  ; R6 = R2 = szPassword
LOAD:00005372             ; 16:   v4 = a1;
LOAD:00005372 04 1C       MOVS            R4, R0  ; R4 = R0 = "Failure"
LOAD:00005374             ; 17:   v5 = a2;
LOAD:00005374 0D 1C       MOVS            R5, R1  ; R5 = R1 = szUserName
LOAD:00005376             ; 18:   result = (const JNINativeInterface *)sub_5328(a1);
LOAD:00005376 FF F7 D7 FF BL              sub_5328 ; R0 = sub_5328(pFailure)

函数sub_5328()用于初始化某些栈空间

5.png

然后有两处判断,判断传入的两个字符串是否为空

判断密码是否为空

LOAD:0000537A             ; 19:   if ( v3 )
LOAD:0000537A 00 2E       CMP             R6, #0  ; if(szPassword == 0)
LOAD:0000537C 2F D0       BEQ             loc_53DE

判断用户名是否为空

LOAD:0000537E             ; 21:     if ( v5 )
LOAD:0000537E 00 2D       CMP             R5, #0  ; if(szUserName == 0)
LOAD:00005380 2D D0       BEQ             loc_53DE

申请空间

LOAD:00005382             ; 23:       v7 = strlen(v5);
LOAD:00005382 28 1C       MOVS            R0, R5  ; s
LOAD:00005384 FF F7 1C EF BLX             strlen  ; R0 = strlen(szUserName)
LOAD:00005388             ; 24:       len_UserName = v7;
LOAD:00005388 02 90       STR             R0, [SP,#0x28+len_UserName] ; len_UserName = strlen(szUserName)
LOAD:0000538A             ; 25:       v8 = v7;
LOAD:0000538A 07 1C       MOVS            R7, R0  ; R7 = R0 = len_UserName
LOAD:0000538C             ; 26:       v9 = strlen(v3);
LOAD:0000538C 30 1C       MOVS            R0, R6  ; s
LOAD:0000538E FF F7 18 EF BLX             strlen  ; R0 = strlen(szPassword)
LOAD:00005392             ; 27:       v10 = v8 + 1;
LOAD:00005392 01 37       ADDS            R7, #1  ; R7 = len_UserName + 1
LOAD:00005394             ; 28:       v14 = v9;
LOAD:00005394 03 1C       MOVS            R3, R0  ; R3 = R0 = len_Password
LOAD:00005396 01 33       ADDS            R3, #1  ; R3 = len_Password + 1
LOAD:00005398 03 90       STR             R0, [SP,#0x28+len_Password] ; len_Password = R0
LOAD:00005398                                     ; R0 = len_UserName + 1
LOAD:0000539A             ; 30:       *(_DWORD *)(v4 + 52) = operator new[](v10);
LOAD:0000539A 38 1C       MOVS            R0, R7  ; unsigned int
LOAD:0000539C             ; 29:       n = v9 + 1;
LOAD:0000539C 01 93       STR             R3, [SP,#0x28+n] ; n = len_Password + 1
LOAD:0000539E FF F7 16 EF BLX             _Znaj   ; operator new[](len_UserName + 1) // 申请空间
LOAD:000053A2 60 63       STR             R0, [R4,#0x34] ; R0为新UserName存储堆地址
LOAD:000053A4             ; 31:       result = (const JNINativeInterface *)operator new[](n);
LOAD:000053A4 01 98       LDR             R0, [SP,#0x28+n] ; unsigned int
LOAD:000053A6 FF F7 12 EF BLX             _Znaj   ; operator new[](uint)
LOAD:000053AA             ; 32:       v11 = *(void **)(v4 + 52);
LOAD:000053AA 63 6B       LDR             R3, [R4,#0x34] ; R3 = pUserName
LOAD:000053AC             ; 33:       *(_DWORD *)(v4 + 56) = result;
LOAD:000053AC A0 63       STR             R0, [R4,#0x38] ; R0为新Password存储堆地址

通过返回的内存分配地址来判断是否申请成功

LOAD:000053AE             ; 34:       if ( v11 )
LOAD:000053AE 00 2B       CMP             R3, #0  ; 判断UserName内存空间是否申请成功
LOAD:000053B0 15 D0       BEQ             loc_53DE

第二处判断

LOAD:000053B2             ; 36:         if ( result )
LOAD:000053B2 00 28       CMP             R0, #0  ; 判断Password内存空间是否申请成功
LOAD:000053B4 13 D0       BEQ             loc_53DE

接下来进行拷贝操作,存储用户名和密码,需要注意到新申请的两个变量的寻址方式为[pFailure + offset]

LOAD:000053B6             ; 38:           memset(v11, 0, v10);
LOAD:000053B6 18 1C       MOVS            R0, R3  ; s
LOAD:000053B8 00 21       MOVS            R1, #0  ; c
LOAD:000053BA 3A 1C       MOVS            R2, R7  ; n
LOAD:000053BC FF F7 FA EE BLX             memset  ; memset(pUserName, 0, len_UserName + 1) //初始化UserName内存空间
LOAD:000053C0             ; 39:           memset(*(void **)(v4 + 56), 0, n);
LOAD:000053C0 00 21       MOVS            R1, #0  ; c
LOAD:000053C2 01 9A       LDR             R2, [SP,#0x28+n] ; n
LOAD:000053C4 A0 6B       LDR             R0, [R4,#0x38] ; s
LOAD:000053C6 FF F7 F6 EE BLX             memset  ; memset(pPassword, 0, len_Password + 1) //初始化Password内存空间
LOAD:000053CA             ; 40:           memcpy(*(void **)(v4 + 52), v5, len_UserName);
LOAD:000053CA 29 1C       MOVS            R1, R5  ; src
LOAD:000053CC 02 9A       LDR             R2, [SP,#0x28+len_UserName] ; n
LOAD:000053CE 60 6B       LDR             R0, [R4,#0x34] ; dest
LOAD:000053D0 FF F7 02 EF BLX             memcpy  ; memcpy(pUserName, szUserName, len_UserName) //拷贝数据到内存空间
LOAD:000053D4             ; 41:           result = (const JNINativeInterface *)memcpy(*(void **)(v4 + 56), v3, v14);
LOAD:000053D4 A0 6B       LDR             R0, [R4,#0x38] ; dest
LOAD:000053D6 31 1C       MOVS            R1, R6  ; src
LOAD:000053D8 03 9A       LDR             R2, [SP,#0x28+len_Password] ; n
LOAD:000053DA FF F7 FE EE BLX             memcpy  ; memcpy(pPassword, szPassword, len_Password) //拷贝数据到内存空间

此时两个关键的变量在栈中的位置

pUserName = [pFailure + 0x34]
pPassword = [pFailure + 0x38]

初始化完栈空间以及相应的内存空间后,进入校验逻辑

LOAD:00005B82 28 1C       MOVS            R0, R5  ; R0 = R5 = pFailure
LOAD:00005B84 FF F7 FA FE BL              sub_597C ; sub_597C(pFailure)

传入"Failure"字符串的指针,该函数稍微有点长

6.png

存储"pFailure"后调用函数sub_53E4()

LOAD:0000597C             sub_597C
LOAD:0000597C
LOAD:0000597C             var_34= -0x34
LOAD:0000597C             var_30= -0x30
LOAD:0000597C             var_28= -0x28
LOAD:0000597C             var_24= -0x24
LOAD:0000597C             var_1C= -0x1C
LOAD:0000597C
LOAD:0000597C F0 B5       PUSH            {R4-R7,LR}
LOAD:0000597E 89 B0       SUB             SP, SP, #0x24
LOAD:00005980 05 1C       MOVS            R5, R0  ; R5 = R0 = pFailure = "Failure"
LOAD:00005982 FF F7 2F FD BL              sub_53E4

sub_53E4()主要是校验用户名和密码的长度合法性

从中我们得出用户名和密码的长度范围

用户名:[6, 20]
密码:[12, 30]

7.png

校验密码的合法性,格式为xxx-xxx-xxx-xxx

8.png

调用sub_5430()

LOAD:000059B0             loc_59B0
LOAD:000059B0 28 1C       MOVS            R0, R5
LOAD:000059B2 FF F7 3D FD BL              sub_5430

这个函数的作用是将密码中的-去掉

9.png

获取一个Table,此Table一开始是空的

LOAD:000059BA 7B 44       ADD             R3, PC ; Base64Table
LOAD:000059BC 1A 78       LDRB            R2, [R3]
LOAD:000059BE 00 2A       CMP             R2, #0
LOAD:000059C0 31 D1       BNE             loc_5A26

全部都是00

10.png

动态运行时会填充数据,第一次运行时会进行Table的生成,通过对这个Table第一个字节的判断,如果是00,表示未生成,如果是01,表示Table已生成,则跳过初始化Table的代码段

动态运行时进行初始化

11.png

接下来逐步进行计算,将Table的[2, 256]字节赋值为0x80

LOAD:000059C2 80 20       MOVS            R0, #0x80 ; '?' ; R0 = 0x80
LOAD:000059C4 01 33       ADDS            R3, #1  ; 从Table的第二位开始赋值
LOAD:000059C6 41 00       LSLS            R1, R0, #1 ; R1 = 0x80 * 2 = 256

开始循环赋值

LOAD:000059C8             ; 41:       byte_15121[v4++] = -128;
LOAD:000059C8
LOAD:000059C8             loc_59C8                ; Table = 0x80
LOAD:000059C8 D0 54       STRB            R0, [R2,R3]
LOAD:000059CA             ; 42:     while ( v4 != 256 );
LOAD:000059CA 01 32       ADDS            R2, #1  ; R2++
LOAD:000059CC 8A 42       CMP             R2, R1
LOAD:000059CE FB D1       BNE             loc_59C8 ; Table = 0x80

12.png

赋值完成后开始处理Table,初始化一些值

LOAD:000059D0             ; 43:     v5 = 0;
LOAD:000059D0 5B 4A       LDR             R2, =(Base64Table - 0x59D8)
LOAD:000059D2 00 23       MOVS            R3, #0  ; R3 = 0
LOAD:000059D4 7A 44       ADD             R2, PC ; Base64Table
LOAD:000059D6 51 1C       ADDS            R1, R2, #1 ; R1 = Base64Tabl + 1

从Table偏移65的位置开始赋值0,长度为26,整个表应该是偏移第67位,因为第一个字节跳过,下标从0开始

LOAD:000059D8             ; 46:       byte_15121[v5 + 65] = v5;
LOAD:000059D8
LOAD:000059D8             loc_59D8                ;
LOAD:000059D8 C8 18       ADDS            R0, R1, R3 ; R0 = Base64Table + i
LOAD:000059DA 41 30       ADDS            R0, #65 ; R0 = Base64Table + i + 65
LOAD:000059DC 03 70       STRB            R3, [R0] ; Base64Table[i + 65] = R3
LOAD:000059DE             ; 47:       ++v5;
LOAD:000059DE 01 33       ADDS            R3, #1  ; R3++
LOAD:000059E0             ; 49:     while ( v5 != 26 );
LOAD:000059E0 1A 2B       CMP             R3, #26
LOAD:000059E2 F9 D1       BNE             loc_59D8 ;
LOAD:000059E2                                     ; R0 = Base64Table + i

13.png

取第98

LOAD:000059E4             ; 50:     v6 = &byte_15182;
LOAD:000059E4 62 32       ADDS            R2, #98 ; R2 = Base64Table[98]

开始赋值,赋值的数据跟着上面的R3后面继续,上面赋值到0x19,这里从0x1A开始

LOAD:000059E6             ; 53:       *v6 = v5;
LOAD:000059E6
LOAD:000059E6             loc_59E6
LOAD:000059E6 13 70       STRB            R3, [R2]
LOAD:000059E8             ; 54:       v5 = (v5 + 1) & 0xFF;
LOAD:000059E8 01 33       ADDS            R3, #1  ; R3++
LOAD:000059EA 1B 06       LSLS            R3, R3, #0x18
LOAD:000059EC 1B 0E       LSRS            R3, R3, #0x18 ; R3 = R3 & 0xFF
LOAD:000059EE             ; 55:       ++v6;
LOAD:000059EE 01 32       ADDS            R2, #1  ; R2 = Base64Table + i
LOAD:000059F0             ; 57:     while ( v5 != 52 );
LOAD:000059F0 34 2B       CMP             R3, #52
LOAD:000059F2 F8 D1       BNE             loc_59E6

14.png

再次定位到49的位置

LOAD:000059F4             ; 58:     v7 = &byte_15151;
LOAD:000059F4 53 4A       LDR             R2, =(Base64Table - 0x59FA)
LOAD:000059F6 7A 44       ADD             R2, PC ; Base64Table
LOAD:000059F8 31 32       ADDS            R2, #49

再次赋值

LOAD:000059FA             ; 61:       *v7 = v5;
LOAD:000059FA
LOAD:000059FA             loc_59FA
LOAD:000059FA 13 70       STRB            R3, [R2]
LOAD:000059FC             ; 62:       v5 = (v5 + 1) & 0xFF;
LOAD:000059FC 01 33       ADDS            R3, #1
LOAD:000059FE 1B 06       LSLS            R3, R3, #0x18
LOAD:00005A00 1B 0E       LSRS            R3, R3, #0x18
LOAD:00005A02             ; 63:       ++v7;
LOAD:00005A02 01 32       ADDS            R2, #1
LOAD:00005A04             ; 65:     while ( v5 != 62 );
LOAD:00005A04 3E 2B       CMP             R3, #62
LOAD:00005A06 F8 D1       BNE             loc_59FA

15.png

最后处理几个单个的位置

LOAD:00005A08             ; 66:     byte_1514C = 62;
LOAD:00005A08 4F 4A       LDR             R2, =(Base64Table - 0x5A0E)
LOAD:00005A0A 7A 44       ADD             R2, PC ; Base64Table
LOAD:00005A0C 11 1C       MOVS            R1, R2  ; R1 = R2 = Base64Table
LOAD:00005A0E 2C 31       ADDS            R1, #44 ; R1 = R1 + 44
LOAD:00005A10 0B 70       STRB            R3, [R1] ; Base64Table[44] = 0x3E
LOAD:00005A12             ; 67:     byte_15150 = 63;
LOAD:00005A12 13 1C       MOVS            R3, R2  ; R3 = R2 = Base64Table
LOAD:00005A14 30 33       ADDS            R3, #48 ; R3 = R3 + 48
LOAD:00005A16 3F 21       MOVS            R1, #63 ; R1 = 63
LOAD:00005A18 19 70       STRB            R1, [R3] ; Base64Table[48] = 63
LOAD:00005A1A             ; 68:     byte_1515E = 0;
LOAD:00005A1A 13 1C       MOVS            R3, R2  ; R3 = R2 = Base64Table
LOAD:00005A1C 3E 33       ADDS            R3, #62 ; R3 = R3 + 62
LOAD:00005A1E 00 21       MOVS            R1, #0  ; R1 = 0
LOAD:00005A20 19 70       STRB            R1, [R3] ; Base64Table[62] = 0
LOAD:00005A22             ; 69:     Base64Table = 1;
LOAD:00005A22 01 23       MOVS            R3, #1  ; R3 = 1
LOAD:00005A24 13 70       STRB            R3, [R2] ; Base64Table[0] = 1 //设置已初始化Table标志

16.png

整个表处理完是下面这样的,因为最开始是判断是否初始化的标志,所以整个表长度为257,由于多次调试,所以下面的内存地址和上面图中可能不一样

A35DD120  01 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  .???????????????
A35DD130  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD140  80 80 80 80 80 80 80 80  80 80 80 80 3E 80 80 80  ????????????>???
A35DD150  3F 34 35 36 37 38 39 3A  3B 3C 3D 80 80 80 00 80  ?456789:;<=???.?
A35DD160  80 80 00 01 02 03 04 05  06 07 08 09 0A 0B 0C 0D  ??..............
A35DD170  0E 0F 10 11 12 13 14 15  16 17 18 19 80 80 80 80  ............????
A35DD180  80 80 1A 1B 1C 1D 1E 1F  20 21 22 23 24 25 26 27  ??...... !"#$%&'
A35DD190  28 29 2A 2B 2C 2D 2E 2F  30 31 32 33 80 80 80 80  ()*+,-./0123????
A35DD1A0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD1B0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD1C0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD1D0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD1E0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD1F0  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD200  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD210  80 80 80 80 80 80 80 80  80 80 80 80 80 80 80 80  ????????????????
A35DD220  80                                                ?

判断处理后的密码是否为空,前面去除了密码中的-

LOAD:00005A26             ; 71:   if ( v3 )
LOAD:00005A26
LOAD:00005A26             loc_5A26                ; R4为密码寄存器,判断是否为0
LOAD:00005A26 00 2C       CMP             R4, #0
LOAD:00005A28 07 D0       BEQ             loc_5A3A

再申请一个存储密码的内存空间

LOAD:00005A2A             ; 73:     v8 = strlen(v3);
LOAD:00005A2A 20 1C       MOVS            R0, R4  ; s
LOAD:00005A2C FF F7 C8 EB BLX             strlen  ; R0 = strlen(pPassword)
LOAD:00005A30             ; 74:     v9 = (const void *)operator new[](v8 + 1);
LOAD:00005A30 01 30       ADDS            R0, #1  ; unsigned int
LOAD:00005A32 FF F7 CC EB BLX             _Znaj   ; R0 = operator new[](len_Password + 1)
LOAD:00005A36 06 1C       MOVS            R6, R0  ; R6 = R0 = new_pPassword
LOAD:00005A38 00 E0       B               loc_5A3C

这里其实可以猜出来是Base64,因为判断3位长度,这个比较看经验了

LOAD:00005A3C             ; 80:   v10 = strlen(v3);
LOAD:00005A3C
LOAD:00005A3C             loc_5A3C                ; s
LOAD:00005A3C 20 1C       MOVS            R0, R4
LOAD:00005A3E FF F7 C0 EB BLX             strlen  ; R0 = R4 = pPassword
LOAD:00005A3E                                     ; R0 = strlen(pPassword)
LOAD:00005A42             ; 81:   v11 = 0;
LOAD:00005A42 42 49       LDR             R1, =(Base64Table - 0x5A4C)
LOAD:00005A44 03 38       SUBS            R0, #3  ; R0 = R0 - 3
LOAD:00005A46 00 23       MOVS            R3, #0  ; R3 = 0
LOAD:00005A48             ; 85:   while ( v11 < (signed int)(v10 - 3) )
LOAD:00005A48 79 44       ADD             R1, PC ; Base64Table
LOAD:00005A4A 01 31       ADDS            R1, #1  ; R1 = Base64Table + 1
LOAD:00005A4C 04 90       STR             R0, [SP,#0x38+var_28] ; len_Password - 3
LOAD:00005A4E             ; 82:   v12 = v9;
LOAD:00005A4E 32 1C       MOVS            R2, R6  ; R2 = R6 = new_pPassword
LOAD:00005A50             ; 83:   v13 = 0;
LOAD:00005A50 1F 1C       MOVS            R7, R3  ; R7 = R3 = 0
LOAD:00005A52             ; 84:   v14 = v3;
LOAD:00005A52 05 91       STR             R1, [SP,#0x38+Base64Tableoff1] ; Base64Table + 1
LOAD:00005A54 A4 46       MOV             R12, R4 ; R12 = R4 = pPassword
LOAD:00005A56 26 E0       B               loc_5AA6 ;

如果没看出来,我们可以手动分析,前提是清楚Base64的计算过程,编码过程是3位转4位,还原过程是4位转3位

比如ABCD,以3个字符为一组,计算每个的ASCII十六进制

01000001 01000010 01000011
01000100

连起来

010000010100001001000011
01000100

以三字节为单位切开,这样3个字符就变成了4个字符每组

010000 010100 001001 000011
010001 00

前面补00,最后除了补零,最后的两个不做处理

00010000 00010100 00001001 00000011
00010001 00000000

转为十进制数字

16 20 09 03
17 00

然后到Base64编码Table里寻找对应的下标

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

计算出来,最后没有数据的补上=,以4字节为一组补

QUJD
RA==

我为什么又要写一遍。。。。。。

第一题的理解程度对于后续的解题很重要,所以我们多写点,反正都是我写。。。。。。

入口判断了长度跟3的关系,长度如果不够说明已经计算到结尾,所以进入特殊处理的分支

17.png

接下来手动分析,进入解码前先进行长度的判断

LOAD:00005AA6             loc_5AA6                ;
LOAD:00005AA6 04 99       LDR             R1, [SP,#0x38+var_28] ; R1 = len_Password - 3
LOAD:00005AA8 8B 42       CMP             R3, R1
LOAD:00005AAA D5 DB       BLT             loc_5A58 ;

初始化一个下标

LOAD:00005A58             loc_5A58                ;
LOAD:00005A58 00 21       MOVS            R1, #0  ; R

然后进入计算的循环,以4字节为一组进行循环获取,获取到的4字节每字节进行查表,这个表就是前面初始化的Table

LOAD:00005A5A             loc_5A5A                ;
LOAD:00005A5A 64 46       MOV             R4, R12 ; R4 = R12 = pPassword
LOAD:00005A5C E0 18       ADDS            R0, R4, R3 ; R0 = R4 + R3 = pPassword + i
LOAD:00005A5E 44 5C       LDRB            R4, [R0,R1] ; R4 = Password[i + j],这里以四字节为单位进行循环遍历
LOAD:00005A60 05 98       LDR             R0, [SP,#0x38+Base64Tableoff1] ; R0 = Base64Table + 1
LOAD:00005A62 04 5D       LDRB            R4, [R0,R4] ; R4 = Base64Table[Password[i + j] + 1]
LOAD:00005A64             ; 91:       *(&v21 + v15) = v16;
LOAD:00005A64 07 A8       ADD             R0, SP, #0x38+buffer ; 4字节临时存储
LOAD:00005A66 0C 54       STRB            R4, [R1,R0] ; 循环取字节进行存储
LOAD:00005A68             ; 92:       if ( v16 & 0x80 )
LOAD:00005A68 24 06       LSLS            R4, R4, #0x18
LOAD:00005A6A 01 D5       BPL             loc_5A70

通过一个变量进行判断4字节每组内部取表操作是否完成

LOAD:00005A70             loc_5A70                ;
LOAD:00005A70 01 31       ADDS            R1, #1  ; R1++,4字节每组内部循环
LOAD:00005A72             ; 96:     while ( v15 != 4 );
LOAD:00005A72 04 29       CMP             R1, #4  ; 判断是否读取完成4字节
LOAD:00005A74 F1 D1       BNE             loc_5A5A ;

4字节取表完成后,进行计算

LOAD:00005A76             ; 97:     v13 += 3;
LOAD:00005A76 07 A9       ADD             R1, SP, #0x38+buffer ; R1 = buffer //获取每组4字节数组基址
LOAD:00005A78 48 78       LDRB            R0, [R1,#1] ; R0 = buffer[1]
LOAD:00005A7A 03 37       ADDS            R7, #3  ; R7 = 0 + 3 = 3
LOAD:00005A7C             ; 98:     v11 += 4;
LOAD:00005A7C 04 33       ADDS            R3, #4  ; R3 = pPassword + 4 //定位下一组起始地址,跳4字节
LOAD:00005A7E             ; 99:     v17 = v22;
LOAD:00005A7E 02 90       STR             R0, [SP,#0x38+var_30] ; R0(buffer[1])存储到var_30
LOAD:00005A80             ; 100:     *(_BYTE *)v12 = ((signed int)v22 >> 4) | 4 * v21;
LOAD:00005A80 0C 78       LDRB            R4, [R1] ; R4 = buffer[0]
LOAD:00005A82 00 11       ASRS            R0, R0, #4 ; R0 = buffer[1] >> 4
LOAD:00005A84 A4 00       LSLS            R4, R4, #2 ; R4 = buffer[0] << 2
LOAD:00005A86 20 43       ORRS            R0, R4  ; R0 = R0 | R4 = (buffer[0] << 2) | (buffer[1] >> 4)
LOAD:00005A88 10 70       STRB            R0, [R2] ; new_Password[0] = (buffer[0] << 2) | (buffer[1] >> 4)
LOAD:00005A8A             ; 101:     v18 = v23;
LOAD:00005A8A 8C 78       LDRB            R4, [R1,#2] ; R4 = buffer[2]
LOAD:00005A8C 01 94       STR             R4, [SP,#0x38+var_34] ; 将R4(buffer[2])存储到var_34
LOAD:00005A8E             ; 102:     *((_BYTE *)v12 + 1) = 16 * v17 | ((signed int)v23 >> 2);
LOAD:00005A8E A0 10       ASRS            R0, R4, #2 ; R0 = R4 >> 2
LOAD:00005A90 02 9C       LDR             R4, [SP,#0x38+var_30] ; R4 = buffer[1]
LOAD:00005A92 24 01       LSLS            R4, R4, #4 ; R4 = buffer[1] << 4
LOAD:00005A94 02 94       STR             R4, [SP,#0x38+var_30] ; buffer[1] << 4的结果存储到var_30
LOAD:00005A96 04 43       ORRS            R4, R0  ; R4 = R0 | R4 = (buffer[1] << 4) | (buffer[2] >> 2)
LOAD:00005A98 54 70       STRB            R4, [R2,#1] ; new_Password[1] = (buffer[1] << 4) | (buffer[2] >> 2)
LOAD:00005A9A             ; 103:     *((_BYTE *)v12 + 2) = (v18 << 6) | v24;
LOAD:00005A9A 01 98       LDR             R0, [SP,#0x38+var_34] ; R0 = buffer[2]
LOAD:00005A9C C9 78       LDRB            R1, [R1,#3] ; R1 = buffer[3]
LOAD:00005A9E 84 01       LSLS            R4, R0, #6 ; R4 = buffer[2] << 6
LOAD:00005AA0 0C 43       ORRS            R4, R1  ; R4 = R1 | R4 = (buffer[1] << 6) | buffer[3]
LOAD:00005AA2 94 70       STRB            R4, [R2,#2] ; new_Password[2] = (buffer[1] << 6) | buffer[3]
LOAD:00005AA4             ; 104:     v12 = (char *)v12 + 3;
LOAD:00005AA4 03 32       ADDS            R2, #3  ; 解码后的数据存储偏移加3,结合上面就是每4位计算出来变成3位

关键的三句,这已经是很明显的Base64解码操作了

new_Password[0] = (buffer[0] << 2) | (buffer[1] >> 4)
new_Password[1] = (buffer[1] << 4) | (buffer[2] >> 2)
new_Password[2] = (buffer[1] << 6) | buffer[3]

接着又进行循环操作,解码完成退出循环,进入数据的存储

LOAD:00005AAC             ; 106:   v19 = (void *)operator new[](v13);
LOAD:00005AAC 38 1C       MOVS            R0, R7  ; R0 = R7 = 解码后的密码长度
LOAD:00005AAE FF F7 8E EB BLX             _Znaj   ; R0 = operator new[](strlen(new_pPassword))
LOAD:00005AB2             ; 107:   memmove(v19, v9, v13);
LOAD:00005AB2 31 1C       MOVS            R1, R6  ; src
LOAD:00005AB4 3A 1C       MOVS            R2, R7  ; n
LOAD:00005AB6 04 1C       MOVS            R4, R0  ; R4为新解码后的内存地址
LOAD:00005AB8 FF F7 A0 EB BLX             memmove ; memmove(R0, new_pPassword, R7)
LOAD:00005ABC             ; 108:   if ( v9 )
LOAD:00005ABC 00 2E       CMP             R6, #0
LOAD:00005ABE 02 D0       BEQ             loc_5AC6 ; memmove函数虽然是移动的意思,但是并不是真正的移动
LOAD:00005ABE                                     ; 所以原来的内存还是存在着数据的

清理一下临时空间

LOAD:00005AC0             ; 109:     operator delete[]((void *)v9);
LOAD:00005AC0 30 1C       MOVS            R0, R6  ; 这里进行内存的删除操作
LOAD:00005AC2 FF F7 72 EB BLX             _ZdaPv  ; operator delete[](new_pPassword)

再次存储数据

LOAD:00005AC6             ; 110:   memcpy((void *)(v1 + 60), v19, v13);
LOAD:00005AC6
LOAD:00005AC6             loc_5AC6                ;
LOAD:00005AC6 28 1C       MOVS            R0, R5  ; R5是一个偏移基址的作用
LOAD:00005AC8 3C 30       ADDS            R0, #0x3C ; '<' ; R5+0x3C为最终解码数据的内存地址
LOAD:00005ACA 21 1C       MOVS            R1, R4  ; src
LOAD:00005ACC 3A 1C       MOVS            R2, R7  ; n
LOAD:00005ACE FF F7 84 EB BLX             memcpy  ; 再拷贝解码后的密码到一个结构体里
LOAD:00005AD2             ; 111:   if ( v19 )
LOAD:00005AD2 00 2C       CMP             R4, #0
LOAD:00005AD4 02 D0       BEQ             loc_5ADC

再清理内存

LOAD:00005AD6             ; 112:     operator delete[](v19);
LOAD:00005AD6 20 1C       MOVS            R0, R4  ; void *
LOAD:00005AD8 FF F7 66 EB BLX             _ZdaPv  ; 再次进行内存的清理操作,删除解码后的密码

最后进入一个对比函数

LOAD:00005ADC             ; 113:   sub_548C(v1);
LOAD:00005ADC
LOAD:00005ADC             loc_5ADC
LOAD:00005ADC 28 1C       MOVS            R0, R5
LOAD:00005ADE FF F7 D5 FC BL              sub_548C
LOAD:00005AE2             ; 114:   return 1;
LOAD:00005AE2 01 20       MOVS            R0, #1

sub_548C()将用户名和解码后的数据进行对比

LOAD:0000548C             sub_548C
LOAD:0000548C
LOAD:0000548C             var_1C= -0x1C
LOAD:0000548C
LOAD:0000548C F7 B5       PUSH            {R0-R2,R4-R7,LR}
LOAD:0000548E 41 6B       LDR             R1, [R0,#0x34] ; R1 = pUserName
LOAD:00005490 06 1C       MOVS            R6, R0  ; R6 = R0 = 结构体基址
LOAD:00005492 1A 4D       LDR             R5, =(_GLOBAL_OFFSET_TABLE_ - 0x54AA)
LOAD:00005494 08 1C       MOVS            R0, R1  ; R0 = R1 = pUserName
LOAD:00005496 01 91       STR             R1, [SP,#0x20+var_1C] ; 将pUserName存储到var_1C
LOAD:00005498 FF F7 92 EE BLX             strlen  ; R0 = strlen(pUserName)
LOAD:0000549C 07 1C       MOVS            R7, R0  ; R7 = R0 = 用户名长度
LOAD:0000549E 30 1C       MOVS            R0, R6  ; R0 = R6 = 结构体基址
LOAD:000054A0 3C 30       ADDS            R0, #0x3C ; '<' ; R0 = 解码后的数据,此处命名为pPassDecoded
LOAD:000054A2 FF F7 8E EE BLX             strlen  ; R0 = strlen(pPassDecoded)
LOAD:000054A6 7D 44       ADD             R5, PC ; _GLOBAL_OFFSET_TABLE_ ; R5 = 全局偏移表
LOAD:000054A8 00 24       MOVS            R4, #0  ; R4 = 0
LOAD:000054AA 87 42       CMP             R7, R0  ; R7 = 用户名长度,此处在判断用户名长度是否为0
LOAD:000054AC 1A D0       BEQ             loc_54E4

循环对比

LOAD:000054C0             loc_54C0                ;
LOAD:000054C0 01 99       LDR             R1, [SP,#0x20+var_1C] ; R1 = pUserName
LOAD:000054C2 33 19       ADDS            R3, R6, R4
LOAD:000054C4 3C 33       ADDS            R3, #0x3C ; '<' ; R3 = PassDecoded + i
LOAD:000054C6 0A 5D       LDRB            R2, [R1,R4] ; R2 = UserName
LOAD:000054C8 1B 78       LDRB            R3, [R3] ; R3 = PassDecoded
LOAD:000054CA 9A 42       CMP             R2, R3  ; 对比用户名和解码后的数据
LOAD:000054CC 09 D0       BEQ             loc_54E2 ;
LOAD:000054CC                                     ; 相等继续对比,i++

不相等则异常退出

18.png

所以整个校验逻辑就是,输入用户名以及用户名的Base64编码作为密码即可,编码后的数据需要每3位插入一个-

长度也需要注意范围的校验,所以简单写个Java程序来计算即可,代码写的挫,不贴了

大概就是这样

19.png

NAGA & PIOWIND 2014 APP应用攻防竞赛第二阶段第二题题解

Java层加了不知道是什么的花指令

1.png

其实认真看一下大概还是能看出来的,我闲着无聊给处理了一下

package com.crackme;

import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.view.View$OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.PrintStream;

public class MainActivity extends Activity {
    private Button btn_login;
    private Button btn_reset;
    public static MainActivity m_lpThisBen;
    private EditText txt_name;
    private EditText txt_passwd;
    private TextView txt_result;

    static {
        String v0 = "crackme";
        System.loadLibrary(v0);
    }

    public MainActivity() {
        super();
    }

    static EditText access$0(MainActivity arg2) {
        EditText v0 = arg2.txt_name;
        return v0;
    }

    static EditText access$1(MainActivity arg2) {
        EditText v0 = arg2.txt_passwd;
        return v0;
    }

    static TextView access$2(MainActivity arg2) {
        TextView v0 = arg2.txt_result;
        return v0;
    }

    static String access$3(MainActivity arg2, String arg3, String arg4) {
        String v0 = arg2.crackme(arg3, arg4);
        return v0;
    }

    private native String crackme(String arg1, String arg2) {
    }

    protected void onCreate(Bundle arg4) {
        super.onCreate(arg4);
        int v0 = 2130903040;
        this.setContentView(v0);
        v0 = 2131165184;
        View v0_1 = this.findViewById(v0);
        this.txt_name = ((EditText)v0_1);
        v0 = 2131165185;
        v0_1 = this.findViewById(v0);
        this.txt_passwd = ((EditText)v0_1);
        v0 = 2131165186;
        v0_1 = this.findViewById(v0);
        this.btn_login = ((Button)v0_1);
        v0 = 2131165187;
        v0_1 = this.findViewById(v0);
        this.btn_reset = ((Button)v0_1);
        v0 = 2131165188;
        v0_1 = this.findViewById(v0);
        this.txt_result = ((TextView)v0_1);
        Button v0_2 = this.btn_login;
        com.crackme.MainActivity$1 v1 = new View$OnClickListener() {
            public void onClick(View arg8) {
                TextView v3_6;
                String v4;
                PrintStream v3_5;
                MainActivity v3 = MainActivity.this;
                EditText v3_1 = MainActivity.access$0(v3);
                Editable v3_2 = v3_1.getText();
                String v0 = v3_2.toString();
                v3 = MainActivity.this;
                v3_1 = MainActivity.access$1(v3);
                v3_2 = v3_1.getText();
                String v1 = v3_2.toString();
                String v3_3 = "";
                boolean v3_4 = v3_3.equals(v0);
                if(v3_4) {
                    v3_5 = System.out;
                    v4 = "name is null or \'\'";
                    v3_5.println(v4);
                    v3 = MainActivity.this;
                    v3_6 = MainActivity.access$2(v3);
                    v4 = "账户为空";
                    v3_6.setText(((CharSequence)v4));
                }
                else {
                    v3_3 = "";
                    v3_4 = v3_3.equals(v1);
                    if(v3_4) {
                        v3_5 = System.out;
                        v4 = "passwd is null or \'\'";
                        v3_5.println(v4);
                        v3 = MainActivity.this;
                        v3_6 = MainActivity.access$2(v3);
                        v4 = "密码为空";
                        v3_6.setText(((CharSequence)v4));
                        while(true) {
                            if(98 >= 0) {
                                goto label_152;
                            }
                        }
                    }
                    v3_5 = System.out;
                    String v5 = "name:";
                    StringBuilder v4_1 = new StringBuilder(v5);
                    v4_1 = v4_1.append(v0);
                    v4 = v4_1.toString();
                    v3_5.println(v4);
                    v3_5 = System.out;
                    v5 = "passwd:";
                    v4_1 = new StringBuilder(v5);
                    v4_1 = v4_1.append(v1);
                    v4 = v4_1.toString();
                    v3_5.println(v4);
                    v3_5 = System.out;
                    v4 = "Please treat me gently, you have to go a long way.";
                    v3_5.println(v4);
                    v3 = MainActivity.this;
                    String v2 = MainActivity.access$3(v3, v0, v1);
                    v3 = MainActivity.this;
                    v3_6 = MainActivity.access$2(v3);
                    v3_6.setText(((CharSequence)v2));
                }
            label_152:
            }
        };
        v0_2.setOnClickListener(((View$OnClickListener)v1));
        v0_2 = this.btn_reset;
        com.crackme.MainActivity$2 v1_1 = new View$OnClickListener() {
            public void onClick(View arg4) {
                MainActivity v0 = MainActivity.this;
                EditText v0_1 = MainActivity.access$0(v0);
                String v1 = "";
                v0_1.setText(((CharSequence)v1));
                v0 = MainActivity.this;
                v0_1 = MainActivity.access$1(v0);
                v1 = "";
                v0_1.setText(((CharSequence)v1));
                v0 = MainActivity.this;
                TextView v0_2 = MainActivity.access$2(v0);
                v1 = "";
                v0_2.setText(((CharSequence)v1));
            }
        };
        v0_2.setOnClickListener(((View$OnClickListener)v1_1));
    }
}

和第一题大概是一样的,再看so文件,同样加密了,使用第一题的方法动态调试

2.png

撸回本地

auto fp, dex_addr, end_addr;  
fp = fopen("E:\\libcrackme.so", "wb");  
for(dex_addr = 0xA350E000; dex_addr < 0xA355B000; dex_addr++)
    fputc(Byte(dex_addr), fp);  

但是我们使用IDA打开的时候,发现出错,使用010editor分析,发现文件头没了

3.png

我们找个正常的so文件的文件头拷贝回去,然后再使用IDA打开,发现可以打开了

4.png

我们继续分析,整个程序的结构和第一题是一样的

5.png

新建内存空间

6.png

Base64解码

7.png

但是我们在最后校验的地方发现了不一样的地方,它校验的方式是反过来的

8.png

突然界面就酷炫了起来

那么就比较简单了,注册码就是用户名倒序的Base64编码再插入-

9.png

注册算法

public class MyKeyGen {
    public static void main(String[] args) throws Exception {
        String userName = "wnagzihxain";
        StringBuilder re_userName = new StringBuilder(userName).reverse();
        StringBuilder temp = new StringBuilder(Base64.getBase64(re_userName.toString()));
        System.out.println(temp);
        StringBuilder regCode = new StringBuilder();
        for (int i = 0; i < temp.length(); i++) {
            regCode.append(temp.charAt(i));
            if (temp.charAt(i + 1) == '=') {
                break;
            }
            if ((i + 1) % 3 == 0) {
                regCode.append('-');
            }
        }
        System.out.println(regCode);
    }
}

NAGA & PIOWIND 2014 APP应用攻防竞赛第二阶段第三题题解

一开始用模拟器跑起来就崩溃,我以为是模拟器系统版本的问题,后来看了配置文件,发现没有问题,猜测可能是有反调试,第三题了应该出现反调试了

于是使用调试模式启动应用

先修改android_server的名称和监听端口

root@generic:/data/local/tmp # ./as -p23333

相应的端口转发也要修改

C:\Users\Luyu>adb forward tcp:23946 tcp:23333

然后调试模式启动

adb shell am start -D -n com.crackme/.MainActivity

开启DDMS,选中待调试的应用,前面出现小虫子

然后输入

C:\Users\Luyu>jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
正在初始化jdb...
>

如果出现如下信息,先关掉Android Studio等玩意

C:\Users\Luyu>jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
java.net.ConnectException: Connection refused: connect
        at java.net.DualStackPlainSocketImpl.connect0(Native Method)
        at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
        at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:589)
        at com.sun.tools.jdi.SocketTransportService.attach(SocketTransportService.java:222)
        at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:116)
        at com.sun.tools.jdi.SocketAttachingConnector.attach(SocketAttachingConnector.java:90)
        at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:519)
        at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:328)
        at com.sun.tools.example.debug.tty.Env.init(Env.java:63)
        at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1082)

致命错误:
无法附加到目标 VM。

修改调试选项,然后使用IDA attach

1.png

挂上去后,点击运行,或者F9,断在ELF的入口

2.png

按照前两题的方法dump解密后的so即可

auto fp, dex_addr, end_addr;  
fp = fopen("E:\\libcrackme.so", "wb");  
for(dex_addr = 0xA33AC000; dex_addr < 0xA3406000; dex_addr++)
    fputc(Byte(dex_addr), fp);

文件头依旧是空的,使用正常的ELF文件头覆盖回去,使用IDA打开,找到_Z9fdog_initv,这里实现了反调试

3.png

跟入该函数,该函数创建了很多的子线程进行反调试,我们一个个跟

4.png

这里有一个问题,创建子线程时的函数地址是动态调试时内存的地址,如果不清楚我们看伪代码

5.png

所有子线程的函数参数全是内存地址,有两种方法,第一种是动态调试,但是需要过掉反调试,第二种方法,可以看到我在上面的截图里已经体现出来了,so加载到内存里的地址一般都是0x*****000,所以我们可以根据地址的后三位来搜索函数,而且IDA识别函数后会将函数命名为sub_****,而且有地址的偏移等信息

那么我们来一个个找

第一个函数是_Z13debuggdb_scanv,实现了循环检测

6.png

跟进其中调用的函数,检测的是进程名,分别检测了gdbgdbserverandroid_serverxposed

7.png

第二个函数_Z26file_pro_thread_strengthenv,这个函数我不知道是什么反调试操作,比较猥琐的感觉

8.png

先放着,我找其他师傅再问问

第三个函数_Z18prevent_attach_onev,看起来好像是调用了上了锁的函数的样子

9.png

这个函数读取了进程的/proc/%d/task/%s/stat文件夹

10.png

判断了标志位

11.png

第四个函数_Z18prevent_attach_twov好像也是调用了上了锁的函数

12.png

反调试部分大概就是这样

接下来我们来看校验部分

13.png

进入校验,先初始化内存空间存储用户名和注册码,然后进入校验

14.png

三处-判断,然后进入校验

15.png

变量的初始化

16.png

将解码后的数据前n - 2位进行奇数位和偶数位的交换

17.png

最后正常对比

18.png

最后测试

19.png

注册代码

public class MyKeyGen {
    public static void main(String[] args) throws Exception {
        String userName = "wnagzihxain";
        StringBuilder re_userName = new StringBuilder();
        for (int i = 0; i < userName.length() - 2; i += 2) {
            re_userName.append(userName.charAt(i + 1));
            re_userName.append(userName.charAt(i));
        }
        re_userName.append(userName.charAt(userName.length() - 1));
//        System.out.println(re_userName.toString());
        StringBuilder temp = new StringBuilder(Base64.getBase64(re_userName.toString()));
        System.out.println(temp);
        StringBuilder regCode = new StringBuilder();
        for (int i = 0; i < temp.length(); i++) {
            regCode.append(temp.charAt(i));
            if (temp.charAt(i + 1) == '=') {
                break;
            }
            if ((i + 1) % 3 == 0) {
                regCode.append('-');
            }
        }
        System.out.println(regCode);
    }
}

NAGA & PIOWIND 2014 APP应用攻防竞赛第二阶段第四题题解

Java层比较简单

1.png

查看so,发现加密,依旧dump,IDA调试时未发现有反调试,不过有那么一瞬间看到了inotify,没具体看

auto fp, dex_addr, end_addr;  
fp = fopen("E:\\libcrackme.so", "wb");  
for(dex_addr = 0xA357D000; dex_addr < 0xA35DE000; dex_addr++)
    fputc(Byte(dex_addr), fp);

修复dump后的so文件头,使用IDA打开,关键的依旧是这个函数

2.png

但是我们跟入后,发现壳好像没有脱干净(后来发现其实不是没脱干净)

3.png

再次动态调试脱壳,这次我们找到校验函数,单步跟下去看看具体是什么情况

我们需要先找到校验函数的地址,使用给dvmUseJNIBridge函数下断点的方法

4.png

我们使用调试模式启动应用,IDA挂上去,找到libdvm.sodvmUseJNIBridge函数,下断点

然后把IDA跑起来,在应用界面输入账号密码,点击登录,就可以发现断在这里了,我们注意观察参数,第二个参数就是我们的crackme函数

5.png

跟过去,可以看到确实是校验函数

6.png

找到我们看到是跳转地址的地方

7.png

双击过去

8.png

再次双击过去,发现是关键加解密点了,此时我们记录一下这个地址

9.png

再次dump这个so文件

动静结合,接下来看能力了

10.png

入口的数据初始化,然后调用_Unwind_GetCFAB,这和前几题是类似的

11.png

跟入,开始做了一些参数的存储操作,然后存储了_Unwind_GetCFAB函数的指针到栈中

LOAD:0002DB28 STMFD           SP!, {R4,R11,LR}
LOAD:0002DB2C ADD             R11, SP, #8
LOAD:0002DB30 SUB             SP, SP, #0x24
LOAD:0002DB34 LDR             R4, =(off_46CC8 - 0x2DB40)
LOAD:0002DB38 ADD             R4, PC, R4 ; off_46CC8 ; 神奇的地址,暂时不知道干什么的
LOAD:0002DB3C STR             R0, [R11,#var_20] ; R0 = var_20 = szUserName
LOAD:0002DB40 STR             R1, [R11,#var_24] ; R1 = var_24 = szRegCode
LOAD:0002DB44 MOV             R3, #0  ; R3 = 0
LOAD:0002DB48 STR             R3, [R11,#var_18] ; R3 = var_18 = 0
LOAD:0002DB4C LDR             R3, =dword_2CC ; R3 = 0x2CC
LOAD:0002DB50 LDR             R3, [R4,R3]
LOAD:0002DB54 LDR             R3, [R3]
LOAD:0002DB58 STR             R3, [R11,#var_14] ; var_14为_Unwind_GetCFAB函数指针

一开始并没有看出来,所以使用了动态调试来确定

12.png

接下来的操作是为了调用tdog_decrypt而做参数的计算

LOAD:0002DB5C LDR             R3, =dword_2CC ; R3 = 0x2CC
LOAD:0002DB60 LDR             R3, [R4,R3]
LOAD:0002DB64 LDR             R3, [R3] ; R3 = _Unwind_GetCFAB
LOAD:0002DB68 ADD             R3, R3, #0x14 ; R3 = _Unwind_GetCFAB + 0x14
LOAD:0002DB6C MOV             R1, R3  ; R1 = R3 = R3 = _Unwind_GetCFAB + 0x14
LOAD:0002DB70 LDR             R3, =dword_2B0 ; R3 = 0x2B0
LOAD:0002DB74 LDR             R3, [R4,R3]
LOAD:0002DB78 LDR             R3, [R3] ; 动调 : R3 = 0x104
LOAD:0002DB7C SUB             R2, R3, #0x14 ; R2 = 0xF0
LOAD:0002DB80 LDR             R3, =dword_2CC ; R3 = 0x2CC
LOAD:0002DB84 LDR             R3, [R4,R3]
LOAD:0002DB88 LDR             R3, [R3] ; R3 = _Unwind_GetCFAB
LOAD:0002DB8C ADD             R3, R3, #0x14 ; R3 = _Unwind_GetCFAB + 0x14
LOAD:0002DB90 LDR             R0, =dword_1D4 ; R0 = 0x1D4
LOAD:0002DB94 LDR             R0, [R4,R0]
LOAD:0002DB98 LDR             R0, [R0]
LOAD:0002DB9C STR             R0, [SP,#0x2C+var_2C] ; var_2C = abs_export_function_key
LOAD:0002DBA0 MOV             R0, R1  ; R0 = _Unwind_GetCFAB + 0x14
LOAD:0002DBA4 MOV             R1, R2  ; R1 = R2 = 0xF0
LOAD:0002DBA8 MOV             R2, R3  ; R3 = _Unwind_GetCFAB + 0x14
LOAD:0002DBAC LDR             R3, =dword_314 ; R3 = 0x314
LOAD:0002DBB0 LDR             R3, [R4,R3] ; 指向内存
LOAD:0002DBB4 BL              tdog_decrypt

看名字就可以猜到这个函数很重要了

tdog_decrypt(_Unwind_GetCFAB + 0x14, 0xF0, _Unwind_GetCFAB + 0x14, 某内存变量指针)

双击跟入,发现其调用了一个XorArray()函数

LOAD:000387F8 STMFD           SP!, {R11,LR}
LOAD:000387FC ADD             R11, SP, #4
LOAD:00038800 SUB             SP, SP, #0x10
LOAD:00038804 ; 5:   v5 = a2;
LOAD:00038804 STR             R0, [R11,#var_8] ; R0 = var_8 = _Unwind_GetCFAB + 0x14
LOAD:00038808 STR             R1, [R11,#var_C] ; R1 = var_C = 0xF0
LOAD:0003880C ; 6:   v6 = a4;
LOAD:0003880C STR             R2, [R11,#var_10] ; R2 = var_10 = _Unwind_GetCFAB + 0x14
LOAD:00038810 STR             R3, [R11,#var_14] ; R3 = var_14 = 第四个参数,为一个指针
LOAD:00038814 ; 7:   XorArray(a5, a1, a1, a2);
LOAD:00038814 LDR             R2, [R11,#var_8] ; R2 = _Unwind_GetCFAB + 0x14
LOAD:00038818 LDR             R3, [R11,#var_8] ; R3 = _Unwind_GetCFAB + 0x14
LOAD:0003881C LDR             R0, [R11,#arg_0] ; 动调 : R0 = 0x5F7C8B38
LOAD:00038820 MOV             R1, R2  ; R1 = R2 = _Unwind_GetCFAB + 0x14
LOAD:00038824 MOV             R2, R3  ; R2 = R3 = _Unwind_GetCFAB + 0x14
LOAD:00038828 LDR             R3, [R11,#var_C] ; R3 = 0xF0
LOAD:0003882C BL              _Z8XorArrayjPhS_j ; XorArray(uint,uchar *,uchar *,uint)

XorArray函数里有一个PolyXorKey函数,用于生成秘钥,这个函数在后续的娜迦壳里面是一个比较重要的特征,后续的类抽取技术里就有用到这个函数进行秘钥的计算

我一直觉得这里加了junk code,前面有些指令反复做同样的操作时我就感觉出来了,但是加的junk code并不是很多,比如这该函数的第一个函数块后面的几句

LOAD:00038D7C STMFD           SP!, {R11,LR}
LOAD:00038D80 ADD             R11, SP, #4
LOAD:00038D84 SUB             SP, SP, #0x20
LOAD:00038D88 ; 10:   v6 = a2;
LOAD:00038D88 STR             R0, [R11,#var_18] ; var_18 = 0X5F7C8B38 //神秘变量
LOAD:00038D8C STR             R1, [R11,#var_1C] ; var_1C = _Unwind_GetCFAB + 0x14
LOAD:00038D90 ; 11:   v5 = a3;
LOAD:00038D90 STR             R2, [R11,#var_20] ; var_20 = _Unwind_GetCFAB + 0x14
LOAD:00038D94 ; 12:   v4 = a4;
LOAD:00038D94 STR             R3, [R11,#var_24] ; var_24 = 0xF0
LOAD:00038D98 ; 13:   v7 = result;
LOAD:00038D98 LDR             R3, [R11,#var_18] ; R3 = 0X5F7C8B38 //神秘,神秘
LOAD:00038D9C STR             R3, [R11,#var_14] ; var_14 = 0X5F7C8B38 //又是神秘
LOAD:00038DA0 ; 14:   v8 = &v7;
LOAD:00038DA0 SUB             R3, R11, #-var_14
LOAD:00038DA4 STR             R3, [R11,#var_10] ; R3存储的是神秘变量0X5F7C8B38的指针
LOAD:00038DA8 ; 15:   v10 = 0;
LOAD:00038DA8 MOV             R3, #0  ; R3 = 0
LOAD:00038DAC STR             R3, [R11,#var_C] ; var_C = 0
LOAD:00038DB0 MOV             R3, #0  ; R3 = 0
LOAD:00038DB4 STR             R3, [R11,#var_8] ; var_8 = 0
LOAD:00038DB8 ; 16:   for ( i = 0; v4 > i; ++i )
LOAD:00038DB8 MOV             R3, #0  ; R3 = 0
LOAD:00038DBC STR             R3, [R11,#var_C] ; R3 = 0
LOAD:00038DC0 B               loc_38E40

开始进入循环

LOAD:00038E40 loc_38E40               ;
LOAD:00038E40 LDR             R2, [R11,#var_24] ; R2 = 0xF0 = 240
LOAD:00038E44 LDR             R3, [R11,#var_C] ; R3 = i
LOAD:00038E48 CMP             R2, R3
LOAD:00038E4C MOVLE           R3, #0
LOAD:00038E50 MOVGT           R3, #1  ; 循环中使用这一句 : i < 240 --- > R3 = 1
LOAD:00038E54 AND             R3, R3, #0xFF ; 用于判断是否到达退出条件
LOAD:00038E58 CMP             R3, #0
LOAD:00038E5C BNE             loc_38DC4

两个基址获取字节数据,进行异或操作,异或后的数据,存在_Unwind_GetCFAB + 0x14 + i指向的字节

LOAD:00038DC4 ; 19:     v5 = v6 ^ *((_BYTE *)v8 + v10);
LOAD:00038DC4 loc_38DC4               ;
LOAD:00038DC4 LDR             R3, [R11,#var_C] ; R3 = i
LOAD:00038DC8 LDR             R2, [R11,#var_20] ; R2 = _Unwind_GetCFAB + 0x14
LOAD:00038DCC ADD             R3, R2, R3 ; R3 = _Unwind_GetCFAB + 0x14 + i
LOAD:00038DD0 LDR             R2, [R11,#var_C] ; R2 = i
LOAD:00038DD4 LDR             R1, [R11,#var_1C] ; R1 = _Unwind_GetCFAB + 0x14
LOAD:00038DD8 ADD             R2, R1, R2 ; R2 = _Unwind_GetCFAB + 0x14 + i
LOAD:00038DDC ; 18:     result = (int)v8;
LOAD:00038DDC LDRB            R1, [R2] ; 取第一个字节,动调 : R1 = 0xBB
LOAD:00038DE0 LDR             R2, [R11,#var_8] ; R2 = j
LOAD:00038DE4 LDR             R0, [R11,#var_10] ; R0为神秘变量指针
LOAD:00038DE8 ADD             R2, R0, R2 ; R2 = 神秘变量指针 + j
LOAD:00038DEC LDRB            R2, [R2] ; 取第一个字节,动调 : 0x38
LOAD:00038DF0 EOR             R2, R1, R2 ; 两个取出来的字节异或
LOAD:00038DF4 AND             R2, R2, #0xFF
LOAD:00038DF8 STRB            R2, [R3]
LOAD:00038DFC ; 20:     if ( v10 == 3 )
LOAD:00038DFC LDR             R3, [R11,#var_8]
LOAD:00038E00 CMP             R3, #3
LOAD:00038E04 BNE             loc_38E28

这里是在计算一个四字节的数据

13.png

我们在内存中跟随,可以看到这四个字节的数据已经修改成了83 93 00 23,不清楚的同学可以在异或的地方下个断点循环调试看看

接着是调用PolyXorKey,参数是神秘变量自身

LOAD:00038E08 ; 22:       result = PolyXorKey(v7);
LOAD:00038E08 LDR             R3, [R11,#var_14] ; 取出神秘变量
LOAD:00038E0C MOV             R0, R3  ; 神秘变量作为参数R0
LOAD:00038E10 BL              _Z10PolyXorKeyj ; PolyXorKey(

先使用异或操作对神秘变量进行修改

LOAD:00038BC8 STR             R11, [SP,#-4+var_s0]!
LOAD:00038BCC ADD             R11, SP, #0
LOAD:00038BD0 SUB             SP, SP, #0x24
LOAD:00038BD4 ; 11:   v4 = 0;
LOAD:00038BD4 STR             R0, [R11,#var_20] ; var_20 = 神秘变量
LOAD:00038BD8 MOV             R3, #0  ; R3 = 0
LOAD:00038BDC STR             R3, [R11,#var_18] ; var_18 = 0
LOAD:00038BE0 MOV             R3, #0  ; R3 = 0
LOAD:00038BE4 STR             R3, [R11,#var_14] ; var_14 = 0
LOAD:00038BE8 ; 12:   v5 = 0;
LOAD:00038BE8 MOV             R3, #0  ; R3 = 0
LOAD:00038BEC STR             R3, [R11,#var_10] ; var_10 = 0
LOAD:00038BF0 ; 13:   v6 = (char *)&v2;
LOAD:00038BF0 SUB             R3, R11, #-var_20
LOAD:00038BF4 STR             R3, [R11,#var_C] ; var_C为神秘变量指针
LOAD:00038BF8 ; 14:   v7 = 0;
LOAD:00038BF8 MOV             R3, #0  ; R3 = 0
LOAD:00038BFC STRB            R3, [R11,#var_7] ; var_7指向的byte为0
LOAD:00038C00 ; 15:   v8 = 0;
LOAD:00038C00 MOV             R3, #0
LOAD:00038C04 STRB            R3, [R11,#var_6] ; var_6指向的byte为0
LOAD:00038C08 ; 16:   v9 = 0;
LOAD:00038C08 MOV             R3, #0
LOAD:00038C0C STRB            R3, [R11,#var_5] ; var_5指向的byte为0
LOAD:00038C10 ; 17:   v2 = a1 ^ 0xDF138530;
LOAD:00038C10 LDR             R3, [R11,#var_20] ; R3 = 神秘变量
LOAD:00038C14 MOV             R2, R3  ; R2 = R3 = 神秘变量
LOAD:00038C18 LDR             R3, =0xDF138530 ; R3 = 0xDF138530
LOAD:00038C1C EOR             R3, R2, R3 ; 神秘变量异或 ---> R3 = 0x5F7C8B38 ^ 0xDF138530
LOAD:00038C20 STR             R3, [R11,#var_20] ; 修改神秘变量为0x806F0E08
LOAD:00038C24 ; 18:   v3 = 0;
LOAD:00038C24 MOV             R3, #0  ; R3 = 0
LOAD:00038C28 STR             R3, [R11,#var_18] ; var_18 = 0
LOAD:00038C2C B               loc_38D48

进入大循环,整个大循环就是循环计算神秘变量的四个字节,但是内部又有很多的循环计算

LOAD:00038D48 ; 19:   while ( v3 <= 3 )
LOAD:00038D48 loc_38D48               ; 开始循环计算
LOAD:00038D48 LDR             R3, [R11,#var_18] ; R3 = i
LOAD:00038D4C CMP             R3, #3  ; 条件判断 i < 4
LOAD:00038D50 MOVGT           R3, #0
LOAD:00038D54 MOVLE           R3, #1
LOAD:00038D58 AND             R3, R3, #0xFF
LOAD:00038D5C CMP             R3, #0
LOAD:00038D60 BNE             loc_38C30

取字节,这里的var_C会在后面自加一

LOAD:00038C30 ; 21:     v7 = *v6;
LOAD:00038C30 loc_38C30               ;
LOAD:00038C30 LDR             R3, [R11,#var_C] ; R3为神秘变量指针
LOAD:00038C34 LDRB            R3, [R3] ; 获取计算后的神秘变量的字节
LOAD:00038C38 STRB            R3, [R11,#var_7] ; var_7 = 08
LOAD:00038C3C ; 22:     v4 = 128;
LOAD:00038C3C MOV             R3, #0x80 ; '?' ; R3 = 0x80
LOAD:00038C40 STR             R3, [R11,#var_14] ; var_14 = 0x80
LOAD:00038C44 ; 23:     v5 = 7;
LOAD:00038C44 MOV             R3, #7  ; R3 = 0x7
LOAD:00038C48 STR             R3, [R11,#var_10] ; var_10 = 0x7
LOAD:00038C4C B               loc_38CE0

内部的循环

LOAD:00038CE0 ; 24:     while ( v4 > 1 )
LOAD:00038CE0
LOAD:00038CE0 loc_38CE0               ;
LOAD:00038CE0 LDR             R3, [R11,#var_14] ; R3 = 0x80
LOAD:00038CE4 CMP             R3, #1
LOAD:00038CE8 MOVLE           R3, #0
LOAD:00038CEC MOVGT           R3, #1
LOAD:00038CF0 AND             R3, R3, #0xFF
LOAD:00038CF4 CMP             R3, #0
LOAD:00038CF8 BNE             loc_38C50

接下来的循环计算可以还原出C代码,但是具体是什么数学算法之类的就不是很清楚了,可能只是个计算,这个函数最终的功能目测应该是计算一个四字节的数据作为返回值

LOAD:00038C50 ; 27:       v8 = ((signed int)(unsigned __int8)(v7 & v4) >> v5) ^ v9;
LOAD:00038C50 loc_38C50               ; 进入循环,重命名神秘变量为sec
LOAD:00038C50 LDRB            R2, [R11,#var_7] ; R2 = 0x08
LOAD:00038C54 LDR             R3, [R11,#var_14] ; R3 = 0x80
LOAD:00038C58 AND             R2, R2, R3 ; R2 = sec & 0x80
LOAD:00038C5C LDR             R3, [R11,#var_10] ; R3 = 0x07
LOAD:00038C60 MOV             R3, R2,ASR R3 ; R3 = (sec & 0x80) / 0x07
LOAD:00038C64 ; 26:       v9 = (v7 & v4 / 2) >> (v5 - 1);
LOAD:00038C64 STRB            R3, [R11,#var_6] ; var_6 = (sec & 0x80) / 0x07
LOAD:00038C68 LDRB            R2, [R11,#var_7] ; R2 = sec
LOAD:00038C6C LDR             R3, [R11,#var_14] ; R3 = 0x80
LOAD:00038C70 MOV             R1, R3,LSR#31 ; R1 = 0x00000080 >> 31 = 0
LOAD:00038C74 ADD             R3, R1, R3 ; R3 = 0x80
LOAD:00038C78 MOV             R3, R3,ASR#1 ; R3 = 0x80 / 2
LOAD:00038C7C AND             R2, R2, R3 ; R2 = sec & (0x80 / 2)
LOAD:00038C80 LDR             R3, [R11,#var_10] ; R3 = 0x07
LOAD:00038C84 SUB             R3, R3, #1 ; R3 = 0x07 - 1
LOAD:00038C88 MOV             R3, R2,ASR R3 ; R3 = (sec & (0x80 / 2)) / (0x07 - 1)
LOAD:00038C8C STRB            R3, [R11,#var_5] ; var_5存储计算后的结果
LOAD:00038C90 LDRB            R2, [R11,#var_6] ; R2 = (sec & 0x80) / 0x07
LOAD:00038C94 LDRB            R3, [R11,#var_5] ; R3 = (sec & (0x80 / 2)) / (0x07 - 1)
LOAD:00038C98 EOR             R3, R2, R3 ; 上面两个进行异或,存储到R3
LOAD:00038C9C STRB            R3, [R11,#var_6] ; 异或结果存储到var_6
LOAD:00038CA0 ; 28:       v8 <<= v5;
LOAD:00038CA0 LDRB            R2, [R11,#var_6] ; R2 = 异或结果
LOAD:00038CA4 LDR             R3, [R11,#var_10] ; R3 = 0x07
LOAD:00038CA8 MOV             R3, R2,LSL R3 ; R3 = 异或结果 << 0x07
LOAD:00038CAC STRB            R3, [R11,#var_6] ; var_6 = 异或结果 << 0x07
LOAD:00038CB0 ; 29:       v7 |= v8;
LOAD:00038CB0 LDRB            R2, [R11,#var_7] ; R2 = sec
LOAD:00038CB4 LDRB            R3, [R11,#var_6] ; R3 = 异或结果 << 0x07
LOAD:00038CB8 ORR             R3, R2, R3 ; R3 = sec | (异或结果 << 0x07)
LOAD:00038CBC STRB            R3, [R11,#var_7] ; var_7 = sec | (异或结果 << 0x07)
LOAD:00038CC0 ; 30:       v4 /= 2;
LOAD:00038CC0 LDR             R3, [R11,#var_14] ; R3 = 0x80
LOAD:00038CC4 MOV             R2, R3,LSR#31 ; R2 = 0
LOAD:00038CC8 ADD             R3, R2, R3 ; R3 = 0x80
LOAD:00038CCC MOV             R3, R3,ASR#1 ; R3 = 0x80 / 2
LOAD:00038CD0 STR             R3, [R11,#var_14] ; var_14 = 0x80 / 2 //这里应该是这个变量循环除2
LOAD:00038CD4 ; 31:       --v5;
LOAD:00038CD4 LDR             R3, [R11,#var_10] ; 0x07-- //这里也是这个变量循环自减一结果作为下次循环的值
LOAD:00038CD8 SUB             R3, R3, #1
LOAD:00038CDC STR             R3, [R11,#var_10]

直接在最后面下个断点跑完这个函数,可以看到返回值是0x80FF1E18

14.png

这是整个大循环

15.png

回到上一层函数,这个值应该是固定的,暂时没有看到有其它参数对这个计算过程造成了影响

一边分析一边写的,估计有些地方会分析错

这个函数整个大循环是0xF0次,也就是240次,我们来验证一下PolyXorKey函数是否每次都是生成一样的数据

0xFFFCBF78
0x60FF7ED8
0xFFFCFFF8
0x60FFFED8
0xFFFCFFF8
0x60FFFED8
0xFFFCFFF8
0x60FFFED8
0xFFFCFFF8
..........
0xFFFCFFF8

开始变成两个常数的交替出现,难道是动态调试出问题了

先放一边好了

这里非常绕,跟了好几次都没有找到关键的地方,后来半猜半想,根据调用operator new[]()的函数往回找,找到了和前几题一样的函数,虽然这里算法不一样,但是对于用户名和注册码的存储还是一样的

16.png

接下来是校验的地方,单步走一遍先,找到关键的地方,可以看到这里调用了四个函数

17.png

但是在静态时这位置我是手动找的,这个费劲,有的函数没有识别出来,红色的。。。。。。

18.png

其实还有非常多的函数未识别出来,不过并不是很重要

由于前面没有完整的跟过来,所以这里的一些偏移需要根据动态调试确定指向的数据是什么

那么0x34偏移指向的就是用户名

19.png

并且有长度的限制,用户名长度应该在[8, 24]之间

LOAD:00005E66 MOVS            R0, R3  ; s
LOAD:00005E68 BLX             strlen  ; 获取用户名长度
LOAD:00005E6C MOVS            R6, R0  ; R6 = R0 = 用户名长度
LOAD:00005E6E SUBS            R6, #8  ; R6 = strlen(UserName) - 8
LOAD:00005E70 MOVS            R0, R5  ; s
LOAD:00005E72 BLX             strlen  ; 获取注册码长度
LOAD:00005E76 CMP             R6, #0x16 ; 对比strlen(UserName) - 8和0x16
LOAD:00005E78 BHI             loc_5E80

偏移0x38指向的是注册码,注册码长度需要在[12, 100]之间

LOAD:00005E7A SUBS            R0, #0xC
LOAD:00005E7C CMP             R0, #0x58 ; 'X' ; 对比strlen(RegCode) - 0xC和0x58
LOAD:00005E7E BLS             loc_5E94

第二个函数比较长

20.png

获取用户名

LOAD:00005D48 PUSH            {LR}
LOAD:00005D4A SUB             SP, SP, #0x2C
LOAD:00005D4C STR             R0, [SP,#0x30+var_20] ; 结构体基址
LOAD:00005D4E LDR             R0, [R0,#0x34] ; 获取用户名
LOAD:00005D50 BLX             strlen  ; R0 = 用户名长度
LOAD:00005D54 CMP             R0, #7  ; 用户名长度与7进行对比
LOAD:00005D56 BGT             loc_5D6E ; 用户名长度需要大于7 ---> strlen(UserMame) >= 8

存储一下中间变量

LOAD:00005D84 loc_5D84
LOAD:00005D84 LDR             R3, =(dword_16AF8 - 0x5D8A)
LOAD:00005D86 ADD             R3, PC ; dword_16AF8
LOAD:00005D88 ADDS            R3, #0x30 ; '0' ; env
LOAD:00005D8A STR             R3, [SP,#0x30+env] ; 将env变量存储到栈中
LOAD:00005D8C STR             R3, [SP,#0x30+var_C] ; var_C = env

进入一个0x08 * 0x100次的循环,循环获取用户名的前八位数据

LOAD:00005D9E loc_5D9E                ;
LOAD:00005D9E LDR             R1, [SP,#0x30+var_20] ; R1为结构体基址
LOAD:00005DA0 LDR             R2, [SP,#0x30+var_28] ; R2 = 0
LOAD:00005DA2 LDR             R3, [R1,#0x34] ; R3 = pUserName
LOAD:00005DA4 LDRB            R3, [R3,R2] ; 循环取用户名的字节数据
LOAD:00005DA6 STR             R3, [SP,#0x30+var_2C] ; 获取的数据暂存栈中

获取一个关键偏移

LOAD:00005DA8 loc_5DA8                ;
LOAD:00005DA8 MOVS            R3, #0  ; R3 = 0
LOAD:00005DAA STR             R3, [SP,#0x30+var_24] ; var_24 = 0
LOAD:00005DAC LDR             R3, =(dword_16AF8 - 0x5DB4)
LOAD:00005DAE LDR             R1, =0x104B2 ; R1 = 0x104B2
LOAD:00005DB0 ADD             R3, PC ; dword_16AF8 ; 定位结构体基址
LOAD:00005DB2 ADDS            R3, #0x30 ; '0' ; env
LOAD:00005DB4 STR             R3, [SP,#0x30+var_14] ; var_14 = env
LOAD:00005DB6 STR             R1, [SP,#0x30+var_10] ; var_10 = 0x104B2

这个偏移在这里的作用是重定位一个Table,通过和用户名相同的偏移来进行数据获取,然后两者异或

LOAD:00005DC8 loc_5DC8                ;
LOAD:00005DC8 LDR             R3, [SP,#0x30+var_10] ; R3 = 0x104B2
LOAD:00005DCA LDR             R2, [SP,#0x30+var_24] ; R2 = i
LOAD:00005DCC LDR             R1, [SP,#0x30+var_2C] ; R1 = UserName
LOAD:00005DCE ADD             R3, PC  ; 动调 : R3 = 0xA33E8284
LOAD:00005DD0 ADDS            R3, #0x38 ; '8'
LOAD:00005DD2 LDRB            R3, [R2,R3] ; 同一偏移取某地址字节数据
LOAD:00005DD4 EORS            R3, R1  ; 两个地址同偏移数据异或
LOAD:00005DD6 LSLS            R3, R3, #0x18 ; 这两句效果等效&0xFF
LOAD:00005DD8 LSRS            R3, R3, #0x18
LOAD:00005DDA STR             R3, [SP,#0x30+var_2C]

大概就是

var_2C = ((byte) UserName ^ (byte) Table) & 0xFF

最后进行次数的判断

LOAD:00005DDC loc_5DDC                ;
LOAD:00005DDC LDR             R2, [SP,#0x30+var_24] ; var_24 = i
LOAD:00005DDE MOVS            R3, #0x100 ; R3 = 0x100
LOAD:00005DE2 ADDS            R2, #1  ; i++
LOAD:00005DE4 STR             R2, [SP,#0x30+var_24]
LOAD:00005DE6 CMP             R2, R3  ; i < 0x100 //循环0x100次
LOAD:00005DE8 BNE             loc_5DB8

每个字节一共是0x100次,动态调试把整个表dump出来

1A B7 00 3A 19 B7 00 2A  20 00 9D E5 C7 97 00 AA
C6 97 00 BA 91 03 03 E0  C8 3D 00 1A C7 3D 00 0A
48 60 9D E5 03 C2 00 CA  02 C2 00 DA 3C 40 8D E5
B4 3F 00 2A B3 3F 00 3A  E7 76 27 E2 F0 31 00 6A
EF 31 00 7A DA 2C 4C E2  AE A2 00 1A AD A2 00 0A
0A 6B 86 E2 04 C1 00 4A  03 C1 00 5A B4 20 9D E5
B2 18 00 AA B1 18 00 BA  06 30 8A E0 83 0C 00 0A
82 0C 00 1A 0A 80 88 E0  FA BE 00 3A F9 BE 00 2A
0C 10 21 E0 4A 9D 00 0A  49 9D 00 1A 21 5A 8F E2
DE 5E 85 E2 00 50 95 E5  63 84 00 AA 62 84 00 BA
D8 70 9D E5 E6 09 00 CA  E5 09 00 DA 91 02 02 E0
88 6B 00 6A 87 6B 00 7A  02 20 86 E0 DE 10 00 2A
DD 10 00 3A 87 3C 83 E2  31 C2 00 9A 30 C2 00 8A
9C B0 9D E5 EE A4 00 9A  ED A4 00 8A DD 06 00 1A
8C 8A 00 3A 8B 8A 00 2A  71 25 82 E2 F0 0C 00 6A
EF 0C 00 7A FA 19 21 E2  65 AF 00 6A 64 AF 00 7A

补充一点,这个表其实不是动态生成的,静态分析时就可以dump出来

21.png

因为异或的计算比较有意思,整个表循环异或一遍其实可以等效于异或一个值,这个值我们可以通过计算来确定,输入为0x00,看输出是什么即可

在IDA里将这个表保存为文件,使用WinHex打开,拷贝存为C Source

unsigned AnsiChar data[256] = {
        0x1A, 0xB7, 0x00, 0x3A, 0x19, 0xB7, 0x00, 0x2A, 0x20, 0x00, 0x9D, 0xE5, 0xC7, 0x97, 0x00, 0xAA, 
        0xC6, 0x97, 0x00, 0xBA, 0x91, 0x03, 0x03, 0xE0, 0xC8, 0x3D, 0x00, 0x1A, 0xC7, 0x3D, 0x00, 0x0A, 
        0x48, 0x60, 0x9D, 0xE5, 0x03, 0xC2, 0x00, 0xCA, 0x02, 0xC2, 0x00, 0xDA, 0x3C, 0x40, 0x8D, 0xE5, 
        0xB4, 0x3F, 0x00, 0x2A, 0xB3, 0x3F, 0x00, 0x3A, 0xE7, 0x76, 0x27, 0xE2, 0xF0, 0x31, 0x00, 0x6A, 
        0xEF, 0x31, 0x00, 0x7A, 0xDA, 0x2C, 0x4C, 0xE2, 0xAE, 0xA2, 0x00, 0x1A, 0xAD, 0xA2, 0x00, 0x0A, 
        0x0A, 0x6B, 0x86, 0xE2, 0x04, 0xC1, 0x00, 0x4A, 0x03, 0xC1, 0x00, 0x5A, 0xB4, 0x20, 0x9D, 0xE5, 
        0xB2, 0x18, 0x00, 0xAA, 0xB1, 0x18, 0x00, 0xBA, 0x06, 0x30, 0x8A, 0xE0, 0x83, 0x0C, 0x00, 0x0A, 
        0x82, 0x0C, 0x00, 0x1A, 0x0A, 0x80, 0x88, 0xE0, 0xFA, 0xBE, 0x00, 0x3A, 0xF9, 0xBE, 0x00, 0x2A, 
        0x0C, 0x10, 0x21, 0xE0, 0x4A, 0x9D, 0x00, 0x0A, 0x49, 0x9D, 0x00, 0x1A, 0x21, 0x5A, 0x8F, 0xE2, 
        0xDE, 0x5E, 0x85, 0xE2, 0x00, 0x50, 0x95, 0xE5, 0x63, 0x84, 0x00, 0xAA, 0x62, 0x84, 0x00, 0xBA, 
        0xD8, 0x70, 0x9D, 0xE5, 0xE6, 0x09, 0x00, 0xCA, 0xE5, 0x09, 0x00, 0xDA, 0x91, 0x02, 0x02, 0xE0, 
        0x88, 0x6B, 0x00, 0x6A, 0x87, 0x6B, 0x00, 0x7A, 0x02, 0x20, 0x86, 0xE0, 0xDE, 0x10, 0x00, 0x2A, 
        0xDD, 0x10, 0x00, 0x3A, 0x87, 0x3C, 0x83, 0xE2, 0x31, 0xC2, 0x00, 0x9A, 0x30, 0xC2, 0x00, 0x8A, 
        0x9C, 0xB0, 0x9D, 0xE5, 0xEE, 0xA4, 0x00, 0x9A, 0xED, 0xA4, 0x00, 0x8A, 0xDD, 0x06, 0x00, 0x1A, 
        0x8C, 0x8A, 0x00, 0x3A, 0x8B, 0x8A, 0x00, 0x2A, 0x71, 0x25, 0x82, 0xE2, 0xF0, 0x0C, 0x00, 0x6A, 
        0xEF, 0x0C, 0x00, 0x7A, 0xFA, 0x19, 0x21, 0xE2, 0x65, 0xAF, 0x00, 0x6A, 0x64, 0xAF, 0x00, 0x7A
};

写个程序跑一下

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

unsigned char xor_table[256] = {
        0x1A, 0xB7, 0x00, 0x3A, 0x19, 0xB7, 0x00, 0x2A, 0x20, 0x00, 0x9D, 0xE5, 0xC7, 0x97, 0x00, 0xAA,
        0xC6, 0x97, 0x00, 0xBA, 0x91, 0x03, 0x03, 0xE0, 0xC8, 0x3D, 0x00, 0x1A, 0xC7, 0x3D, 0x00, 0x0A,
        0x48, 0x60, 0x9D, 0xE5, 0x03, 0xC2, 0x00, 0xCA, 0x02, 0xC2, 0x00, 0xDA, 0x3C, 0x40, 0x8D, 0xE5,
        0xB4, 0x3F, 0x00, 0x2A, 0xB3, 0x3F, 0x00, 0x3A, 0xE7, 0x76, 0x27, 0xE2, 0xF0, 0x31, 0x00, 0x6A,
        0xEF, 0x31, 0x00, 0x7A, 0xDA, 0x2C, 0x4C, 0xE2, 0xAE, 0xA2, 0x00, 0x1A, 0xAD, 0xA2, 0x00, 0x0A,
        0x0A, 0x6B, 0x86, 0xE2, 0x04, 0xC1, 0x00, 0x4A, 0x03, 0xC1, 0x00, 0x5A, 0xB4, 0x20, 0x9D, 0xE5,
        0xB2, 0x18, 0x00, 0xAA, 0xB1, 0x18, 0x00, 0xBA, 0x06, 0x30, 0x8A, 0xE0, 0x83, 0x0C, 0x00, 0x0A,
        0x82, 0x0C, 0x00, 0x1A, 0x0A, 0x80, 0x88, 0xE0, 0xFA, 0xBE, 0x00, 0x3A, 0xF9, 0xBE, 0x00, 0x2A,
        0x0C, 0x10, 0x21, 0xE0, 0x4A, 0x9D, 0x00, 0x0A, 0x49, 0x9D, 0x00, 0x1A, 0x21, 0x5A, 0x8F, 0xE2,
        0xDE, 0x5E, 0x85, 0xE2, 0x00, 0x50, 0x95, 0xE5, 0x63, 0x84, 0x00, 0xAA, 0x62, 0x84, 0x00, 0xBA,
        0xD8, 0x70, 0x9D, 0xE5, 0xE6, 0x09, 0x00, 0xCA, 0xE5, 0x09, 0x00, 0xDA, 0x91, 0x02, 0x02, 0xE0,
        0x88, 0x6B, 0x00, 0x6A, 0x87, 0x6B, 0x00, 0x7A, 0x02, 0x20, 0x86, 0xE0, 0xDE, 0x10, 0x00, 0x2A,
        0xDD, 0x10, 0x00, 0x3A, 0x87, 0x3C, 0x83, 0xE2, 0x31, 0xC2, 0x00, 0x9A, 0x30, 0xC2, 0x00, 0x8A,
        0x9C, 0xB0, 0x9D, 0xE5, 0xEE, 0xA4, 0x00, 0x9A, 0xED, 0xA4, 0x00, 0x8A, 0xDD, 0x06, 0x00, 0x1A,
        0x8C, 0x8A, 0x00, 0x3A, 0x8B, 0x8A, 0x00, 0x2A, 0x71, 0x25, 0x82, 0xE2, 0xF0, 0x0C, 0x00, 0x6A,
        0xEF, 0x0C, 0x00, 0x7A, 0xFA, 0x19, 0x21, 0xE2, 0x65, 0xAF, 0x00, 0x6A, 0x64, 0xAF, 0x00, 0x7A
};

int main()
{
        unsigned char test =0x00;
        for (int i = 0; i < 256; i++)
        {
                test ^= xor_table;
        }
        printf("0x%x\n", test);
        return 0;
}

可以看到整个异或表的异或效果和单独异或0x93的效果是一样的

22.png

计算完后会判断计算后的数据是否为0

LOAD:00005DFE loc_5DFE                ;
LOAD:00005DFE LDR             R1, [SP,#0x30+var_1C] ; R1 = 异或后的数据
LOAD:00005E00 CMP             R1, #0  ; 判断异或后的数据是否为0
LOAD:00005E02 BNE             loc_5E1E

如果是0,则会改为0x99

LOAD:00005E04 LDR             R0, =(dword_16AF8 - 0x5E0A)
LOAD:00005E06 ADD             R0, PC ; dword_16AF8
LOAD:00005E08 ADDS            R0, #0x30 ; '0' ; env
LOAD:00005E0A BLX             setjmp_0
LOAD:00005E0E MOVS            R2, #0x99 ; '
LOAD:00005E10 STR             R2, [SP,#0x30+var_2C] ; 如果计算后的数据为0,则改为0x99
LOAD:00005E12 CMP             R0, #0
LOAD:00005E14 BEQ             loc_5E1E

每个字节计算完成存储到栈中,一共八次

LOAD:00005E1E loc_5E1E                ;
LOAD:00005E1E LDR             R2, [SP,#0x30+var_28] ; R2 = i
LOAD:00005E20 LDR             R1, [SP,#0x30+var_20] ; R1 = 结构体基址
LOAD:00005E22 ADDS            R3, R1, R2
LOAD:00005E24 LDR             R2, [SP,#0x30+var_28]
LOAD:00005E26 MOV             R1, SP
LOAD:00005E28 LDRB            R1, [R1,#0x30+var_2C]
LOAD:00005E2A ADDS            R3, #0x5A ; 'Z' ; 0x5A为计算后数据存储偏移
LOAD:00005E2C ADDS            R2, #1
LOAD:00005E2E STRB            R1, [R3] ; 将计算后的值存储到栈中
LOAD:00005E30 STR             R2, [SP,#0x30+var_28]
LOAD:00005E32 CMP             R2, #8  ; 计算8字节,那么取的就是用户名前8位
LOAD:00005E34 BNE             loc_5D8E

最终我们可以看到生成的8字节数据

23.png

在上图的位置下个断点,数据区跟随R3,可以看到完整的生成过程

第三个函数,就一个小循环,应该比较简单

24.png

后来分析下来是我错了,它不简单,参数之类的预处理

LOAD:000060A0 PUSH            {R4-R6,LR}
LOAD:000060A2 LDR             R3, =(off_15E9C - 0x60AC)
LOAD:000060A4 SUB             SP, SP, #0x18
LOAD:000060A6 STR             R0, [SP,#0x28+var_24] ; var_24 = 结构体基址
LOAD:000060A8 ADD             R3, PC ; off_15E9C
LOAD:000060AA LDR             R3, [R3] ; __stack_chk_guard
LOAD:000060AC ADD             R0, SP, #0x28+s ; s
LOAD:000060AE MOVS            R1, #0  ; R1 = 0
LOAD:000060B0 LDR             R3, [R3]
LOAD:000060B2 MOVS            R2, #0xA ; R2 = 0x0A
LOAD:000060B4 STR             R3, [SP,#0x28+var_14] ; 栈保护
LOAD:000060B6 BLX             memset  ; memset(s, 0, 0x0A)
LOAD:000060BA LDR             R0, =(dword_16AF8 - 0x60C0)
LOAD:000060BC ADD             R0, PC ; dword_16AF8
LOAD:000060BE ADDS            R0, #0x30 ; '0' ; env
LOAD:000060C0 BLX             setjmp_0
LOAD:000060C4 SUBS            R4, R0, #0 ; R4 = 0 //影响了标志位,这里用于判断函数的执行
LOAD:000060C6 BEQ             loc_60CE

调用了一个函数,这个函数可复杂了

LOAD:000060CE loc_60CE                ;
LOAD:000060CE LDR             R0, [SP,#0x28+var_24] ; R0 = 结构体基址
LOAD:000060D0 ADDS            R0, #0x5A ; 'Z' ; 取出计算后的8字节数据
LOAD:000060D2 BL              sub_57D4

参数是计算后的8字节数据,里面有五个函数的调用,继续一个个跟

25.png

开始做参数的存储,重定位了一个Table

LOAD:000057D4 PUSH            {R0-R2,R4-R7,LR}
LOAD:000057D6 LDR             R4, =(dword_165F8 - 0x57E0)
LOAD:000057D8 MOVS            R1, R0  ; R1 = R0 = 计算后的8字节数据
LOAD:000057DA MOVS            R2, #0x40 ; '@' ; R2 = 0x40
LOAD:000057DC ADD             R4, PC ; dword_165F8

这个Table在动态调试的过程中是有值的

26.png

但是在静态分析的时候是空的,这里有一个0x30的偏移

27.png

接下来以为我会继续分析下去吗?

不,其实这里是初始化秘钥的地方,我还分析下去,神经病啊

我看到了后面一层又一层的,而且明显的跟其它数据分开了

我发现不对劲,而且这么多计算我都看不懂,于是开启猜测模式

这里应该是某加密,前面那个8字节应该是秘钥,然后后面的函数一个个看,看看有没有什么Table,现代加密算法一般都有各种Table去做计算

运气不错,发现了DES加密算法的S盒,要是不知道S盒是啥的。。。。。。

28.png

它是八个二维数组,规格就是8 * 4 * 16

可以自行对比一下,当然也可以靠其它Table的特征

当然AES也有S盒,但是这两者的S盒是有很多区别的,比如AES的S盒如下

unsigned char sBox[] =
{ /*  0    1    2    3    4    5    6    7    8    9    a    b    c    d    e    f */
    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, /*0*/ 
    0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, /*1*/
    0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, /*2*/
    0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, /*3*/
    0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, /*4*/
    0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, /*5*/
    0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, /*6*/ 
    0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, /*7*/
    0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, /*8*/
    0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, /*9*/
    0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, /*a*/
    0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, /*b*/
    0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, /*c*/
    0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, /*d*/
    0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, /*e*/
    0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16  /*f*/
};

首先是规模不一样,其实是数量不一样,输入输出的值也都不一样

那么这里可以确定是DES加密算法,但是它是加密还是解密就需要再考量一下了

先放着,我们接着看代码,在初始化完秘钥后,开始给两个数组进行初始化操作

LOAD:000060D6 LDR             R2, [SP,#0x28+var_24] ; R2 = 结构体基址
LOAD:000060D8 LDR             R0, [R2,#0x38] ; R0 = s1 //结构体偏移0x38,为注册码
LOAD:000060DA BLX             strlen  ; R0 = strlen(s)
LOAD:000060DE LDR             R5, [SP,#0x28+var_24] ; R5 = 结构体基址
LOAD:000060E0 LSRS            R6, R0, #4 ; R6 = strlen(s) >> 4 //长度一定是非负,等效于除16
LOAD:000060E2 MOVS            R1, R4  ; R1 = R4 = 0
LOAD:000060E4 ADDS            R5, #0x3C ; '<' ; R5 = s2 //结构体偏移0x3C
LOAD:000060E6 MOVS            R0, R5  ; R0 = R5 = s2
LOAD:000060E8 MOVS            R2, #0x1E ; R2 = 0x1E
LOAD:000060EA BLX             memset  ; memset(s2, 0, 0x1E)
LOAD:000060EE B               loc_6108

顺带把注册码分为16字节每组,每组进行循环解密

LOAD:00006108 loc_6108
LOAD:00006108 CMP             R4, R6
LOAD:0000610A BLT             loc

解密后的数据存储到s2,结构体偏移0x3C

LOAD:000060F0 loc_60F0                ;
LOAD:000060F0 LDR             R2, [SP,#0x28+var_24] ; R2 = 结构体基址
LOAD:000060F2 LSLS            R3, R4, #4 ; i << 4 //此处用于注册码偏移的跳转
LOAD:000060F4 ADD             R0, SP, #0x28+s ; R0 = s = buffer
LOAD:000060F6 LDR             R1, [R2,#0x38] ; R1 = s1 = 注册码
LOAD:000060F8 ADDS            R4, #1
LOAD:000060FA ADDS            R1, R1, R3
LOAD:000060FC BL              sub_58F8 ; 此处入口对数据进行解密
LOAD:00006100 MOVS            R0, R5  ; dest
LOAD:00006102 ADD             R1, SP, #0x28+s ; src
LOAD:00006104 BLX             strcat_0 ; 解密后的数据存储到s2

最后第四个函数就是解密后的注册码和用户名进行对比,红色表示异常分支,蓝色表示正常循环,最后由两个灰色的代码块结束循环

29.png

那么,那么,那么

我们来计算一组有效的KEY

不过好像出了点问题,哪里不对的样子

30.png

因为在分析的时候我注意到了取了用户名前8位进行计算秘钥,而且后续使用了十六位进行分组解密

所以这里单纯的使用了一个八字节字符串当做用户名进行输入

竟然出错了。。。。。。

再次打个断点进行调试,看看解密后的数据是个啥

首先获取注册码

31.png

然后两组计算完后,得到解密后的数据

32.png

那这个就很尴尬了,怎么会多出八位

百撕不得姐,于是找老司机求教

33.png

发现用Java的加解密库计算出来的数据并不正确,其实可能是校验的过程改了

正常情况下解密出来的数据应该是这样的

34.png

而我上面那个是个啥玩意。。。。。。

搞得我很尴尬啊。。。。。。

既然这样,那我就不客气了,去网上找DES的C代码实现

随意找了个代码,看到了S盒,想起刚才也是S盒,会不会S盒动了手脚,于是对比了一波S盒

首先把正常DES算法的S盒准备好

static char S_Box[8][4][16] = {
        // S1 
        14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
        0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
        4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
        15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
        // S2 
        15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
        3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
        0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
        13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
        // S3 
        10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
        13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
        13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
        1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
        // S4 
        7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
        13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
        10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
        3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
        // S5 
        2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
        14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
        4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
        11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
        // S6 
        12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
        10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
        9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
        4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
        // S7 
        4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
        13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
        1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
        6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
        // S8 
        13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
        1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
        7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
        2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
};

然后将动态调试时的S盒dump出来

35.png

跟前面拷贝出xor_table一样,使用保存为文件,然后WinHex转为C Source

unsigned AnsiChar data[512] = {
        0x0E, 0x04, 0x0D, 0x01, 0x02, 0x0F, 0x0B, 0x08, 0x03, 0x0A, 0x06, 0x0C, 0x05, 0x09, 0x00, 0x07, 
        0x00, 0x0F, 0x07, 0x04, 0x0E, 0x02, 0x0D, 0x01, 0x0A, 0x06, 0x0C, 0x0B, 0x09, 0x05, 0x03, 0x08, 
        0x04, 0x01, 0x0E, 0x08, 0x0D, 0x06, 0x02, 0x0B, 0x0F, 0x0C, 0x09, 0x07, 0x03, 0x0A, 0x05, 0x00, 
        0x0F, 0x0C, 0x08, 0x02, 0x04, 0x09, 0x01, 0x07, 0x05, 0x0B, 0x03, 0x0E, 0x0A, 0x00, 0x06, 0x0D, 
        0x0F, 0x01, 0x08, 0x0E, 0x06, 0x0B, 0x03, 0x04, 0x09, 0x07, 0x02, 0x0D, 0x0C, 0x00, 0x05, 0x0A, 
        0x03, 0x0D, 0x04, 0x07, 0x0F, 0x02, 0x08, 0x0E, 0x0C, 0x00, 0x01, 0x0A, 0x06, 0x09, 0x0B, 0x05, 
        0x00, 0x0E, 0x07, 0x0B, 0x0A, 0x04, 0x0D, 0x01, 0x05, 0x08, 0x0C, 0x06, 0x09, 0x03, 0x02, 0x0F, 
        0x0D, 0x08, 0x0A, 0x01, 0x03, 0x0F, 0x04, 0x02, 0x0B, 0x06, 0x07, 0x0C, 0x00, 0x05, 0x0E, 0x09, 
        0x0A, 0x00, 0x09, 0x0E, 0x06, 0x03, 0x0F, 0x05, 0x01, 0x0D, 0x0C, 0x07, 0x0B, 0x04, 0x02, 0x08, 
        0x0D, 0x07, 0x00, 0x09, 0x03, 0x04, 0x06, 0x0A, 0x02, 0x08, 0x05, 0x0E, 0x0C, 0x0B, 0x0F, 0x01, 
        0x0D, 0x06, 0x04, 0x09, 0x08, 0x0F, 0x03, 0x00, 0x0B, 0x01, 0x02, 0x0C, 0x05, 0x0A, 0x0E, 0x07, 
        0x01, 0x0A, 0x0D, 0x00, 0x06, 0x09, 0x08, 0x07, 0x04, 0x0F, 0x0E, 0x03, 0x0B, 0x05, 0x02, 0x0C, 
        0x07, 0x0D, 0x0E, 0x03, 0x00, 0x06, 0x09, 0x0A, 0x01, 0x02, 0x08, 0x05, 0x0B, 0x0C, 0x04, 0x0F, 
        0x0D, 0x08, 0x0B, 0x05, 0x06, 0x0F, 0x00, 0x03, 0x04, 0x07, 0x02, 0x0C, 0x01, 0x0A, 0x0E, 0x09, 
        0x0A, 0x06, 0x09, 0x00, 0x0C, 0x0B, 0x07, 0x0D, 0x0F, 0x01, 0x03, 0x0E, 0x05, 0x02, 0x08, 0x04, 
        0x03, 0x0F, 0x00, 0x06, 0x0A, 0x01, 0x0D, 0x08, 0x09, 0x04, 0x05, 0x0B, 0x0C, 0x07, 0x02, 0x0E, 
        0x02, 0x0C, 0x04, 0x01, 0x07, 0x0A, 0x0B, 0x06, 0x08, 0x05, 0x03, 0x0F, 0x0D, 0x00, 0x0E, 0x09, 
        0x0E, 0x0B, 0x02, 0x0C, 0x04, 0x07, 0x0D, 0x01, 0x05, 0x00, 0x0F, 0x0A, 0x03, 0x09, 0x08, 0x06, 
        0x04, 0x02, 0x01, 0x0B, 0x0A, 0x0D, 0x07, 0x08, 0x0F, 0x09, 0x0C, 0x05, 0x06, 0x03, 0x00, 0x0E, 
        0x0B, 0x08, 0x0C, 0x07, 0x01, 0x0E, 0x02, 0x0D, 0x06, 0x0F, 0x00, 0x09, 0x0A, 0x04, 0x05, 0x03, 
        0x0C, 0x01, 0x0A, 0x0F, 0x09, 0x02, 0x06, 0x08, 0x00, 0x0D, 0x03, 0x04, 0x0E, 0x07, 0x05, 0x0B, 
        0x0A, 0x0F, 0x04, 0x02, 0x07, 0x0C, 0x00, 0x05, 0x06, 0x01, 0x0D, 0x0E, 0x00, 0x0B, 0x03, 0x08, 
        0x09, 0x0E, 0x0F, 0x05, 0x02, 0x08, 0x0C, 0x03, 0x07, 0x00, 0x04, 0x0A, 0x01, 0x0D, 0x0B, 0x06, 
        0x04, 0x03, 0x02, 0x0C, 0x09, 0x05, 0x0F, 0x0A, 0x0B, 0x0E, 0x01, 0x07, 0x06, 0x00, 0x08, 0x0D, 
        0x04, 0x0B, 0x02, 0x0E, 0x0F, 0x00, 0x08, 0x0D, 0x03, 0x0C, 0x09, 0x07, 0x05, 0x0A, 0x06, 0x01, 
        0x0D, 0x00, 0x0B, 0x07, 0x04, 0x00, 0x01, 0x0A, 0x0E, 0x03, 0x05, 0x0C, 0x02, 0x0F, 0x08, 0x06, 
        0x01, 0x04, 0x0B, 0x0D, 0x0C, 0x03, 0x07, 0x0E, 0x0A, 0x0F, 0x06, 0x08, 0x00, 0x05, 0x09, 0x02, 
        0x06, 0x0B, 0x0D, 0x08, 0x01, 0x04, 0x0A, 0x07, 0x09, 0x05, 0x00, 0x0F, 0x0E, 0x02, 0x03, 0x0C, 
        0x0D, 0x02, 0x08, 0x04, 0x06, 0x0F, 0x0B, 0x01, 0x0A, 0x09, 0x03, 0x0E, 0x05, 0x00, 0x0C, 0x07, 
        0x01, 0x0F, 0x0D, 0x08, 0x0A, 0x03, 0x07, 0x04, 0x0C, 0x05, 0x06, 0x0B, 0x00, 0x0E, 0x09, 0x02, 
        0x07, 0x0B, 0x04, 0x01, 0x09, 0x0C, 0x0E, 0x02, 0x00, 0x06, 0x0A, 0x0D, 0x0F, 0x03, 0x05, 0x08, 
        0x02, 0x01, 0x0E, 0x07, 0x04, 0x0A, 0x08, 0x0D, 0x0F, 0x0C, 0x09, 0x00, 0x03, 0x05, 0x06, 0x0B
};

然后跟上面正常的S盒进行循环对比,找到不同的地方

36.png

还真的有两处不一样,出题的你良心不会痛吗?

那看来这里是需要自己实现加密代码了。。。。。。

好在以前存了不少代码,小书包翻啊翻,把Java实现的代码翻了出来

不过测试的时候发现各种问题,弄的很尴尬。。。。。。

所以还是老实找C语言实现的代码

接下来就开始扎心了。。。。。。

找了个C实现的DES算法代码,发现结果不对

想了想,如果S盒有问题,那么其它几个Table和盒子可能也有问题,于是开始对比了一波,最后发现PC2_Table有问题

37.png

再一次的计算,发现注册码计算还是有问题,当时场面一度很尴尬。。。。。。

突然,我想起了一件事,秘钥开始的时候经过了一次神奇的异或

这尼玛。。。。。。

赶紧的赶紧的,继续改代码

东平西凑,瞎改瞎改

38.png

就先这样吧,眼泪掉下来,以后再找个时间分析一个这个样本的保护技术

最后,如果是第一次接触这种动静结合分析的同学,要时刻注意指令集的切换,中间有大量的指令集切换,看指令的地址即可,通常都是三步走,断在调用处,先别跟过去,此时跟过去会断不下来的,直接效果就是和F9一样,这一点应该有体会吧,比如使用的是BL R3,在这一句下个断点,先断下来,直接在反汇编窗口跟随R3,就可以看到要执行的代码了,但是如果指令集识别有问题,需要先ALT + G,选择Thumb模式,然后按一下C转为代码模式,再按P识别函数

免费评分

参与人数 42吾爱币 +46 热心值 +41 收起 理由
挥汗如雨 + 1 + 1 我很赞同!
gxhayxj + 1 + 1 用心讨论,共获提升!
不定期纠结先生 + 1 + 1 我很赞同!
lapop + 1 + 1 用心讨论,共获提升!
gamingnow + 1 + 1 我很赞同!
红阳 + 1 + 1 用心讨论,共获提升!
Lokavid + 1 + 1 谢谢@Thanks!
snccwt + 1 + 1 热心回复!
hexiaomo + 1 + 1 用心讨论,共获提升!
qwe159 + 1 + 1 谢谢@Thanks!
dryzh + 2 + 1 用心讨论,共获提升!
w1263883565 + 1 大神,,厉害
codelive + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Wester + 1 + 1 谢谢@Thanks!
SomnusXZY + 1 + 1 热心回复!
sunnylds7 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
soyiC + 1 + 1 谢谢@Thanks!
helloword121 + 1 + 1 谢谢@Thanks!
demon_lin + 1 + 1 谢谢@Thanks!
驿路迷茫 + 1 + 1 谢谢@Thanks!
cqr2287 + 3 + 1 zixian大神太厉害,完全看不懂
1254981099 + 1 + 1 用心讨论,共获提升!
52破解☆ + 1 + 1 已答复!
taczer + 1 + 1 我很赞同!
hello!你很阳光 + 1 + 1 热心回复!
XhyEax + 1 + 1 改s盒的好坑hhhhhhhhhh
lies2014 + 1 + 1 用心讨论,共获提升!
cher + 1 + 1 谢谢@Thanks!
测试中…… + 1 + 1 我很赞同!
我是who + 1 + 1 谢谢楼主分享 写的很详细!
zsyhn + 1 + 1 谢谢@Thanks!
kyrzy0416 + 1 + 1 热心回复!
foolboy + 1 + 1 神仙打架
mamingming22 + 1 + 1 谢谢@Thanks!
混丶沌灬 + 1 + 1 用心讨论,共获提升!
hjh5201314 + 1 + 1 用心讨论,共获提升!
chenjingyes + 1 + 1 哈哈哈 谢谢楼主分享 写的很详细
尕可 + 1 + 1 哇,学到了!!!消化了半天。。
一笑的我 + 1 + 1 我很赞同!
中国法制史 + 1 + 1 牛啊!!!
白笙 + 1 + 1 用心讨论,共获提升!
a君莫笑 + 1 + 1 我很赞同!

查看全部评分

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

 楼主| 发表于 2017-10-10 10:57 | 显示全部楼层
Pingerfy 发表于 2017-10-10 09:00
我有一些C语言的基础,目前也不是从事编程方面的工作,就是单纯的想学习一下逆向的知识,比如去广告,去 ...

如果不是想深入研究这方面的话,可以跟着别人的教程上手练习,不过个人的想法是:不懂安卓基本的开发,会经常踩坑,所以稍微学一下安卓开发我觉得会比较好,可以自己扩展出一些破解思路,破解的思路是很多的,就比如你写C一样,同一个函数有不同的写法,那么破解也是一样的,去广告,去升级,简单的注册机实现其实都不需要很深入的逆向知识,其实就是很简单一些修改,我没有一个很明确的建议,不过我的想法大概就是上面这样,我比较偏深入研究原理,所以会比较希望一起交流的人也是喜欢深入研究的那种,给的建议同理

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-10 14:18 | 显示全部楼层
wnagzihxain 发表于 2017-10-10 10:57
如果不是想深入研究这方面的话,可以跟着别人的教程上手练习,不过个人的想法是:不懂安卓基本的开发,会 ...

是的,之前就尝试把一个软件去升级,也是想到了几个思路。但是直接看反编译后的代码,确实有些看不太懂,这方面的代码必须是要学习一下的。说起原理,不知道是优点还是缺点,有时候看代码就是必须要理解了原理才能记住,不然就算记住了,很快也会忘记。所以呢,原理方面是我肯定要学习到的。
学习安卓开发,有考虑过,只是担心工作外的时间不够导致半途而废,现在决定每天固定出来一定的时间来学习一下,如果学习这方面的开发的话,学习Java语言外,还有什么要注意的吗

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-2 16:44 | 显示全部楼层

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-2 17:08 | 显示全部楼层
膜拜大神,这个分析过程写得很详细啊。

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-2 18:43 | 显示全部楼层
大大大大佬~路过评论一下下

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-2 23:01 | 显示全部楼层
大佬你是说的普通话吗 表示听不懂

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-3 00:10 | 显示全部楼层
哈哈哈  谢谢楼主分享  写的很详细!

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-3 01:25 | 显示全部楼层
厉害了啊啊啊啊

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-3 10:53 | 显示全部楼层
感谢分享

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-4 08:12 | 显示全部楼层
膜拜,满满的干货

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2017-10-4 17:20 | 显示全部楼层
牛批  厉害  膜拜

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【CB】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则


免责声明:
吾爱破解所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Service@52PoJie.Cn

快速回复 收藏帖子 返回列表 搜索

RSS订阅|手机版|小黑屋|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2017-12-12 05:03

Powered by Discuz!

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表