博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
记一次混淆算法逆向分析
阅读量:6914 次
发布时间:2019-06-27

本文共 19060 字,大约阅读时间需要 63 分钟。

ThomasKing · 2015/12/30 10:26

0x00 前言


小弟最近整理之前的资料,偶然发现半年前的混淆对抗研究以及一道CTF练习题目,故分享以作记录。限于水平,难免会有疏漏或者错误之处,望各位读者批评指正。

0x01 基本分析


jeb打开文件,找到方法校验方法。逻辑很简单,校验函数既是Native函数check.

#!vbpublic native boolean check(String arg1) {}protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    this.setContentView(2130903040);    this.inputCode = this.findViewById(2131099648);    this.btn_submit = this.findViewById(2131099649);    this.btn_submit.setOnClickListener(new View$OnClickListener() {        public void onClick(View v) {            if(MainActivity.this.check(MainActivity.this.inputCode.getText().toString())) {                MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class));            }            else {                Toast.makeText(MainActivity.this.getApplicationContext(), "Incorrect Password!",                         0).show();            }        }    });}复制代码

直接使用IDA默认Loader打开直接崩溃,存在畸形ELF文件对抗,使用自定义LOADER加载,也是然并卵的节奏。

使用Tracer动态打印check函数地址,挂起进程,dump出对应的代码段加载到IDA,找到check函数。

#!bashseg000:4561E4E8 checkseg000:4561E4E8                 LDR             PC, =sub_4561E4ECseg000:4561E4E8 ; End of function checkseg000:4561E4E8seg000:4561E4EC ; =============== S U B R O U T I N E   seg000:4561E4EC sub_4561E4EC                            ; CODE XREF: checkjseg000:4561E4EC                                         ; DATA XREF:    seg000:4561E4EC                 STMFD           SP!, {R0-R12,LR}seg000:4561E4F0                 LDR             R0, =6seg000:4561E4F4                 B               loc_4561E444复制代码

通过分析发现,其实为一个汇编stub,通过此stub跳到真正的check函数。

#!bashseg013:80A0135C sub_80A0135C                            ; DATA XREF: seg013:80A13F98oseg013:80A0135C                 B               sub_80A065B8seg013:80A0135C ; End of function sub_80A0135Cseg013:80A01360seg013:80A01360 ; =============== S U B R O U T I N E   seg013:80A01360 ; Attributes: thunkseg013:80A01360seg013:80A01360 sub_80A01360                            ; DATA XREF: sub_80A065C4+Coseg013:80A01360                 B               sub_80A065F8seg013:80A01360 ; End of function sub_80A01360seg013:80A01364 ; =============== S U B R O U T I N E   seg013:80A01364 ; Attributes: thunkseg013:80A01364seg013:80A01364 sub_80A01364                            ; CODE XREF: sub_80A06620jseg013:80A01364                 B               sub_80A0663Cseg013:80A01364 ; End of function sub_80A01364复制代码

以80A0135C(B sub_80A065B8)为例子,跟进sub_80A065B8,可以看到如下指令:

#!bash// 0x80A0135Cseg013:80A065B8                 BEQ             loc_80A0658Cseg013:80A065BC                 BNE             loc_80A0658Cseg013:80A0658C                 STMFD           SP!, {R3-R8,R10,LR} 真实指令seg013:80A06590                 STMFD           SP!, {R8,LR}seg013:80A06594                 LDR             R8, loc_80A065A4seg013:80A06598                 LDR             R8, loc_80A065A8seg013:80A0659C                 LDR             R8, loc_80A065ACseg013:80A065A0                 LDR             R8, locret_80A065B0seg013:80A065A4                 LDR             R8, =(sub_80A065C4 - 0x80A065B0)seg013:80A065A8                 ADD             R8, PC, R8 ; sub_80A065C4seg013:80A065AC                 STR             R8, [SP,#4]seg013:80A065B0                 LDMFD           SP!, {R8,PC}seg013:80A065C4                 STMFD           SP!, {R8,LR}seg013:80A065C8                 LDR             R8, =0xFFFFAD25seg013:80A065CC                 EOR             R8, R8, #0xADseg013:80A065D0                 ADD             R8, PC, R8 ; loc_80A01360 //返回到80A01360seg013:80A065D4                 STR             R8, [SP,#8+var_4]seg013:80A065D8                 LDMFD           SP!, {R8,PC}复制代码

通过分析可以得到真实指令(STMFD SP!, {R3-R8,R10,LR}),其余指令为混淆指令,最终返回到下一条B即80A01360(B sub_80A065F8)指令。通过分析其他B指令,可以得到类似的混淆指令中夹在一条真实指令,只是存在多种混淆的方式。 至此,我们可以得到此混淆的思路:执行"一个B指令"即一条真实的指令,混淆抽象为:

  • 执行前跳转混淆
  • 真实指令
  • 执行后跳转混淆

不难发现,如果仅仅靠一条一条的寻找真实指令,是非常费时费力的。由于执行前后都存在多种模式的混淆,但总的模式是有限的,那么通过提取指令特征匹配即可以自动化实现去混淆,找出真实指令。

0x02 基于指令特征匹配对抗混淆


通过分析找到所有的混淆模式,最后大概几种。限于篇幅,列举一些做说明

#!bash// 0x80A0135Cseg013:80A065B8                 BEQ             loc_80A0658Cseg013:80A065BC                 BNE             loc_80A0658Cseg013:80A0658C                 STMFD           SP!, {R3-R8,R10,LR} 真实指令seg013:80A06590                 STMFD           SP!, {R8,LR}seg013:80A06594                 LDR             R8, loc_80A065A4seg013:80A06598                 LDR             R8, loc_80A065A8seg013:80A0659C                 LDR             R8, loc_80A065ACseg013:80A065A0                 LDR             R8, locret_80A065B0seg013:80A065A4                 LDR             R8, =(sub_80A065C4 - 0x80A065B0)seg013:80A065A8                 ADD             R8, PC, R8 ; sub_80A065C4seg013:80A065AC                 STR             R8, [SP,#4]seg013:80A065B0                 LDMFD           SP!, {R8,PC}seg013:80A065C4                 STMFD           SP!, {R8,LR}seg013:80A065C8                 LDR             R8, =0xFFFFAD25seg013:80A065CC                 EOR             R8, R8, #0xADseg013:80A065D0                 ADD             R8, PC, R8 ; loc_80A01360seg013:80A065D4                 STR             R8, [SP,#8+var_4]seg013:80A065D8                 LDMFD           SP!, {R8,PC}复制代码

执行前混淆:B(连续两条条件完全相反的指令) next_jmp 执行后混淆:这里有两组STMFD--LDMFD构成的跳转stub,但其是为一种模式。那如何计算next_jmp呢?这里我采用取巧的方式,通过从LDMFD所在地址反向找到ADD指令,得到";loc_80a01360",再解析出地址80a01360。当然,存在多种prefix,需要作简单处理获取地址。

#!pythondef prefix_match(str):    pattern = ['sub_', 'loc_', 'unk_', 'locret_']    for prefix in pattern:        if str.find(prefix) > -1:            substr = str[str.find(prefix) + len(prefix):]            return string.atoi(substr, 16)    return 0xffffffff;复制代码

真实指令:通过解析跳转遍历完整个混淆后,通过堆栈平衡原理,提取出真实指令。以上述分析为例,遍历回到下一条指令80a01360后,对指令进行分组即(B)(STMFD SP!, {R3-R8,R10,LR})(STMFD-LDMFD)(STMFD-LDMFD),非常容易获取真实指令。实现时,可将分组过程可融入到指令的遍历即可。

再接着看另一组混淆,以80A01360(B sub_80A065F8)为例。

#!bash// 0x80A01360seg013:80A065F8                 STMFD           SP!, {R0,LR}seg013:80A065FC                 LDR             R0, loc_80A0660Cseg013:80A06600                 LDR             R0, loc_80A06610seg013:80A06604                 LDR             R0, loc_80A06614seg013:80A06608                 LDR             R0, locret_80A06618seg013:80A0660C                 LDR             R0, =(loc_80A065E0 - 0x80A06618)seg013:80A06610                 ADD             R0, PC, R0 ; loc_80A065E0seg013:80A06614                 STR             R0, [SP,#4]seg013:80A06618seg013:80A06618                 LDMFD           SP!, {R0,PC}seg013:80A065E0                 LDR             R3, [R0] //真实指令seg013:80A065E4                 STMFD           SP!, {R0,LR}seg013:80A065E8                 MOV             LR, PCseg013:80A065EC                 BL              loc_80A065F0seg013:80A065F0seg013:80A065F0 loc_80A065F0                            ; CODE XREF: seg013:80A065ECjseg013:80A065F0                 LDMFD           SP!, {R0,LR}seg013:80A065F4                 B               sub_80A06620seg013:80A06620                 B               sub_80A01364复制代码

执行前混淆:STMFD-LDMFD跳转到loc_80A065E0。获取next_jmp和上述一致。

执行后混淆:通过STMFD-LDMFD和两次B直接跳转返回到下一条B指令地址sub_80A01364。
真实指令:和上述类似,遍历混淆指令时,对指令进行分组(STMFD-LDMFD)、(LDR R3, [R0])、(B)、(B)。易获取真实指令(LDR R3, [R0])。

通过上述方法,大概分析20个多有的B指令即可找到所有的混淆模式,总的来说混淆的模式是有限的。

通过编写IDAPython脚本,即可实现自动打印真实指令。

#!bash0x80a0135c            PUSH            {r3, r4, r5, r6, r7, r8, sl, lr}0x80a01360            LDR             r3, [r0]0x80a01364            MOV             r1, r20x80a01368            MOV             r6, r20x80a0136c            LDR             r3, [r3, #0x2a4]0x80a01370            MOV             r2, #00x80a01374            MOV             r4, r00x80a01378            BLX             r30x80a0137c            MOV             r7, r0复制代码

但存在问题,当IDA并没有识别出指令时,无法通过GetMnem等API获取信息。

由于混淆对IDA指令识别的影响,致使IDA无法自动将指令反汇编出来。可能已经有读者意识到,遇到这种情况直接调用MakeCode将数据转化为指令即可。然而,实际使用MakeCode自动处理时,并不能完成手动按'C'识别指令的功能。那么,是否遇到这种情况后,就手动去完成指令反汇编呢?答案是否定的。由于存在很多这种情况,手动转化也很费时(测试环境IDA6.8)。

到这里可以看到,单纯依靠简单的字符串匹配比较的方法,并不能完全满足自动化提取指令对抗混淆的需求。

0x03 基于指令解析执行对抗混淆


通过上述分析,由于IDA存在无法自动反汇编一部分opcode数据,故单纯依靠IDAPython是无法满足指令解析指令的需求的。为了实现对指令的解析,可采用两种途径:

  1. 对照arm汇编手册,编写常见的opcode解析脚本。以笔者的经验,这部分内容是比较耗时的。
  2. 引入现有的反汇编引擎,且这种反汇编引擎具备对指令的想尽分析的能力。这里,我选用Capstone。

Capstone是一款支持多种架构的反汇编引擎,支持对汇编指令粗略和详细的分析,支持多种语言。当然,Capstone还有很多其他优点,这里就不赘述了。

3.1 ARM处理器模拟

可能有读者马上会问,模拟arm处理器执行不又是一大工程呢。的确,完全模拟确实包含许多工作量。但结合此混淆的一些特性,整个模拟执行可简化许多。

首先,此混淆并不存在流程分支扁平化(与OLLVM相对比)。结合上述分析也可以看到,所有的混淆执行并不会影响条件标志即CPSR寄存器。

再者,结合堆栈平衡原理,SP寄存器仅仅只需要保存堆栈的变化,比如stmfd仅仅对SP寄存器进行减法操作。

最后,根据上述找到的混淆模式,可以发现使用的指令其实很少,实际编写模拟函数工作量也比较小。

#!pythondef do_emulate(code, base, Rx):    ret_addr = 0xffffffff    emu = ARM_emu()    md = Cs(CS_ARCH_ARM, CS_MODE_ARM)    md.detail = True    for i in md.disasm(code, base):        emu.regs[PC] = i.address + 2 * inst_size        dst = i.operands[0]        src = i.operands[1]        if (i.mnemonic).upper() == 'LDR':            if dst.type == ARM_OP_REG and src.type == ARM_OP_MEM:                Rd = conv_reg(dst.value.reg)                    Rs = conv_reg(src.value.mem.base)                addr = emu.regs[Rs] + src.value.mem.disp                emu.regs[Rd] = Dword(addr & 0xffffffff)                if Debug:                    print ('\t LDR %s :\t0x%x' %(i.op_str, emu.regs[Rd]))        elif (i.mnemonic).upper() == 'ADD':            if i.operands[0].type == ARM_OP_REG and i.operands[1].type == ARM_OP_REG and i.operands[2].type == ARM_OP_REG:                Rd = conv_reg(i.operands[0].value.reg)                R1 = conv_reg(i.operands[1].value.reg)                R2 = conv_reg(i.operands[2].value.reg)                emu.regs[Rd] = (emu.regs[R1] + emu.regs[R2]) & 0xffffffff                if Debug:                    print ('\t ADD %s :\t0x%x' %(i.op_str, emu.regs[Rd]))        ...复制代码

在模拟执行一条真实指令时,首先将所有寄存器的初始值设置为0,通过主流程中的B指令进入到混淆指令。

3.2 真实指令提取

模拟执行时,将混淆中的每条指令都存储到一个指令堆栈中。结合之前直接字符串模式的思路,来实现对真实指令的提取。

以80A01364为例子来说明真实指令的提取方法。

#!bashseg013:80A01364 sub_80A01364                            ; CODE XREF: sub_80A06620jseg013:80A01364                 B               sub_80A0663Cseg013:80A0663C                 BMI             loc_80A06648seg013:80A06640                 BPL             loc_80A06644seg013:80A06644seg013:80A06644 loc_80A06644                            ; CODE XREF: sub_80A0663C+4jseg013:80A06644                                         ; sub_80A0663C:loc_80A06648jseg013:80A06644                 B               loc_80A06624seg013:80A06648 ; seg013:80A06648seg013:80A06648 loc_80A06648                            ; CODE XREF: sub_80A0663Cjseg013:80A06648                 B               loc_80A06644seg013:80A06624 loc_80A06624                            ; CODE XREF: seg013:80A06624                 MOV             R1, R2seg013:80A06628                 STMFD           SP!, {R0,LR}seg013:80A0662C                 MOV             LR, PCseg013:80A06630                 BL              loc_80A06634seg013:80A06634 ; seg013:80A06634seg013:80A06634 loc_80A06634                            ; CODE XREF: sub_80A0663C-Cjseg013:80A06634                 LDMFD           SP!, {R0,LR}seg013:80A06638                 B               sub_80A0664Cseg013:80A0664C sub_80A0664C                            ; CODE XREF: sub_80A0663C-4pseg013:80A0664C                 B               sub_80A01368复制代码

若不在模拟执行中对指令堆栈修正,那么执行完后存储指令如下所示:

#!bash80A0663C    BMI     loc_80A0664880A06648    B       loc_80A0664480A06644    B       loc_80A0662480A06624    MOV     R1, R280A06628    STMFD   SP!, {R0,LR}80A06630    BL      loc_80A0663480A06634    LDMFD   SP!, {R0,LR}80A06638    B       sub_80A0664C80A0664C    B       sub_80A01368复制代码

对于BMI,虽然形式上和之前分析的(BEQ loc_80A0658C,BNE loc_80A0658C)直接跳到next_jmp,但检测下一条指令即可根据条件相反去处。

对于STM-LDM,当遇到LDM指令时,将STM-LDM及其之间的指令出栈移除。

剩余(B B MOV B)这些指令,根据上述人工分析的结果可知,因为只存在一条真实指令,那么MOV必定是真实指令。另外,存在这种情况(B B BNE B),产生这种情况的根本原因是混淆前这条指令是if或者循环语句的判定点,直接取出BNE指令即可。

3.3 函数识别

不管是基于指令名称匹配还是解析执行,都需要对函数进行识别。先来看一个函数混淆片段:

#!bashseg013:80A067F0 loc_80A067F0                    ; CODE XREF: seg013:loc_80A06838jseg013:80A067F0                 ADR             LR, sub_80A06814seg013:80A067F4                 STMFD           SP!, {R8,R9,LR}seg013:80A067F8                 LDR             R8, loc_80A067FCseg013:80A067FCseg013:80A067FC loc_80A067FC                            ; DATA XREF: seg013:80A067F8rseg013:80A067FC                 LDR             R9, =0x1A6016A4seg013:80A06800                 ADD             R8, R9, R8seg013:80A06804                 ADD             R8, PC, R8 ; j_strlenseg013:80A06808                 STR             R8, [SP,#8]seg013:80A0680C                 LDMFD           SP!, {R8,R9,PC}复制代码

对于未混淆的指令,函数通常被编译为BL或者BLX(指令模式切换)。由于B指令本身的跳转地址范围很有限,那么混淆后代码膨胀必定需要对其指令修正,有点类似InlineHook指令修正。另外,函数的返回地址需要显式存放到LR寄存器。

这样,上述代码在模拟执行时,当LR寄存器值不为0时,将后续的函数调用转化为'Call sub_xxx'指令,将PC置为next_jmp(sub_80A06814)接着模拟。

另外,便于更加清晰的分析,将libc.so加载到和进程一致的基地址,通过IDAPython GetFunctionName获取函数名称。

至此,即可提取出真实指令,check函数流程:

#!bash0x80a0135c            PUSH            {r3, r4, r5, r6, r7, r8, sl, lr}0x80a01360            LDR             r3, [r0]0x80a01364            MOV             r1, r20x80a01368            MOV             r6, r20x80a0136c            LDR             r3, [r3, #0x2a4]0x80a01370            MOV             r2, #00x80a01374            MOV             r4, r00x80a01378            BLX             r30x80a0137c            MOV             r7, r00x80a01380            call  j_strlen0x80a01384            ADD             sl, r0, #10x80a01388            MOV             r8, r00x80a0138c            MOV             r0, sl0x80a01390            call  j_malloc_00x80a01394            MOV             r1, r70x80a01398            MOV             r2, sl0x80a0139c            MOV             r5, r00x80a013a0            call  j_memcpy0x80a013a4            LDR             r3, [r4]0x80a013a8            MOV             r2, #00x80a013ac            STRB            r2, [r5, r8]0x80a013b0            LDR             r3, [r3, #0x2a8]0x80a013b4            MOV             r2, r70x80a013b8            MOV             r0, r40x80a013bc            MOV             r1, r60x80a013c0            BLX             r30x80a013c4            LDR             R0, =0x12BC40x80a013c8            MOV             r1, #0x800x80a013cc            LDR             R0, [PC,R0]0x80a013d0            call  0x80a010480x80a013d4            ADD             r5, r5, r00x80a013d8            MOV             r0, r50x80a013dc            call  0x80a010c00x80a013e0            MOV             r4, r00x80a013e4            MOV             r0, r50x80a013e8            call  j_free_10x80a013ec            MOV             r0, r40x80a013f0            POP             {r3, r4, r5, r6, r7, r8, sl, pc}复制代码

0x04 算法逆向分析


通过简单分析check即可看到算法的核心流程在0x80a010c0这个函数,而0x80a01048函数的功能是对指令路径上的断点进行检测,和其他平台的反调试思路类似,这里把重点放在0x80a010c0的逆向上。自动化分析得到0x80a010c0函数:

#!bash0x80a010c0            PUSH            {r4, r5, r6, r7, r8, sb, sl, lr}0x80a010c4            LDR             R7, =0x12EB40x80a010c8            SUB             sp, sp, #0x3080x80a010cc            ADD             r6, sp, #4 0x80a010d0            LDR             R7, [PC,R7]0x80a010d4            LDR             r3, [r7]0x80a010d8            MOV             r4, r00x80a010dc            MOV             r1, #00x80a010e0            MOV             r2, #0x1000x80a010e4            MOV             r0, r60x80a010e8            ADD             r5, sp, #0x104 0x80a010ec            STR             r3, [sp, #0x304]0x80a010f0            call  j_memset0x80a010f4            MOV             r1, #00x80a010f8            MOV             r2, #0x1000x80a010fc            MOV             r0, r50x80a01100            call  j_memset0x80a01104            MOV             r0, r40x80a01108            call  j_strlen0x80a0110c            SUBS            sb, r0, #00x80a01110            MOVEQ           r0, sb0x80a01114            BNE             #0x80a011300x80a01118            LDR             r2, [sp, #0x304]0x80a0111c            LDR             r3, [r7]0x80a01120            CMP             r2, r30x80a01124            BNE             #0x80a013340x80a01128            ADD             sp, sp, #0x3080x80a0112c            POP             {r4, r5, r6, r7, r8, sb, sl, pc}//获取代码段起始256字节作为key0x80a011bc            LDR             R0, =0x12DC0   //读取代码段起始地址0x80a011c0            LDR             LR, =0x666666670x80a011c4            MOV             r4, #00x80a011c8            LDR             R0, [PC,R0]0x80a011cc            MOV             r3, r00x80a011d0            SMULL           r2, ip, lr, r40x80a011d4            ASR             r2, r4, #0x1f0x80a011d8            LDRB            r1, [r3]0x80a011dc            RSB             r2, r2, ip, asr #10x80a011e0            ADD             r2, r2, r2, lsl #20x80a011e4            RSB             r2, r2, r40x80a011e8            STRB            r1, [r6, r4]0x80a011ec            ADD             r4, r4, #10x80a011f0            CMP             r4, #0x1000x80a011f4            ADD             r3, r3, r20x80a011f8            BNE             #0x80a011d0//key变换流程0x80a01218            MOV             r3, #00x80a0121c            MOV             r0, r30x80a01220            ADD             r4, r4, #10x80a01224            ADD             r6, sp, #0x3080x80a01228            AND             r4, r4, #0xff0x80a0122c            ADD             r1, r6, r40x80a01230            LDRB            r2, [r1, #-0x304] 0x80a01234            LDRB            r8, [r5, r3] 0x80a01238            AND             ip, r3, #7 0x80a0123c            ADD             r0, r2, r00x80a01240            AND             r0, r0, #0xff0x80a01244            ADD             r6, r6, r00x80a01248            LDRB            sl, [r6, #-0x304]0x80a0124c            ASR             sb, r8, #50x80a01250            ORR             r8, sb, r8, lsl #3 0x80a01254            STRB            sl, [r1, #-0x304] 0x80a01258            STRB            r2, [r6, #-0x304]0x80a0125c            LDRB            r6, [r1, #-0x304]0x80a01260            ADD             sl, sp, #0x3080x80a01264            RSB             r1, ip, #8 // 8 - [0, 7]0x80a01268            ADD             r2, r2, r60x80a0126c            AND             r2, r2, #0xff0x80a01270            ADD             r2, sl, r20x80a01274            LDRB            r2, [r2, #-0x304]0x80a01278            EOR             r2, r2, r80x80a0127c            AND             r2, r2, #0xff0x80a01280            LSL             r1, r2, r1 0x80a01284            ORR             ip, r1, r2, asr ip // 循环左移(8 - i)位0x80a01288            STRB            ip, [r5, r3] 0x80a0128c            ADD             r3, r3, #10x80a01290            CMP             r3, #0x1000x80a01294            BNE             #0x80a01220for(i = 0; i < 0x100; i++){    left_rotate(right_rotate(mid_code[i], 5) ^ key_stream[i], 8 - (i % 8));...复制代码

限于篇幅,就不在一一分析。其中包括RC4算法。最后得到算法编码主流程:

#!cppchar gen_mid_code[N];char key_stream[N];for(i = 0; i < N; i++){    gen_mid_code[i] = left_rotate(str[gen_index(i, strlen(str))], 8 - (i % 8));}gen_key_stream(ori_key, key_stream);RC4_encrypt(gen_mid_code, key_stream, final_code);for(i = 0; i < N; i++){    if(final_code[i] != check_code[i]){            ...    }}复制代码

最后,得到flag:Hello Tomorrow!

至此,此ctf题目大致分析完毕。

题目下载地址:

转载地址:http://tiacl.baihongyu.com/

你可能感兴趣的文章
RabbitMQ的安装和使用
查看>>
WebAssembly起步
查看>>
基于CentOS搭建Hexo博客--设置NexT主题及个性化定制
查看>>
百度移动端首页秒开学习
查看>>
【304天】每日项目总结系列042(2017.12.06)
查看>>
数人云|给还在犹豫选择的你,微服务架构与整体架构的各自优势
查看>>
ES6之数值的扩展
查看>>
JavaScript Event loop 事件循环
查看>>
Java究极打基础之ArrayList篇
查看>>
Java窗口(JFrame)从零开始(4)——流布局+边界布局+网格布局
查看>>
MySQL忘记密码后重置密码(Mac )
查看>>
谈谈基于机器学习的编程到底比传统编程强在哪里?
查看>>
Python中的集合类模块collections详解
查看>>
Chef在InSpec 2.0增强了云安全的自动化功能
查看>>
全能App研发助手!滴滴开源DoraemonKit
查看>>
NoSQL 数据库敏捷数据模型
查看>>
Oracle回应用户锁定,自治数据库是更好选择
查看>>
函数式编程能否支持更高效的区块链基础设施?
查看>>
R语言:数据输出至文件
查看>>
Cisco系列路由器密码恢复研究与实践
查看>>