AndroidApp加密数据明文抓取测试方法——hook方式
0x00 前言
在做移动安全的app渗透或者说移动app的漏洞挖掘时,往往会碰到一种情况:好不容易绕过了app的反抓包机制,通过burp抓到了app传输的数据包,这时想对这部分数据做一些爆破、篡改之类的测试,却发现关键数据进行了加密处理,那么这时就不得不首先解决一下数据解密截取的问题。
0x01 环境搭建
(一).抓包环境
首先是抓包环境,需要针对app的反抓包机制做一些绕过,这不是本篇文章讨论重点,因此在另一篇文章中做介绍。
(二).frida框架
其次是我们做hook操作,需要依赖一些hook框架,来帮助我们更好的完成操作,这里我选用的是frida框架:简单安装教程如下:
1)首先安装python3环境,之后使用pip工具安装frida框架:
pip3 install frida(默认安装最新版)
当然可以指定版本安装:
pip3 install frida==14.2.18 frida-tools==9.2.4
!!注意frida与frida-tools的版本对应关系,可以去github查找
2)再安装frida-server到手机(如果是模拟器注意安装包的选取),下载后解压传输安装赋权启用即可
adb push xxxfdserverxxx /data/local/tmp/fs14218
cd /data/local/tmp
chmod 777 fs14218
./fs14218
//启动
之后另起一个cmd窗口,输入命令查看手机上的信息:
frida-ps -Uai
如果能看到如下一类信息,即安装成功。
(三).反编译工具 jadx-gui
直接下载打开即可食用
0x03 apk逆向&&明文抓取
(零)思路介绍
首先配置好代理,抓包看一下:
可以看到,app传输的数据都经过了一些未知的复杂加密处理,sign字段看过去类似是经过摘要算法得到的签名值之类,那么这里如果我们想要对data、timestamp等字段做一个测试,显然我们是不能直接修改的(可以更改一下看看效果)
既然这里不是一些简单的base64编码之类的处理,不能采取直接解码,又一眼看不出这里的加密方式(密码算法逆向),那么此时我们的思路还有什么呢?
可以想象一下数据的传输过程:
明文数据-->app调用加密算法进行加密--->app调用摘要算法计算消息摘要--->app发送请求,传输加密数据---->........
一眼看过去,可操作的攻击思路是不是就清晰了?
1.可以对加密算法进行逆向,完全掌控加密过程
2.对明文数据在进行加密之前进行截取,并做更改
这篇文章就介绍第二种攻击思路:
对app进行反编译,找到明文传输路径,通过hook方式在加密操作之前,对app的明文数据进行篡改操作,这样的篡改操作在加密与摘要之前,在app的验证机制看来完全合法。接下来实操尝试一下:
(一)反编译及寻找可疑方法调用栈
打开jadx-gui,将得到的app安装包丢进去反编译一下(有壳的先砸壳处理一下):
如何寻找可疑方法呢?这里介绍一种最常用的简单方法:
回头看一下我们抓到的包:
可以看到一些关键字,那么我们就直接在反编译的代码中去检索这些关键字,大概率就可以找到关键位置(如果源码做了混淆,就得采用一些其他办法,这里不做讨论):搜索一下sign关键字:
有所发现,一一进行查看即可,有经验的话,可能可以猜到一些特征:sign是网络请求中的字段等等...随意联想。
经过检查,找到最具可疑性的目标:
出现了请求包中出现的sign、_ver字段,以及可疑的DATA,继续跟进一下:
原来,DATA就是我们的data,hahah~~~
回头追踪sign字段:发现与e.c.a.i.c.a()方法相关,跟过去看一下
阅读代码,找到源头,原来sign值就是将传入的数据+一定的密钥通过encrypt()方法处理得到的,那么这里就可以作为我们的切入点,hook验证一下看看我们的猜测对不对,数据是不是真的经过了这里:
脚本如下:
//原理:APP应用程序在处理数据、提交数据时,通常会将数据存放于集合中,而HashMap又是其中较为常用的。因此,可以通过Hook HashMap的put方法来定位关键代码所在的位置。
// 使用Java.perform包装代码块以确保在正确的线程和类加载器上下文中执行
Java.perform(function () {
// 获取 java.util.HashMap 类引用
var HashMap = Java.use('java.util.HashMap');
// Hook HashMap 的 put 方法
HashMap.put.implementation = function (key, value) {
// 将 Java String 转换为 JavaScript 字符串
var keyStr = key ? key.toString() : '';
// // 如果 key 是 "data" 或 "sign"
if (keyStr === 'data' || keyStr === 'sign') {
// 打印 key 和 value
console.log('Key:', keyStr);
console.log('Value:', value);
// 打印调用栈
console.log('Call Stack:', Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()));
}
// console.log('Key:', keyStr);
// console.log('Value:', value);
// 调用原始的 put 方法实现,并返回结果
return this.put.call(this, key, value);
};
});
使用frida注入脚本:
首先在手机开启frida服务器:
./fs14218
之后执行命令,注入脚本:
frida -U -f 包名 -l hook脚本 --no-pause
如果此时app在前台运行,可以选择-UF模式
frida -UF -l hook脚本
刷新一下app试试:
果然,看到了相应的数据,证明我们的猜测无误,同时脚本也打印出了调用栈。
根据分析调用栈,寻找源头:(找与app包名相关的东西):找到:
e.c.a.i.d.a()方法,追踪到e.c.a.i.c.a()方法,和前面我们的分析吻合。那么这里我们就找到了精准位置
将该方法复制为一段frida代码片段,hook一下看看:
let c = Java.use("e.c.a.i.c");
c["a"].overload('java.lang.String').implementation = function (str) {
console.log(`c.a is called: str=${str}`);
let result = this["a"](str);
console.log(`c.a result=${result}`);
return result;
};
改成简单的hook脚本:1.js
Java.perform(function () {
let c = Java.use("e.c.a.i.c");
c["a"].overload('java.lang.String').implementation = function (str) {
console.log(`c.a is called: str=${str}`);
let result = this["a"](str);
console.log(`c.a result=${result}`);
return result;
};
});
注入一下看看:方法如上
截取到了想要的数据。切入点无误,开始操作,抓取明文:
(二)编写脚本,抓取明文(被动调用)
前面我们找到了精准位置如下:
因此这里就编写对应的hook脚本与python脚本,提取控制转发明文即可达成我们的目的:
首先简单的只打印看一下:
Java.perform(function () {
var c = Java.use("e.c.a.i.c");
var EncryptUtil = Java.use("com.qq.lib.EncryptUtil");
c.a.overload('java.lang.String').implementation = function(str){
console.log("\n请求加密前明文:\n",str);
return this.a(str);
}
EncryptUtil.decrypt.implementation = function(str,str2){
console.log("\n响应解密后明文:\n",this.decrypt(str,str2));
return this.decrypt(str,str2);
}
});
看到,明文已经抓取成功了,接下来就是需要丰富一下脚本功能,把请求与响应转发到我们的burp工具去,方便我们进行改包测试(这里我利用python来实现转发请求,调用hook脚本等功能)
python脚本如下(构建镜像服务器):
import argparse
from threading import Thread
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
import requests
import frida
# 定义端口号
ECHO_PORT = 28080
BURP_PORT = 8080
# 创建一个请求处理器类,继承自 BaseHTTPRequestHandler
class RequestHandler(BaseHTTPRequestHandler):
# 定义处理请求的方法
def do_REQUEST(self):
content_length = int(self.headers.get('content-length', 0))
self.send_response(200)
self.end_headers()
self.wfile.write(self.rfile.read(content_length))
# 处理响应的方法与处理请求的方法相同
do_RESPONSE = do_REQUEST
def echo_server_thread():
print('start echo server at port {}'.format(ECHO_PORT))
server = HTTPServer(('', ECHO_PORT), RequestHandler)
server.serve_forever()
t = Thread(target=echo_server_thread)
t.daemon = True
t.start()
# 添加命令行参数解析
parser = argparse.ArgumentParser(description="Frida Python script with command line arguments.")
parser.add_argument("process_name", help="The process name you want to attach to.")
parser.add_argument("js_file", help="The path to the Frida JS script.")
args = parser.parse_args()
# 通过 USB 设备附加到指定进程
session = frida.get_usb_device().attach(args.process_name)
# 加载 Frida JS 脚本
with open(args.js_file, "r", encoding='utf-8') as f:
js_code = f.read()
script = session.create_script(js_code)
# 定义处理来自 Frida 脚本的消息的函数
def on_message(message, data):
if message['type'] == 'send':
payload = message['payload']
_type, data = payload['type'], payload['data']
# print(message)
if _type == 'REQ':
data = str(data)
r = requests.request('REQUEST', 'http://127.0.0.1:{}/'.format(ECHO_PORT),
proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)},
data=data.encode('utf-8'))
script.post({'type': 'NEW_REQ', 'payload': r.text})
elif _type == 'RESP':
r = requests.request('RESPONSE', 'http://127.0.0.1:{}/'.format(ECHO_PORT),
proxies={'http': 'http://127.0.0.1:{}'.format(BURP_PORT)},
data=data.encode('utf-8'))
script.post({'type': 'NEW_RESP', 'payload': r.text})
# 为 Frida 脚本设置消息处理函数
script.on('message', on_message)
script.load()
# 使主线程保持运行,等待用户输入
sys.stdin.read()
hook脚本如下:
// 使用 Java.perform 函数确保 Frida 在 Java 虚拟机中运行此代码
Java.perform(function () {
// 获取 "e.c.a.i.c" 类
var c = Java.use("e.c.a.i.c");
// 获取 "com.qq.lib.EncryptUtil" 类
var EncryptUtil = Java.use("com.qq.lib.EncryptUtil");
// 定义一个变量来存储从 Python 接收到的新字符串
var newStr;
// Hook "e.c.a.i.c" 类中的 "a" 方法,参数类型为 "java.lang.String"
c.a.overload('java.lang.String').implementation = function (str) {
// 将原始参数转换为 JSON 字符串并将其发送给 Python
send({ type: 'REQ', data: JSON.stringify(str) });
// 等待来自 Python 的新参数
var newArgs = recv('NEW_REQ', function (data) {
// 将从 Python 接收到的 JSON 字符串转换为 JavaScript 对象
newStr = JSON.parse(data.payload);
});
// 等待 recv 函数处理完数据
newArgs.wait();
// 使用新参数调用原始方法并返回结果
return this.a(newStr);
}
// 定义一个变量来存储从 Python 接收到的新明文
var newPlaintext;
// Hook "com.qq.lib.EncryptUtil" 类中的 "decrypt" 方法
EncryptUtil.decrypt.implementation = function (str, str2) {
// 使用原始参数调用原始解密方法并将结果存储在 "plaintext" 变量中
var plaintext = this.decrypt(str, str2);
// 将解密后的明文转换为 JSON 字符串并将其发送给 Python
send({ type: 'RESP', data: JSON.stringify(plaintext) });
// 等待来自 Python 的新明文
var newResult = recv('NEW_RESP', function (data) {
// 将从 Python 接收到的 JSON 字符串转换为 JavaScript 对象
newPlaintext = JSON.parse(data.payload);
});
// 等待 recv 函数处理完数据
newResult.wait();
// 返回新的明文
return newPlaintext;
}
});
记得在burp监听相应的端口:测试效果
启动python脚本:python3 main.py org.tdyoa.mcdfmv(包名为org.tdyoa.mcdfmv) hook.js
burp上接收到python转发过来的响应包,我们进行修改试试:以用户名为例:
原用户名:
改包到新用户名:xiaoheilaoshi123
已经成功篡改,可以开始愉快的测试啦!!!
(三)进阶-主动调用
用了前面的方法,有的小伙伴肯定就要说了,每次测试,我都要去点app,操作手机,好麻烦啊,算了算了。别急,咱们这就解决一下
前面我们知道,我们是通过反编译找到关键函数点,然后用frida进行hook打印寻找调用栈,那么不妨拓展一下省力思路:
能不能找到关键方法,然后脚本直接调用方法,进行hook,这样我们就只需要把app开着放在一边,直接去运行脚本,就可以完成操作?
显然是可以的(就以同一个app中data字段的加密解密方法为例):
编写一下脚本如下(找到该方法相关的类名、密钥等信息,利用python提供的frida、flask库实现frida的功能):
#rpchook.py
import frida
import sys
from flask import Flask, request, jsonify
app = Flask(__name__)
device = frida.get_usb_device()
session = device.attach("org.tdyoa.mcdfmv") # 使用你的APP包名替换
@app.route('/decrypt', methods=['POST'])
def decrypt():
encrypted_data = request.json['encrypted_data']
decrypt_key = request.json['decrypt_key']
script = session.create_script("""
Java.perform(function() {
var EncryptUtil = Java.use('com.qq.lib.EncryptUtil');
var encrypted_data = '%s';
var decrypt_key = '%s';
var decrypted_data = EncryptUtil.decrypt(encrypted_data, decrypt_key);
send(decrypted_data);
});
""" % (encrypted_data, decrypt_key))
decrypted_data = None
def on_message(message, data):
nonlocal decrypted_data
if message['type'] == 'send':
decrypted_data = message['payload']
script.on('message', on_message)
script.load()
script.unload()
return jsonify({'decrypted_data': decrypted_data})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
#testjiemi.py
import requests
url = "http://localhost:5000/decrypt"
headers = {"Content-Type": "application/json"}
data = {
"encrypted_data": "8039C68F5AAE16B87FED41779E66ADC704ACC42028F9D422F97BFAB7D3288972139060B21A850DA4DB0107F6D2165076DCB064BA6C4796B61C4E2AF0F3B02947F41F6D7E6454F6FD06117A714093FDD71A46A017A490E9E51B5D73A403E34F5BFF83A1F2963935B12B04E960BD31540F8773AFB6A6B916FE66531B0D8891B1A0F1B9A0EA4DE7976FC01E3DE4DACEB8BA07D2086F845D2191A06A20626B9C1906B1A7AC02275C9DB8AF88E2D5254F9C7588104E4668C1229365850FA0BB6C2B55CB2DE8D3186C6F5A6DD5E3C1EE4EB8D50E02C7DA6C388D4A0D94C31D180B882E2C05CD15D13F226BD0966BE78FC2942FD23862E92D2A5AC3CF3C818D13C20D37680C3E4B7F42101C84EB795289945107EBBB539B27704A083D3D55B2815FD848F359C9ACE6986E1B751F1041CD9B1DBCF35591DAA4C74974F6A9CD57B4082C15A0FB8FD77A5F9F17B7F80F008D5D18D5D0C4D7D842E5BA5BABDDCB52C7A1E4860ED439F4A97FFBDDFE6BDF7BA82CCA479113A1A2FF59A91E0A4C0304CCB1CF0B64D7307F98C810088D49CB4AF9D06C674B3F8C38DAF82D5D1ABE3D3F08CD29763C7233B55960512D3D16FD4412537872EC6BE1E219B145B6F0116BDCC48C811DCE60CAF080198067138DB7BFC7D3C216B9FACF3D5DCF5734E38A6C99EEF23327CF188089820183E4F739022CB64ABF433A61F1B71F4C00B926E81D52486BF70FFA9A20EAB9381C63143A414391DD9D4A1B49701027F7F7BDCF4A6A108B8212FC463063B6DC42C086292F7A6F9B675805E4064327F8FE0ECDCF8FBCC407858A540FC4D47AA9028CC00C8DB9EB41BB6266187EEEBABB5138C1E83EE25C88A83EB20B0EAD4A7B09D45B9D4DEB44D1887C017B10F985021B2231A5F69053468EC0A45B6411561780D85C12E6E59597846FC4014F2ACB28E17175F952A64EAB5E3C1AA520A3821D6340627EA83448F1505B29D51097577A37F32B2F68D3B3BFC2018B8F5AADC41E20D1D9DA4383D4AEC7BBC6F86AE3602EAE99774EF695532B52E05824E025250F43A70137F56B6B6EBE7B8604FF785039D1F38E1CC10D49DA238F11C50B9BBE9E30FE785A5BFC49E28D3A3025A207A82BEC973E3760915A827428E3BA11B9433D95640EB3FA51394A02D543622F1B815A47C918453A2E6A917B00730F89E28DE31DF9184DAFFEC5737789122AA5A35A7ACA32A289031626CCC59251F890A213C893DBC0B17CAA3EDA18A9B365CA3DD3AC1E49445C4B67F63106BEED0A3346F50E18B7FAAE0600908F15E494C28140E2437282481C0687D31D938514D93CA4FF32F274823A4075982517DB18C8F9ED081452ECA3E35BBA8DE95E77612E3DD8449389B67DE4D631605079B609024CE7F03AB83F7BAAD7FF9DE5DCF6255FF78219FF724F0DBEFA29B4506375FC6383828638AB77B19FA0A0057EF8A84CD09CFB28C239CD11042D52D370FD0FC6C2F284BF4287A5735802FBEDF666601EED5EB9029E1C2448E6393E44536FEC7D178E8BD3843DA4E0B6137C590AD23310F52CD3B8F5AF93081EB3C5D01003AC9D272FECC40C5808EBFC9533421C84C8C5B1C372BF9146107F6B7946B8ECC3D0773AA1211F953B2773E18C06C57949163BADA100A4BC0A3E6A7555914DA70C2537C1EB709C7C54F8BE3C8D2617381BCDEDB9790B1E673EC7B73BBE9AA2B45139EEFC17AC6416215D5B02219968B4DF9311AD8EF3992910932A3B7B72CA9BA5AC66BDDF45DB272D469D08607E4B12201C07BF0EB8FBDA7F41D343ADB8217E289D730EECCA4D84027C9A5AD4619C9326B452AEC88F2C6B1F0A004C7B45594CA5888C022E4975C9E7D337519336D0607AD32BCEE34557DE7374EECC55717AC09139155D7932B19189C48AF9D90C157805B2B97145BE7348637DBA0F3C6A7F91EAEE489FB309CC60041F83EAD4845AC1F2F4EFE06326575602A4715D6E4A296459D5FFB466A5089EC45FEC0158302388A96D725228855A3D85CAC933DBCE2153209069EEF2DE1DA4780A7616492295D81397D581589E13FEAE061D57CCC6D427131E8A7FDB9AEB454F2F7555E598C056A46662E9E0BF96A1B9AA5E09423E6653710A16177AB658F2E14BA9C83639BB64DFF4E4F1CA21143FD2E01B62CAF69D6BC559BAD2B76749919AE6F1648C0E9608B39746A13CFA42F47C9AC52A2198438CFA4283D2B6CFF961D69C40F6DF36DCE6F8DCED811E0197427BDD7F135050A49D004C262D1AECA4417F25A823D95FA921C491853783F56914ABDA14384163640DDC7FA36C2C302A1EF396DF846C3CC8A130583B7C6826771948D60029AA14CCE816D84B113B1E418A2361AA0A4E64BFD5F6DC1EDF9DCC548BC6F7A329D3FABDE9CF1AA6692DF2F67A53D5E951804F769BE31CEA7681F63C8AADCD629460564D31F43843125A35B7CE4EDF8A6D79E15363CF74F955AD6029DD6C0B1EBF8031AC52B502DA90CACF9AEB5FEAD5A19D85216C12F03160CB33F015E461CD2AA6CCFCD68865E53930B544C5364E00A7821B8DE76116D0BD4A3EE231801C0B44C12CC11376BE0437962FD43C677D70D9C39AC2A479550A5A8DFD1C5678964FE3C207AE124AD3D0B406573A72DB2CBDA5421647C3D625D1C29A58395421A186A7BD0B48AA8CD1FD5CCF73DEE9719ACC7837A47B9B0CC436ACAB936872F72F1E65C36FA02CB0B218F1B1D37B552713627F4FE400882185DA042CBD7EB79E90AF628692141704D3B72948536CF62C427477370B65B18D43C9B2D02D6E376F0ABB59916B6C7FBFC3CBD7018BB7F4BE1F91D381A1E059B1D0953FC23DF55A648D50AD4BC44903BCD68E8D6E61090C8DF4A06532DEC45FF9F69CED82B4909563AF336AA339F1684A6AE7D6DBBCD32A897A99049BEBFF8847357DE40A437BE4E49DCF41D2DE7F9804CECE81B7E19F291480F1D0279506020D7960F9563C731F5C23BEC2FE3FC754A8F7D4F6E9C5703A149703ACFCC67068083739EF1E646977FF53F7C11333BEFA0EF7C824D697AA62774BAB7B616ECD2B99A4D3F3D56D01C09F374869B2577E4BACC91539DDE51EB484EEDBF5DFF84E302510E9AB39FB9269511CB8D7788EB0567C5A897F7FDA5D815201DB4CD0C5DEA6836A6C5BC62424640A9869B743D499A865C65D1EF9F9651722D38002DADC41025C1C63B6E3E1022F085A92882E1DC4F23FC9A9A2BF2609E6DF54AFB36F6F136909C7",
"decrypt_key": "PT0dPVxYXglbDARZXwlfXwwMDApYDgReWV4JXl4ICAtfWAxcJD4WCD8SFRYCFQ=="
}
response = requests.post(url, json=data, headers=headers)
print(response.json())
运行效果如下:
好啦,本篇文章讨论就到这里啦,小伙伴们开始愉快的玩耍吧!!!
AndroidApp加密数据明文抓取测试方法——hook方式的更多相关文章
- 【Hibernate学习】 —— 抓取策略(注解方式)
当应用程序须要在关联关系间进行导航的时候.hibernate怎样获取关联对象的策略. 抓取策略的方式: FetchType.LAZY:懒载入.载入一个实体时.定义懒载入的属性不会立即从数据库中载入. ...
- Vue项目中jsonp抓取数据实现方式
因为最近在做vue的项目,在前端做数据的时候遇到了数据抓取的难题,查了一些资料,自己也研究了一下,总体来说是搞出来了(基于黄奕老师的项目找出来的经验),废话不多说,直接上代码 ------------ ...
- 「拉勾网」薪资调查的小爬虫,并将抓取结果保存到excel中
学习Python也有一段时间了,各种理论知识大体上也算略知一二了,今天就进入实战演练:通过Python来编写一个拉勾网薪资调查的小爬虫. 第一步:分析网站的请求过程 我们在查看拉勾网上的招聘信息的时候 ...
- PowerShell定时抓取屏幕图像
昨天的博文写了定时记录操作系统行为,其实说白了就是抓取了击键的记录和对应窗口的标题栏,而很多应用程序标题栏又包含当时记录的文件路径和文件名,用这种方式可以大致记录操作了哪些程序,打开了哪些文 ...
- Java抓取网页数据(原网页+Javascript返回数据)
有时候由于种种原因,我们需要采集某个网站的数据,但由于不同网站对数据的显示方式略有不同! 本文就用Java给大家演示如何抓取网站的数据:(1)抓取原网页数据:(2)抓取网页Javascript返回的数 ...
- Java抓取网页数据(原来的页面+Javascript返回数据)
转载请注明出处! 原文链接:http://blog.csdn.net/zgyulongfei/article/details/7909006 有时候因为种种原因,我们须要採集某个站点的数据,但因为不同 ...
- Nutch抓取流程
nutch抓取流程注入起始url(inject).生成爬取列表(generate).爬取(fetch).解析网页内容(parse).更新url数据库(updatedb)1:注入起始url(inject ...
- Java实现多种方式的http数据抓取
前言: 时下互联网第一波的浪潮已消逝,随着而来的基于万千数据的物联网时代,因而数据成为企业的重要战略资源之一.基于数据抓取技术,本文介绍了java相关抓取工具,并附上demo源码供感兴趣的朋友测试! ...
- php抓取页面的几种方式
在做一些天气预报或者RSS订阅的程序时,往往 需要抓取非本地文件,一般情况下都是利用php模拟浏览器的访问,通过http请求访问url地址, 然后得到html源代码或者xml数据,得到数据我们不能直接 ...
- 抓取Js动态生成数据且以滚动页面方式分页的网页
代码也可以从我的开源项目HtmlExtractor中获取. 当我们在进行数据抓取的时候,如果目标网站是以Js的方式动态生成数据且以滚动页面的方式进行分页,那么我们该如何抓取呢? 如类似今日头条这样的网 ...
随机推荐
- Docker部署Reids单机
一.Redis镜像拉取 docker pull redis 指定版本 docker pull redis:5.0.8 二.Redis单实例安装 1.创建容器挂在目录 (-p 递归创建目录,上级目录不存 ...
- ASP.NET实现前台调用后台变量或者方法
前台页面 <div> <%= Name %> </div> <div> <%= getName() %> </div> 后台代码 ...
- Exception processing template "index": An error happened during template rendering
问题所在 brandList[0].brandId出错,要不就改正,要不就把他删了,注释也会报错.
- EVE如何提升名望值
目录 背景介绍 简介 名望值划分 军团名望值 利弊 背景介绍 玩eve将近3个星期,开着毒蜥级刷1级代理人任务感觉没有一点难度,想尽快刷3.4级代理任务,而我目前能够接到的最高代理任务也就才1级. ...
- NX1946_MoldWizard 注塑模向导建立标准库
NX1946_MoldWizard 注塑模向导建立标准库
- 定制centos发行版
//轻量级Centos定制发行版==================================================================================== ...
- sap shift语法
shift xxx LEFT DELETING LEADING / RIGHT DELETING TRAILING mask 语法. xxx中的第一或最后一个字符出现在mask中,则xxx左移或者右 ...
- NSIS 将一整个文件夹拷贝
在做安装包的时候,有时候需要将文件夹以及文件夹下面所包含的所有文件夹和文件都拷贝到目标文件夹,一下有两种方法可以连同文件夹一起拷贝: 各文件的位置如下: 其中src 文件夹下的文件如下: 一开始dst ...
- ansible用authorized_key模块批量推送密钥到受控主机实现免密登录
一,ansible的authorized_key模块的用途 用来配置密钥实现免密登录: ansible所在的主控机生成密钥后,如何把公钥上传到受控端? 当然可以用ssh-copy-id命令逐台手动处理 ...
- 纠删码在实时视频流中的应用丨Dev for Dev 专栏
本文为「Dev for Dev 专栏」系列内容,作者为声网网络体验团队王瑞. 01 背景 在实时音视频通话中,音视频质量受网络丢包影响较大,特别是对于视频. 为什么视频对丢包更敏感呢?通常来说,音频的 ...