Java反序列化漏洞之殇
ref:https://xz.aliyun.com/t/2043
小结:
3.2.2版本之前的Apache-CommonsCollections存在该漏洞(不只该包)
1.漏洞触发场景
在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:
1)HTTP请求中的参数,cookies以及Parameters。
2)RMI协议,被广泛使用的RMI协议完全基于序列化
4)JMX 同样用于处理序列化对象
5)自定义协议 用来接收与发送原始的java对象
2. 漏洞挖掘
(1)确定反序列化输入点
首先应找出readObject方法调用,在找到之后进行下一步的注入操作。一般可以通过以下方法进行查找:
1)源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
2)对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等。注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。
(2)再考察应用的Class Path中是否包含Apache Commons Collections库,这一点需要考虑?
如果在顶层容器中例如tomcat/jetty等包含commcollectios3.1,而在业务中引用commcollections3.2,则仍可能触发漏洞。
Java反序列化漏洞之殇
关于反序列化漏洞分析及利用研究的文章不少,但鲜有检测及修复方面的介绍,本文旨站在应用安全的角度,从安全编码、代码审计、漏洞检测及修复方案对反序列化漏洞进行详细分享。
概述
序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
漏洞成因
序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
漏洞代码示例如下:
......
//读取输入流,并转换对象
InputStream in=request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//恢复对象
ois.readObject();
ois.close();
这里特别要注意的是非预期的对象,正因为此java标准库及大量第三方公共类库成为反序列化漏洞利用的关键。安全研究人员已经发现大量利用反序列化漏洞执行任意代码的方法,最让大家熟悉的是Gabriel Lawrence和Chris Frohoff在《Marshalling Pickles how deserializing objects can ruin your day》中提出的利用Apache Commons Collection实现任意代码执行。此后安全研究人员也陆续爆出XML、Json、Yaml等反序列化的相关漏洞。
除了commons-collections 3.1可以用来利用java反序列化漏洞,还有更多第三方库同样可以用来利用反序列化漏洞并执行任意代码,部分如下:
- commons-fileupload 1.3.1
- commons-io 2.4
- commons-collections 3.1
- commons-logging 1.2
- commons-beanutils 1.9.2
- org.slf4j:slf4j-api 1.7.21
- com.mchange:mchange-commons-java 0.2.11
- org.apache.commons:commons-collections 4.0
- com.mchange:c3p0 0.9.5.2
- org.beanshell:bsh 2.0b5
- org.codehaus.groovy:groovy 2.3.9
- ......
Java反序列化详解
序列化数据结构
通过查看序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号0x00 05的数据。此外还包含了类名、成员变量的类型和个数等。
这里以类SerialObject示例来详细进行介绍Java对象序列化后的数据结构:
public class SerialObject implements Serializable{private static final long serialVersionUID = 5754104541168322017L;private int id;public String name;public SerialObject(int id,String name){this.id=id;this.name=name;}...
}
序列化SerialObject实例后以二进制格式查看:
00000000: aced 0005 7372 0024 636f 6d2e 7878 7878 ....sr.$com.xxxx
00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e xx.sec.web.home.
00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97 SerialObjectO...
00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e .......I..idL..n
00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67 amet..Ljava/lang
00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400 /String;xp....t.
00000060: 0563 7279 696e 0a .cryin.
序列化的数据流以魔术数字和版本号开头,这个值是在调用ObjectOutputStream序列化时,由writeStreamHeader方法写入:
protected void writeStreamHeader() throws IOException {bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACEDbout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5}
序列化后的SerialObject对象详细结构:
STREAM_MAGIC (2 bytes) 0xACED
STREAM_VERSION (2 bytes) 0x0005TC_OBJECT (1 byte) 0x73TC_CLASSDESC (1 byte) 0x72classNamelength (2 bytes) 0x24 = 36text (36 bytes) com.xxxxxx.sec.web.home.SerialObjectserialVersionUID (8 bytes) 0x4FDAAF97F8CCC5E1 = 5754104541168322017classDescInfoclassDescFlags (1 byte) 0x02 = SC_SERIALIZABLEfieldscount (2 bytes) 2field[0]primitiveDescprim_typecode (1 byte) I = integerfieldNamelength (2 bytes) 2text (2 bytes) idfield[1]objectDescobj_typecode (1 byte) L = objectfieldNamelength (2 bytes) 4text (4 bytes) nameclassName1TC_STRING (1 byte) 0x74length (2 bytes) 0x12 = 18text (18 bytes) Ljava/lang/String;classAnnotationTC_ENDBLOCKDATA (1 byte) 0x78superClassDescTC_NULL (1 byte) 0x70classdata[]classdata[0] (4 bytes) 0xe107 = id = 2017classdata[1]TC_STRING (1 byte) 0x74length (2 bytes) 5text (8 bytes) cryin
反序列化过程详解
Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。
如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。
如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了Serializable和Externalizable接口的类的对象才能被序列化。
反序列化漏洞检测方案
代码审计
反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如下:
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
...
同时也要关注存在漏洞的第三方库及版本是否安全。
进阶审计
对于直接获取用户输入进行反序列化操作这种点比较好审计并发现,目前反序列化漏洞已经被谈起太多次了,所以有经验的开发都会在代码中有相应的修复。但并不是所有修复都无懈可击。比如采用黑名单校验的修复方式,对于这种修复可在工程代码中尝试挖掘新的可以利用的’gadget‘。
代码中有使用到反序列化操作,那自身项目工程中肯定存在可以被反序列化的类,包括Java自身、第三方库有大量这样的类,可被反序列化的类有一个特点,就是该类必定实现了Serializable接口,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的。一个典型的示例如下:
public class SerialObject implements Serializable{private static final long serialVersionUID = 5754104541168322017L;private int id;public String name;public SerialObject(int id,String name){this.id=id;this.name=name;}public void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{//执行默认的readObject()方法in.defaultReadObject();}
}
所以在代码审计时对这些类也可进行特别关注,分析并确认是否有可能被发序列化漏洞利用执行任意代码。发现新的可利用的类即可突破使用黑名单进行校验的一些应用。
白盒检测
大型企业的应用很多,每个都人工去审计不现实,往往都有相应的自动化静态代码审计工具,这里以ObjectInputStream.readObject()为例,其它原理也相似。在自动化检测时,可通过实现解析java源代码,检测readObject()方法调用时判断其对象是否为java.io.ObjectOutputStream。如果此时ObjectInputStream对象的初始化参数来自外部请求输入参数则基本可以确定存在反序列化漏洞了。这是只需确认是否存在相应的安全修复即可。 检测方式可参考lgtm.com对于Deserialization of user-controlled data的实现:
/*** @name Deserialization of user-controlled data* @description Deserializing user-controlled data may allow attackers to* execute arbitrary code.* @kind problem* @problem.severity error* @precision high* @id java/unsafe-deserialization* @tags security* external/cwe/cwe-502*/
import java
import semmle.code.java.security.DataFlow
import semmle.code.java.frameworks.Kryo
import semmle.code.java.frameworks.XStream
import semmle.code.java.frameworks.SnakeYamlclass ObjectInputStreamReadObjectMethod extends Method {ObjectInputStreamReadObjectMethod() {this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and(this.hasName("readObject") or this.hasName("readUnshared"))}
}class XMLDecoderReadObjectMethod extends Method {XMLDecoderReadObjectMethod() {this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") andthis.hasName("readObject")}
}class SafeXStream extends FlowSource {SafeXStream() {any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this}
}class SafeKryo extends FlowSource {SafeKryo() {any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this}
}predicate unsafeDeserialization(MethodAccess ma, Expr sink) {exists(Method m | m = ma.getMethod() |m instanceof ObjectInputStreamReadObjectMethod andsink = ma.getQualifier()orm instanceof XMLDecoderReadObjectMethod andsink = ma.getQualifier()orm instanceof XStreamReadObjectMethod andsink = ma.getAnArgument() andnot exists(SafeXStream sxs | sxs.flowsTo(ma.getQualifier()))orm instanceof KryoReadObjectMethod andsink = ma.getAnArgument() andnot exists(SafeKryo sk | sk.flowsTo(ma.getQualifier()))orma instanceof UnsafeSnakeYamlParse andsink = ma.getArgument(0))
}class UnsafeDeserializationSink extends Expr {UnsafeDeserializationSink() {unsafeDeserialization(_, this)}MethodAccess getMethodAccess() { unsafeDeserialization(result, this) }
}from UnsafeDeserializationSink sink, RemoteUserInput source
where source.flowsTo(sink)
select sink.getMethodAccess(), "Unsafe deserialization of $@.", source, "user input"
黑盒检测
调用ysoserial并依次生成各个第三方库的利用payload(也可以先分析依赖第三方包量,调用最多的几个库的paylaod即可),该payload构造为访问特定url链接的payload,根据http访问请求记录判断反序列化漏洞是否利用成功。如:
java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " '
也可通过DNS解析记录确定漏洞是否存在。现成的轮子很多,推荐NickstaDB写的SerialBrute,还有一个针对RMI的测试工具BaRMIe,也很不错~。.
RASP检测
Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。
类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以RASP检测反序列化漏洞时可通过重写ObjectInputStream对象的resolveClass方法获取反序列化的类即可实现对反序列化类的黑名单校验。
百度的开源RASP产品就是使用的这种方法,具体可参考其DeserializationHook.java的实现:
@Overrideprotected MethodVisitor hookMethod(int access, String name, String desc,String signature, String[] exceptions, MethodVisitor mv) {if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) {return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {@Overrideprotected void onMethodEnter() {loadArg(0);invokeStatic(Type.getType(HookHandler.class),new Method("checkDeserializationClass", "(Ljava/io/ObjectStreamClass;)V"));}};}return mv;}
其中检测覆盖的反序列化类黑名单如下:
plugin.register('deserialization', function (params, context) {var deserializationInvalidClazz = ['org.apache.commons.collections.functors.InvokerTransformer','org.apache.commons.collections.functors.InstantiateTransformer','org.apache.commons.collections4.functors.InvokerTransformer','org.apache.commons.collections4.functors.InstantiateTransformer','org.codehaus.groovy.runtime.ConvertedClosure','org.codehaus.groovy.runtime.MethodClosure','org.springframework.beans.factory.ObjectFactory','xalan.internal.xsltc.trax.TemplatesImpl']var clazz = params.clazzfor (var index in deserializationInvalidClazz) {if (clazz === deserializationInvalidClazz[index]) {return {action: 'block',message: '尝试反序列化攻击',confidence: 100}}}return clean
})
攻击检测
通过查看反序列化后的数据,可以看到反序列化数据开头包含两字节的魔术数字,这两个字节始终为十六进制的0xAC ED。接下来是两字节的版本号。我只见到过版本号为5(0x00 05)的数据。考虑到zip、base64各种编码,在攻击检测时可针对该特征进行匹配请求post中是否包含反序列化数据,判断是否为反序列化漏洞攻击。
xxxdeMacBook-Pro:demo xxx$ xxd objectexp 00000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb ionHandlerU.....
但仅从特征匹配只能确定有攻击尝试请求,还不能确定就存在反序列化漏洞,还要结合请求响应、返回内容等综合判断是否确实存在漏洞。
Java反序列化漏洞修复方案
通过Hook resolveClass来校验反序列化的类
通过上面序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。这个方法最早是由IBM的研究人员Pierre Ernst在2013年提出《Look-ahead Java deserialization》,具体实现代码示例如下:
public class AntObjectInputStream extends ObjectInputStream{public AntObjectInputStream(InputStream inputStream)throws IOException {super(inputStream);}/*** 只允许反序列化SerialObject class*/@Overrideprotected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,ClassNotFoundException {if (!desc.getName().equals(SerialObject.class.getName())) {throw new InvalidClassException("Unauthorized deserialization attempt",desc.getName());}return super.resolveClass(desc);}
}
通过此方法,可灵活的设置允许反序列化类的白名单,也可设置不允许反序列化类的黑名单。但反序列化漏洞利用方法一直在不断的被发现,黑名单需要一直更新维护,且未公开的利用方法无法覆盖。
SerialKiller 是由Luca Carettoni利用上面介绍的方法实现的反序列化类白/黑名单校验的jar包。具体使用方法可参考其代码仓库。
contrast-rO0是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下:
SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true);
in.addToWhitelist(SerialObject.class);in.readObject();
使用ValidatingObjectInputStream来校验反序列化的类
使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考ValidatingObjectInputStream介绍;示例代码如下:
private static Object deserialize(byte[] buffer) throws IOException,
ClassNotFoundException , ConfigurationException {Object obj;ByteArrayInputStream bais = new ByteArrayInputStream(buffer);// Use ValidatingObjectInputStream instead of InputStreamValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais); //只允许反序列化SerialObject classois.accept(SerialObject.class);obj = ois.readObject();return obj;
}
使用ObjectInputFilter来校验反序列化的类
Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的setObjectInputFilter设置过滤器来实现反序列化类白/黑名单控制。示例代码如下:
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.io.ObjectInputFilter;
class BikeFilter implements ObjectInputFilter {private long maxStreamBytes = 78; // Maximum allowed bytes in the stream.private long maxDepth = 1; // Maximum depth of the graph allowed.private long maxReferences = 1; // Maximum number of references in a graph.@Overridepublic Status checkInput(FilterInfo filterInfo) {if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) {return Status.REJECTED;}Class<?> clazz = filterInfo.serialClass();if (clazz != null) {if (SerialObject.class == filterInfo.serialClass()) {return Status.ALLOWED;}else {return Status.REJECTED;}}return Status.UNDECIDED;} // end checkInput
} // end class BikeFilter
上述示例代码,仅允许反序列化SerialObject类对象,上述示例及更多关于ObjectInputFilter的均参考自NCC Group Whitepaper由Robert C. Seacord写的《Combating Java Deserialization Vulnerabilities with Look-Ahead Object Input Streams (LAOIS)》
黑名单校验修复
在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法,因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单。
但有某些场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,部分的黑名单类如下:
- org.apache.commons.collections.functors.InvokerTransformer
- org.apache.commons.collections.functors.InstantiateTransformer
- org.apache.commons.collections4.functors.InvokerTransformer
- org.apache.commons.collections4.functors.InstantiateTransformer
- org.codehaus.groovy.runtime.ConvertedClosure
- org.codehaus.groovy.runtime.MethodClosure
- org.springframework.beans.factory.ObjectFactory
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- org.apache.commons.fileupload
- org.apache.commons.beanutils
- ...
安全编码建议
- 更新commons-collections、commons-io等第三方库版本;
- 业务需要使用反序列化时,尽量避免反序列化数据可被用户控制,如无法避免建议尽量使用白名单校验的修复方式;
总结
关于反序列化漏洞分析及利用研究的文章不少,但鲜有检测及修复方面的介绍,本文旨站在应用安全的角度,从安全编码、代码审计、漏洞检测及修复方案对反序列化漏洞进行详细分享。希望对从事应用安全的朋友有所帮助。文中若有问题之处欢迎指出交流。
参考
- https://www.nccgroup.trust/us/our-research/combating-java-deserialization-vulnerabilities-with-look-ahead-object-input-streams-laois/
- https://dzone.com/articles/a-first-look-into-javas-new-serialization-filterin
- https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html
- https://www.owasp.org/index.php/Deserialization_of_untrusted_data
- https://github.com/Cryin/Paper/blob/master/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E6%A3%80%E6%B5%8B%E6%96%B9%E6%A1%88.md
- https://www.ibm.com/developerworks/library/se-lookahead/
- https://github.com/baidu/openrasp
转载于:https://www.cnblogs.com/studyskill/p/9207117.html
Java反序列化漏洞之殇相关推荐
- java反序列化漏洞的一些gadget
目录 0x00 URLDNS 0x01 Commons Collections 0x02 RMI的codebase任意代码执行 0x03 JNDI 0x04 LDAP 0x05 JDK7u21 首先说 ...
- java 反序列化漏洞 利用思路简介
目录 序列化的过程 readObject方法 反射链 完成反序列漏洞实践 结论 之前听别人讲解反序列化的漏洞听的晕乎乎的,刚脆就趁着周末研究一下反序列化漏洞,并且搭建实战环境实际操作了一把,明白了之后 ...
- java 反序列化漏洞简介
目录 一.Java的序列化与反序列化 二.对java序列化的理解 三.反序列化的漏洞原理概述 四.关于反射链 一.Java的序列化与反序列化 在这里我们直接自己定义一个类,然后对这个类的对象(一个实例 ...
- java序列化_技术干货 | JAVA反序列化漏洞
目录 反序列化漏洞 序列化和反序列化 JAVA WEB中的序列化和反序列化 对象序列化和反序列范例 JAVA中执行系统命令 重写readObject()方法 Apache Commons Collec ...
- common-collections中Java反序列化漏洞导致的RCE原理分析
2019独角兽企业重金招聘Python工程师标准>>> common-collections中Java反序列化漏洞导致的RCE原理分析 隐形人真忙 · 2015/11/11 22:4 ...
- linux反序列化漏洞,思科多个产品Java反序列化漏洞(CVE-2015-6420)
思科多个产品Java反序列化漏洞(CVE-2015-6420) 发布日期:2015-12-15 更新日期:2015-12-17 受影响系统: Cisco Unified Computing Cisco ...
- 修而未复:说说WebLogic那修不完的Java反序列化漏洞
编者说明:这篇文章初稿写在Oracle CPU补丁发布之后,考虑到文章内容的影响,并未在当时发布,WebLogic 的 Java 反序列化漏洞,已经修复了多次,最终的修复仍然未彻底解决问题. 背景 当 ...
- 带你掌握java反序列化漏洞及其检测
摘要:在本文中将先介绍java反序列化漏洞的原理,然后在此基础上介绍安全工具如何检测.扫描此类漏洞. 本文分享自华为云社区<java反序列化漏洞及其检测>,作者: alpha1e0. 1 ...
- shrio反序列漏洞修复_Apache Shiro Java 反序列化漏洞分析
Author: rungobier(知道创宇404安全实验室) Date: 2016-08-03 0x00 概述 Apache Shiro 在 Java 的权限及安全验证框架中占用重要的一席之地,在它 ...
- Java反序列化漏洞研究
Java反序列化漏洞研究 漏洞原理 java序列化就是把对象转换成字节流,便于保存在内存.文件.数据库中:反序列化即逆过程,由字节流还原成对象.当反序列化的输入来源于程序外部,可以被用户控制,恶意用户 ...
最新文章
- python打开网页存图_python 获取网页图片 保存在本地
- 汇编语言之大小写转换问题
- OpenVC环境搭建问题
- python作业题目用户输入行数、输出倒的等腰三角形_智慧职教云课堂APPPython程序设计(常州工业职业技术学院)作业期末考试答案...
- jdk8 接口默认方法_JDK 8中方便的新地图默认方法
- c语言坐标绕路,C语言中的奇技淫巧
- Java的搜索引擎框架
- 《算法设计与分析基础》【part1】
- Xposed 框架检测机制
- lux视频下载工具的安装和使用
- Aspose.Cells 使用FreezePanes()冻结行和列
- 谷歌放弃了IE6/The Google phasing out support for IE6.0
- 【蓝桥杯单片机笔记6】PWM脉宽调制信号
- 联想thinkpad笔记本240首次使用计算机中只有c与q盘,原创Lenovo Thinkpad New X1 Carbon 512G 硬盘分区方法...
- 美术文献杂志美术文献杂志社美术文献编辑部2022年第7期目录
- Djano3.0使用-CBV使用实例
- codeforces 76A Gift 最小生成树
- linux禁用小企鹅输入法,小企鹅(fcitx)输入法停止开发
- 社群运营普遍存在的3个问题,微信群助手帮你通通解决!
- 12点的idft c语言,【整理】用IDFT实现UF-OFDM和OFDM的模拟调制