0x01

前几日,在公司对客户单位的网站与APP进行授权渗透,在使用用户提供账号登录APP端进行抓包时,发现APP传输包与返回包均进行了加密

bp抓包界面如下:

为破解密文获取请求与传输包中内容,于是便对APK文件进行逆向

0x02 APP逆向寻找破解点

使用jadx 打开 APK 文件, 搜索发送加密请求接口,寻找加密方式

跟踪到请求接口,发现请求接口加密使用注解@Encrypt,未找到直接的请求包加密方式,在jadx中搜索Request,找到 RequestDataBuilder类

该类中包含有请求包加密解密函数,并含有一个APP_KEY

在主管帮助下在github上获取到一个js文件

因该文件为js文件,且客户单位给的网站与APP同样进行了数据加密,猜测客户单位的网站应该是使用了该js文件进行传输数据加解密,于是根据该js文件尝试对客户单位网站数据加密进行破解

0x03 网站js加密破解

对该JS文件进行分析,发现使用该JS文件会发送两种类型请求,登录请求 和 登录后发送的普通请求 , 对应函数如下:

  1. 登录请求

  1. 登录后发送的普通请求

分析登录请求

登录请求会调用login,首先对登录函数login进行分析,该函数传值并调用generateKey函数

/**

* 登录函数

* @param username 会员号

* @param pass 密码

*/

export function login(username, pass, resultfunc) {

generateKey(username,

function(obj) {

dologin(obj, username, pass, resultfunc);

},

function(info) {

resultfunc(false, info);

});

}

/*

* 在这个login函数中传入username变量和两个function函数到generateKey函数中

* 因为在JavaScript中函数是第一公民,所以可以作为变量传递

*/

login函数传参到generateKey函数后,分析generateKey函数逻辑

/**

* 发送请求到服务端,进行秘钥的产生,产生的秘钥包括RSA密钥对,和进行秘钥交换通讯的会话秘钥

* @param tjid

* @param successfunc

* @param errorfunc

*/

function generateKey(tjid, successfunc, errorfunc) {

$.ajax({

type: "get",

url: nctUrl + "/gen/key",

data: {

"tjid": tjid

},

dataType: "json",

success: function(obj) {

successfunc(obj);

},

error: function(req, info) {

errorfunc(info);

}

});

}

/*

* generateKey函数通过get请求向接口 /gen/key 发起请求,传入参数为tjid

* tjid参数其实就是登录网站时输入的用户名

* 如果请求成功, 则调用第一个传入generateKey函数的函数,就是调用dologin函数

* 如果请求失败, 则调用第二个传入generateKey函数,就是resultfunc函数

*/

// resultfunc函数其实也是传入的一个函数变量,猜测传入的函数应该为failfunc函数

// 该函数作用其实就是alert一下错误信息,如下

function failfunc(info) {

alert(info);

}

// 总结一下, generateKey函数的作用就是拿用户输入的用户名去跟服务器请求会话秘钥

// 如果成功调用dologin函数, 如果失败alert弹出错误信息

调用 /gen/key 接口如下

如果tjid存在,也就是用户名存在,则请求成功,获取到服务器返回的 ss 和 ps两个值。

当generateKey函数发起请求成功后,则调用dologin函数,下面查看dologin函数

/***

* 执行登录

* @param obj

* @param username

* @param pass

*/

function dologin(obj, username, pass, resultfunc) {

if (obj.code == 1) {

var sessionkey = obj.data.ss;

$.ajax({

url: nctUrl + "/login",

type: "post",

data: {

"username": username,

"userpass": pass,

"publickey": obj.data.ps

},

dataType: "json",

beforeSend: function(xhr) {

addHeaders(xhr);

},

error: function(xhr, info) {

resultfunc(false, info);

},

success: function(obj) {

if (obj.code == 1) {

var secretkey = obj.data.secretkey;

var cryptedInfo = obj.data.info;

decryptSecretKey(username, sessionkey, secretkey, function(retObj) {

if (retObj.code == 1) {

sessionKey = decodeAndDecryptWith3Des(sessionkey, retObj.data);

var decoded = decode(cryptedInfo);

var plainInfo = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decoded);

var plainObj = JSON.parse(plainInfo);

sessionId = plainObj.sessionid;

$.cookie("sessionKey", sessionKey);

$.cookie("sessionId", sessionId);

// $("#content1").val("sessionid:" + sessionId + " sessionKey:" + sessionKey);

resultfunc(true, "登录成功!", plainInfo);

} else {

resultfunc(false, retObj.msg);

}

}, function(info) {

resultfunc(false, info);

});

} else {

resultfunc(false, obj.msg);

}

}

});

} else {

resultfunc(false, obj.msg);

}

}

/*

* dologin函数首先将之前请求获取到的ss值存储到sessionKey变量中,

* 之后向 /login 接口发送POST请求,请求中带有username、password以及之前获取到的ps值(作为publicKey参数)

* 如果请求成功,则从返回数据包中获取secretKey和info

* 之后调用decryptSecretKey函数对secretKey进行解密,解密成功后操作如下:

* 1. 对sessionKey值(注意是sessionKey,不是secretKey,sessionKey就是之前请求返回的ss)加密,处理后存储到浏览器cookie中

* 2. 对info(info值存储到变量cryptedInfo中,因此解密使用该变量名)进行解密,解密后获取sessionId存储到cookie中

* 3. 页面alert登录成功信息

* 另外如果dologin函数请求失败,也就是登录失败,则alert错误信息

*/

请求获得secretKey如下:

上面dologin函数解密用到的decryptSecretKey函数分析如下:

// 对服务端返回的secretKey进行解密的decryptSecretKey函数

/**

* 对服务端返回的secretKey进行解密,目前通过与服务端交互实现

* @param secretKey

*/

function decryptSecretKey(tjid, sessionkey, secretKey, succcessfunc, failfunc) {

var crypted = encryptWith3DesAndEncode(sessionkey, secretKey);

$.ajax({

type: "get",

url: nctUrl + "/gen/unc",

data: {

tjid: tjid,

crypted: crypted

},

dataType: "json",

success: function(obj) {

succcessfunc(obj);

},

error: function(xhr, info) {

failfunc(info);

}

});

}

/*

* 该函数首先使用加密函数encryptWith3DesAndEncode,将sessionKey(此时sessionKey还未进行处理,值还是之前的服务端返回的ss的值)和secreteKey进行加密,加密后的结果赋值给crypted变量

* 之后向 /gen/unc 接口发送POST请求,请求参数是tjid(用户输入的用户名)和上面加密得到的crypted

* 如果请求成功,服务端会返回一个data参数,并进行后续操作,也就是上面dologin函数分析的decryptSecretKey函数解密成功后的操作

* 如果请求失败,则alert失败信息

*/

请求解密secretKey如下:

其他加解密函数如下:

/**

* 用3des加密,结果采用编码返回

* @param key

* @param plainText

* @returns {*}

*/

function encryptWith3DesAndEncode(key, plainText) {

if (key == null || key == '' || plainText == null || plainText == '') {

return '';

}

var ckey = produce3DesKey(key);

var result = CryptoJS.TripleDES.encrypt(plainText, CryptoJS.enc.Utf8.parse(ckey), {

mode: CryptoJS.mode.ECB,

padding: CryptoJS.pad.Pkcs7

});

return result.toString();

}

var secretSeed = "123456789012345678901234";

function produce3DesKey(key) {

if (key == undefined || key == null || key == '')

return secretSeed;

if (key.length >= 24) {

return key.substr(0, 24);

}

return key + secretSeed.substr(key.length);

}

/**

* 进行解码

* @param bs64

* @returns {*}

*/

function decode(bs64) {

var decoded = CryptoJS.enc.Utf8.stringify(CryptoJS.enc..parse(bs64));

return decoded;

}

/**

* 进行编码

* @param

* @returns {*}

*/

function encode(str) {

var encoded = CryptoJS.enc..stringify(CryptoJS.enc.Utf8.parse(str));

return encoded;

}

/**

* 进行3des解密,解密之前,会将密文进行解码

* @param key

* @param cryptedBs64

* @returns {*}

*/

function decodeAndDecryptWith3Des(key, cryptedBs64) {

if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') {

return '';

}

var ckey = produce3DesKey(key);

var result = CryptoJS.TripleDES.decrypt(

cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), {

mode: CryptoJS.mode.ECB,

padding: CryptoJS.pad.Pkcs7

});

return result.toString(CryptoJS.enc.Utf8)

}

至此登录请求过程已经分析完成,流程如下:

  • 用户输入账号和密码,之后浏览器携带用户输入的账号向服务端/gen/key接口发起请求,如果用户输入的账号存在,则服务器返回 ss 和 ps参数
  • 浏览器携带用户输入的账号、密码以及上面请求返回的ps向服务端 /login 接口发起请求,如果信息正确,服务端返回secretKey和info参数
  • 浏览器对ss和secretKey合并加密,获取到新内容crypted,浏览器携带用户输入的账号和crypted向服务端/gen/unc接口发起请求对secretKey进行解密,解密成功后返回data参数,之后对ss进行处理,处理后的值作为sessionKey存储到浏览器cookie中,同时浏览器对之前返回的info进行解密,解密后获取到sessionId,sessionId也存储到浏览器中

下面分析第二种请求,登录后发送的普通请求

登录进web系统后浏览器向服务端请求数据都是使用该请求,该请求本质是调用函数sendRequest,如下:

/**

* 发送请求到服务端

* @param url

* @param params

* @param successfunc

* @param failfunc

*/

export function sendRequest(url, params, successfunc, failfunc, $this) {

var crypted = "";

var pstr = JSON.stringify(params);

var sessionKey = $.cookie("sessionKey");

var sessionId = $.cookie("sessionId");

if (pstr != "{}") {

crypted = encode(encryptWith3DesAndEncode(produceSessionKey(sessionKey), pstr));

}

$.ajax({

url: url,

type: "post",

data: crypted,

dataType: "json",

contentType: "application/json",

xhrFields: {

withCredentials: true

},

crossDomain: true,

beforeSend: function(xhr) {

addHeaders(xhr);

//发送业务请求时,需要进行签名

var timestamp = parseInt(new Date().getTime() / 1000);

var random = randomString(8);

var sign = produceSign(sessionId, timestamp, random, crypted);

xhr.setRequestHeader("sessionid", sessionId);

xhr.setRequestHeader("timestamp", timestamp);

xhr.setRequestHeader("random", random);

xhr.setRequestHeader("sign", sign);

},

error: function(xhr, info) {

failfunc(info);

$this.$Message.error({

content: info,

duration: 3,

closable: true

})

},

success: function(obj) {

if (obj.code == 1) {

var data = obj.data;

if (data) {

var decoded = decode(data);

var plainText = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decoded);

var plainObj = JSON.parse(plainText);

successfunc(plainObj);

} else {

successfunc(obj);

}

} else {

failfunc(obj);

$this.$Message.error({

content: obj.msg,

duration: 3,

closable: true

})

}

}

});

}

/*

* 该请求主要分为两部分,请求前数据加密和请求后数据解密

* 请求前数据加密:

* 1. 从cookie中取出sessionKey,进行处理

* 2. 将处理后的seesionKey同要加密的数据一起通过encryptWith3DesAndEncode函数进行加密,加密后* 在进行一次编码,编码后的结果发送到服务器

* 请求后数据解密:

* 1. 从cookie中取出sessionKey,进行处理

* 2. 对服务端返回的结果进行解码,之后将处理后的sessionKey和解码后的数据一同通过调用

* decodeAndDecryptWith3Des函数进行解密

*/

// 对sessionKey进行处理的函数如下

function produceSessionKey(key) {

return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase();

}

至此登录后发送的普通请求过程分析完成,通过对以上两种类型请求过程分析,并通过使用该js文件中提供的加解密函数写出请求的加解密脚本(脚本为js脚本,可通过node调用运行),如下:

获取SeesionKey脚本, 该脚本获取的sessionKey是放置到cookie中的sessionKey

const CryptoJS = require('crypto-js')

var secretSeed = "123456789012345678901234";

function decodeAndDecryptWith3Des(key, cryptedBs64) {

if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') {

return '';

}

var ckey = produce3DesKey(key);

var result = CryptoJS.TripleDES.decrypt(

cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), {

mode: CryptoJS.mode.ECB,

padding: CryptoJS.pad.Pkcs7

});

return result.toString(CryptoJS.enc.Utf8)

}

function produce3DesKey(key) {

if (key == undefined || key == null || key == '')

return secretSeed;

if (key.length >= 24) {

return key.substr(0, 24);

}

return key + secretSeed.substr(key.length);

}

var ss = ""; // 请求/gen/key接口返回的ss

var data = ""; // 请求 /gen/unc 接口返回的data

console.log(decodeAndDecryptWith3Des(ss, data));

加密数据包脚本

const CryptoJS = require('crypto-js')

var secretSeed = "123456789012345678901234";

var APP_KEY_SEED = "terjoycht2014!@#";

/**

* 用3des加密,结果采用编码返回

* @param key

* @param plainText

* @returns {*}

*/

function encryptWith3DesAndEncode(key, plainText) {

if (key == null || key == '' || plainText == null || plainText == '') {

return '';

}

var ckey = produce3DesKey(key);

var result = CryptoJS.TripleDES.encrypt(plainText, CryptoJS.enc.Utf8.parse(ckey), {

mode: CryptoJS.mode.ECB,

padding: CryptoJS.pad.Pkcs7

});

return result.toString();

}

/**

* 进行编码

* @param

* @returns {*}

*/

function encode(str) {

var encoded = CryptoJS.enc..stringify(CryptoJS.enc.Utf8.parse(str));

return encoded;

}

function produce3DesKey(key) {

if (key == undefined || key == null || key == '')

return secretSeed;

if (key.length >= 24) {

return key.substr(0, 24);

}

return key + secretSeed.substr(key.length);

}

function produceSessionKey(key) {

return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase();

}

var sessionKey = ''; // 浏览器cookie中的sessionKey

pstr = ''; // 浏览器发送的需要加密的数据

crypted = encode(encryptWith3DesAndEncode(produceSessionKey(sessionKey), pstr));

console.log(crypted)

解密数据包脚本

const CryptoJS = require('crypto-js')

var APP_KEY_SEED = "terjoycht2014!@#";

var secretSeed = "123456789012345678901234";

function decodeAndDecryptWith3Des(key, cryptedBs64) {

if (key == null || key == '' || cryptedBs64 == null || cryptedBs64 == '') {

return '';

}

var ckey = produce3DesKey(key);

var result = CryptoJS.TripleDES.decrypt(

cryptedBs64, CryptoJS.enc.Utf8.parse(ckey), {

mode: CryptoJS.mode.ECB,

padding: CryptoJS.pad.Pkcs7

});

return result.toString(CryptoJS.enc.Utf8)

}

/**

* 进行解码

* @param bs64

* @returns {*}

*/

function decode(bs64) {

var decoded = CryptoJS.enc.Utf8.stringify(CryptoJS.enc..parse(bs64));

return decoded;

}

function produceSessionKey(key) {

return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase();

}

function produce3DesKey(key) {

if (key == undefined || key == null || key == '')

return secretSeed;

if (key.length >= 24) {

return key.substr(0, 24);

}

return key + secretSeed.substr(key.length);

}

sessionKey = ""; // 存储在浏览器Cookie中的sessionKey

let data = ""; // 要解密的数据,可以是请求包,也可以是服务端返回包的数据

let crypted = decodeAndDecryptWith3Des(produceSessionKey(sessionKey), decode(data));

console.log(crypted);

脚本使用对数据进行加解密如下:

  1. 获取sessionKey

  1. 数据解密

  1. 数据加密(对上面解密出的请求包进行加密,确认加密效果一致)

至此已完成数据包的加解密,当准备进行渗透测试时,发现只要修改数据包,服务端就会返回401错误

然后问了主管,发现请求包中存在签名sign,还需要修改数据包签名,于是查看js中的签名函数,编写生成签名脚本

签名函数如下

/***

* 根据给出的数据产生签名

* @param sessionid

* @param timestamp

* @param random

* @param crypted 加密之后的请求参数

* @returns {string}

*/

function produceSign(sessionid, timestamp, random, crypted) {

var arr = new Array();

arr.push("sessionid=" + sessionid);

arr.push("apptype=" + APP_TYPE);

arr.push("iversion=" + APP_IVERSION);

arr.push("versioncode=" + APP_VERSION);

if (crypted != "")

arr.push("data=" + crypted);

arr.push("timestamp=" + timestamp);

arr.push("random=" + random);

arr.push("secretkey=" + produceSessionKey($.cookie("sessionKey")));

arr.sort();

var signString = "[" + arr.join() + "]";

return CryptoJS.MD5(signString).toString().toUpperCase();

}

签名脚本如下

const CryptoJS = require('crypto-js')

var APP_KEY_SEED = "terjoycht2014!@#";

var APP_TYPE = "20";

var APP_VERSION = "388";

var APP_IVERSION = "2";

/***

* 根据给出的数据产生签名

* @param sessionid

* @param timestamp

* @param random

* @param crypted 加密之后的请求参数

* @returns {string}

*/

function produceSign(sessionid, timestamp, random, crypted) {

var arr = new Array();

arr.push("sessionid=" + sessionid);

arr.push("apptype=" + APP_TYPE);

arr.push("iversion=" + APP_IVERSION);

arr.push("versioncode=" + APP_VERSION);

if (crypted != "")

arr.push("data=" + crypted);

arr.push("timestamp=" + timestamp);

arr.push("random=" + random);

arr.push("secretkey=" + produceSessionKey(sessionKey));

arr.sort();

var signString = "[" + arr.join() + "]";

return CryptoJS.MD5(signString).toString().toUpperCase();

}

function produceSessionKey(key) {

return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase();

}

var sessionId = ""; // 请求头中的sessionId

var sessionKey = ""; // 浏览器Cookie中的sessionKey

var timestamp = ""; // 请求包中的 timestamp

var random = ""; // 请求包中random

var crypted = ""; // 加密后的请求数据

let sign = produceSign(sessionId,

timestamp,

random,

crypted)

console.log(sign)

但是用编写的签名脚本生成签名后,服务端依旧返回401

感觉哪里不对,于是对最初的请求进行计算签名,查看是否和请求中的签名一致

生成的签名为B1576578C9D96654E7FBF961907DD6B6, 原始请求签名为B467D7093028A1B4E9BB97492B76EF9F,两者并不一致,猜测该网站开发人员修改了签名中的部分配置参数,为了找寻修改后的配置参数,于是进行chrome debug

0x04 chrome debug 进行签名破解

首先进入后台,之后打开chrome开发人员工具,在Sources菜单处,找到页面js源代码位置,因为该网站对源代码进行了打包,因此js文件分为app , mainfest 和 vendor这三类,只用查看app的js文件即可,app的js文件为该网站开发人员的开发代码打包后的文件,另外需要点击pretty-print,方便调试

之后找到一处可控的查询位置处,点击查询

抓包,获取请求接口

在js源代码中搜索该接口,因查询到该接口有4处,于是在该4处全打上断点

打上断点后,再一次点击查询,chrome debug开始

调试跟踪找到签名函数调用位置后,在签名函数的调用位置打上断点,然后点击Resume script excution

取消之前接口处的4个断点,仅留该签名函数处的断点,重新点击查询,chrome debug开始

点击箭头处,深入到函数执行内部

两个箭头搭配使用

寻找到全局参数的不同点,即开发人员将APP_TYPE 这个参数从之前的js文件中的20修改为了42

之后修改签名脚本中的该参数,调用签名脚本获取签名,并和之前请求包中的签名进行比对,结果一致,成功获取签名,改包之后也可生成新的签名,成功发包

修改后签名脚本如下:

const CryptoJS = require('crypto-js')

var APP_KEY_SEED = "terjoycht2014!@#";

var APP_TYPE = "42";

var APP_VERSION = "388";

var APP_IVERSION = "2";

/***

* 根据给出的数据产生签名

* @param sessionid

* @param timestamp

* @param random

* @param crypted 加密之后的请求参数

* @returns {string}

*/

function produceSign(sessionid, timestamp, random, crypted) {

var arr = new Array();

arr.push("sessionid=" + sessionid);

arr.push("apptype=" + APP_TYPE);

arr.push("iversion=" + APP_IVERSION);

arr.push("versioncode=" + APP_VERSION);

if (crypted != "")

arr.push("data=" + crypted);

arr.push("timestamp=" + timestamp);

arr.push("random=" + random);

arr.push("secretkey=" + produceSessionKey(sessionKey));

arr.sort();

var signString = "[" + arr.join() + "]";

return CryptoJS.MD5(signString).toString().toUpperCase();

}

function produceSessionKey(key) {

return CryptoJS.MD5(APP_KEY_SEED + key).toString().toUpperCase();

}

var sessionId = ""; // 请求头中的sessionId

var sessionKey = ""; // 浏览器Cookie中的sessionKey

var timestamp = ""; // 请求包中的 timestamp

var random = ""; // 请求包中random

var crypted = ""; // 加密后的请求数据

let sign = produceSign(sessionId,

timestamp,

random,

crypted)

console.log(sign)

0x05 后记

这里主要记录写这篇文章复盘时的一些新发现

  • 因为sessionKey是存储在cookie中的,因此不用特意写脚本获取,可以直接在浏览器的cookie中找到sessionKey
  • 在复盘的时候抓包,突然发现apptype这个值在某些请求包中自带了,不需要特意通过chrome debug获取

近期渗透测试基础课程有公开课,跟着大佬学挖漏洞!可留言发课程哦!

*本文章仅供技术交流分享,请勿做未授权违法攻击,雨笋教育不负任何责任。具体请参考《网络安全法》。

CTF高手教你如何实现文件加解密破解相关推荐

  1. java实现对文件加解密操作

    源文件: 加密后的文件: 解密后的文件: package com.gblfy.test;import java.io.*;/*** java 实现对文件加解密的方法** @author gblfy* ...

  2. GPG对文件加解密的简单实现

    解密时如何覆盖已经存在的解密后文件,而不是弹出询问框是否覆盖 初识GPG 加解密实现 gui加解密 生成密钥 命令行方式加解密 信任度设置 方法一 方法二 参考链接 解密时如何覆盖已经存在的解密后文件 ...

  3. 基于CH375的嵌入式USB文件加解密系统的设计

    基于CH375的嵌入式USB文件加解密系统的设计 [日期:2008-7-21] 来源:电子技术应用  作者:华中科技大学 电子与信息工程系, 湖北 武汉 430074 [字体:大 中 小] <s ...

  4. 文件加解密(任意文件类型的文件)

    直接上代码: 支持任意类型的文件的加密和解密 可以自由修改密钥 需要修改文件路径 文件加解密. #define _CRT_SECURE_NO_WARNINGS #include <stdlib. ...

  5. 国密 SM4 文件加解密

    国密算法SM4 对文件加解密 说明:调用开源bcprov-jdk15on 加密算法工具,使用SM4算法,对文件进行加密.解密:文件流的操作使用hutool工具包来实现. 引用依赖 <depend ...

  6. 密码技术应用--SM4文件加解密

    记录一下对一些稍大文件进行SM4加解密的实现,这里只列出了核心代码,其他不涉及的代码或者有任何疑问可以查看我之前写的密码技术专题博客 var key = []byte("1234567812 ...

  7. 密码技术应用--AES文件加解密

    记录一下对一些稍大文件进行AES加解密的实现,这里只列出了核心代码,其他不涉及的代码或者有任何疑问可以查看我之前写的密码技术专题博客 var key = []byte("1234567812 ...

  8. 文件加解密,文件操作

     1.fseek,ftell,fread,fwrite(简单文件加密) #define _CRT_SECURE_NO_WARNINGS  //去掉安全检查 #include <stdio.h ...

  9. android文件加解密开发,Android文件加密解密的实现

    [实例简介] 最近项目中需要用到加解密功能,言外之意就是不想让人家在反编译后通过不走心就能获取文件里一些看似有用的信息 [实例截图] [核心代码] TestCipher └── TestCipher ...

最新文章

  1. C#中string与byte[]的转换帮助类
  2. 《强化学习周刊》第21期:EMNLP 2020-2021强化学习的最新研究与应用
  3. C++for循环中i++与++i的区别
  4. SAP 电商云 Spartacus UI 4.1 版本的延迟加载技术介绍
  5. 对MATLAB课程教程的建议,关于MATLAB课程教学的几点探讨
  6. Django连接现有mysql数据库
  7. 计算(a+b)/c的值(信息学奥赛一本通-T1008)
  8. 机器学习算法(二):基于决策数的分类预测
  9. ubuntu16.04中文乱码解决方案
  10. opencv 平滑处理
  11. 3.3 keras模型构建的三种方式
  12. Q107:Linux系统下GDB对PBRT-V3进行debug
  13. 【已解决】Could not get lock /var/lib/dpkg/lock-frontend
  14. 网络基础---NAT穿越技术
  15. python对行为进行推理_一道有意思推理题,用python来解答
  16. WINVNC(二)omni_thread
  17. uclinux系统简介
  18. 票房拐点之后的影院并购,继续“小而频”还是大洗牌
  19. 假如银行利率如下所示,请分别计算存款10000元,活期1年、活期2年、定期1年、定期2年后的本息合计。(结果四舍五入,不保留小数位。使用Math.round(double d)实现
  20. 使用C++计算3次牛顿插值法

热门文章

  1. 后端程序员生产力工具合集
  2. 华三(H3C)单臂路由
  3. 电源中104电容的作用
  4. 【视觉-单目三维重建-理论篇】单目视觉三维重建----论文调研1
  5. 水浒传 第三十六回 没遮拦追赶及时雨 船火儿夜闹浔阳江
  6. Dynamic CRM 2016 IFD配置(1)证书颁发机构配置
  7. android简易播放器2:activity和service同步显示
  8. HDU 5441并查集 by cyl
  9. python 多因素方差分析_SPSS分析技术:多元方差分析
  10. 我理解的国密知识与加密过程