news 2026/10/2 14:40:56

CTF图片隐写实战:LSB+DES多层解密全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF图片隐写实战:LSB+DES多层解密全解析

1. 项目概述:一张图片里藏了多少秘密?

“[QCTF2018]picture”这个标题乍看平平无奇——不就是一道CTF比赛里的图片题吗?但如果你真把它当成普通JPG点开就完事,那恭喜你,第一关就卡在了加载界面。我第一次看到这道题时,也是随手双击打开,看到一张灰蒙蒙的夜景图,天空有几颗模糊的星点,远处山影轮廓淡得几乎要融进背景里。没水印、没EXIF异常、没明显噪点,连用StegSolve拖拽RGB通道都看不出毛边。直到我右键“属性”→“详细信息”,扫到一行不起眼的备注:“Created with GIMP 2.8.22”。GIMP?不是Photoshop?这个细节像根小刺扎进脑子里——CTF题从来不会白给任何信息。

后来才知道,这张图是QCTF2018线上赛的一道经典入门题,表面考图像隐写,实则是一场多层嵌套的密码学拆解实战。它不靠炫技的算法堆砌,而是用最基础的LSB(最低有效位)隐写打底,再套一层DES加密,最后用Python2脚本收口。整个链路像剥洋葱:先从像素里抠出一串二进制流,再拿DES密钥解密,最终得到flag。而那个“python2”的提示,根本不是让你装环境的客套话——它是整条解密链上最关键的版本锁。我试过用Python3跑原题附带的lsb.py,结果解出来的全是乱码,折腾半小时才发现DES模块在Py3里默认用PKCS#7填充,而原题用的是PKCS#5,差那1个字节,整个密文就全崩。这种细节,文档里不会写,教程里懒得提,只有亲手踩过坑的人才懂。

这道题适合三类人:刚学隐写的新人,能借它把LSB原理从“改最后一位”具象成“逐像素读取RGB值”;正在啃密码学基础的考生,DES的16轮Feistel结构、S盒查表、初始置换IP这些抽象概念,在真实密文解密过程中突然变得可触摸;还有那些总被环境问题绊倒的实战派,它逼着你直面Python2/3生态断层——不是“装个包就行”,而是理解底层字节处理逻辑差异。别小看这张图,它像一把钥匙,开的不是单个工具的门,而是CTF逆向思维的第一道锁。

2. 整体设计思路与技术选型逻辑

2.1 为什么用LSB打头阵?——隐写效率与隐蔽性的黄金平衡点

很多人一听说“图片藏数据”,第一反应是用OpenStego或OutGuess这类专业工具。但在QCTF2018这道题里,出题人偏偏选了最朴素的LSB隐写,原因很实在:它不需要额外元数据、不改变文件尺寸、对视觉干扰近乎为零,且实现门槛极低——你甚至能用Excel手动改像素值验证。我拆过原图的像素矩阵,发现它只动用了每个像素RGB三通道的最低1位,也就是每3个像素就能藏1字节(3×1 bit = 3 bit,但实际按字节对齐,8 bit需约3个像素×3通道=9 bit,冗余1 bit用于校验)。这种“轻量级入侵”让图片在肉眼和常规检测工具面前完全隐身。

更关键的是,LSB天然适配CTF的“分步解题”逻辑。它不像Base64编码那样一步到位,而是把数据拆成原子级操作:读像素→取低位→拼字节→存文件。这种可中断、可调试的流程,让选手能随时验证中间态——比如先提取前100字节看是否含magic number,再决定后续解密方向。我见过有人直接跳过LSB分析,用binwalk扫图,结果扫出一堆“疑似ZIP”“疑似ELF”的误报,白白浪费半小时。而LSB方案,只要写个5行Python循环,就能确认数据是否存在:for i in range(10): print(bin(pixels[i][0] & 1)),输出全是0或1,立刻知道数据就在那里。

提示:LSB不是万能的。它抗不了有损压缩(JPEG会重算DCT系数,直接抹掉低位),所以原题用的是PNG格式——无损压缩,像素值100%保真。这点在解题前必须确认,否则用JPEG当输入,解出来全是废码。

2.2 为什么选DES而非AES?——考察能力的精准锚点

看到“DES”这个词,很多人的第一反应是“这算法早淘汰了吧”。但恰恰是它的“过时”,成了出题人的精妙设计。DES密钥长度仅56位,在现代算力下暴力破解只需数小时,但QCTF2018没给你留这个时间——它把密钥硬编码在lsb.py脚本里,要求你读懂代码逻辑才能拿到。这种设计直指CTF核心能力:不是考你会不会调库,而是考你能不能从代码里反推密钥生成逻辑。

我对比过原题lsb.py和标准DES实现,发现两个关键差异:一是初始密钥处理用了自定义置换表(非标准PC-1),二是解密时S盒查表顺序被刻意打乱。这意味着,哪怕你复制粘贴网上现成的DES解密函数,也会因置换表不匹配而失败。出题人用DES,就是要逼你动手实现Feistel网络——不是调用pyDes,而是自己写轮函数、自己做异或、自己查S盒。我在调试时卡在第3轮输出不对,最后发现是S盒索引计算少了个+1偏移,这种细节只有手写过一遍才记得住。

注意:DES的“弱密钥”(如全0、全1)在这里是陷阱。原题密钥是十六进制字符串"133457799BBCDFF1",转换成二进制后检查,它不属于已知的4个弱密钥之一,但出题人故意选了这个经典测试密钥,就是让你意识到:密钥安全性不能只看长度,更要关注其在S盒映射中的行为。

2.3 为什么死守Python2?——生态断层的真实战场

“python2”这个热搜词,表面看是环境提示,实则是整道题的终极考验。我最初以为只是print语句语法差异,结果栽在更底层:Python2的str类型直接对应字节流,而Python3的str是Unicode,bytes才是字节。DES加解密操作的对象必须是原始字节,一旦用Py3的str.encode()处理不当,就会引入BOM或UTF-8多字节编码,导致密文长度错乱。

举个具体例子:原题lsb.py中有一行key = "133457799BBCDFF1".decode('hex'),在Py2里直接返回8字节二进制串;但在Py3里,.decode('hex')已被移除,你得用bytes.fromhex("133457799BBCDFF1"),看似等价,实则bytes.fromhex返回的是bytes对象,而Py2的decode('hex')返回的是str——在Py3中,str和bytes不可混用,DES.new(key, DES.MODE_ECB)会直接报错TypeError: key must be bytes。更隐蔽的是填充逻辑:Py2版pyDes默认PKCS#5,Py3版pycryptodome默认PKCS#7,虽然两者在8字节块上表现一致,但当明文长度恰好整除8时,PKCS#7会额外补8字节,而PKCS#5不会——这就导致解密后多出一串0x08字节。

实操心得:别试图强行迁移到Py3。我试过用2to3工具自动转换,结果pyDes模块的pad函数参数名被改错,反而更难调试。最稳的方案是:用Docker拉一个python:2.7-slim镜像,把lsb.py和图片丢进去,一条命令跑通。这比折腾兼容性快得多。

3. 核心细节解析与实操要点

3.1 LSB数据提取:像素级操作的三个致命细节

LSB提取看似简单,但实际操作中三个细节决定成败:

第一,像素遍历顺序必须严格按行优先(Row-major order)。很多人习惯用PIL的getdata()直接获取扁平化像素列表,这是安全的;但若自己写嵌套循环,必须确认外层是y(行),内层是x(列)。我曾因循环写成for x in range(w): for y in range(h):,导致提取的数据完全错乱——因为图片存储是按行存的,x变化快意味着在内存里跳着读,数据自然串位。验证方法很简单:用已知内容测试图(比如藏了"ABC"的图),提取前3字节,看是否等于ord('A'), ord('B'), ord('C')。

第二,RGB通道的读取顺序不能颠倒。PNG默认是RGBA,但原题图是RGB模式(用im.mode确认)。如果误读alpha通道,会多出一堆0xFF值。更隐蔽的是,有些工具(如Stegsolve)默认显示BGR顺序,而代码里按RGB读,会导致字节序反转。我的教训是:先用im.getpixel((0,0))打印左上角像素,确认输出是(R,G,B)三元组,再开始批量读取。

第三,LSB拼接时的字节对齐策略。理论上每3像素(R,G,B各1bit)可凑1字节,但实际常采用“连续读取所有像素的R通道LSB→所有G→所有B”,这样更高效。原题lsb.py用的是前者:for i in range(len(pixels)): r,g,b = pixels[i]; bits.append(r&1); bits.append(g&1); bits.append(b&1)。关键点在于,当bits列表长度不是8的倍数时,必须截断末尾冗余位。我遇到过提取后数据长度为1025字节,但DES要求8字节对齐,多出的1字节其实是padding,直接丢弃即可。

提示:用matplotlib可视化LSB位图能快速定位数据区域。把提取的bit流reshape成二维数组(如32×32),用plt.imshow(bits, cmap='gray')显示,如果看到清晰文字或图案,说明提取正确;如果一片噪点,大概率是顺序错了。

3.2 DES密钥与模式解析:ECB模式下的明文特征

原题明确使用DES-ECB模式,这既是便利也是陷阱。ECB(Electronic Codebook)模式的特点是:相同明文块加密后产生相同密文块。这意味着,如果你拿到的密文有大量重复8字节序列,基本可以断定是ECB。我用xxd -p picture.png | fold -w16把密文转成十六进制,果然发现1a2b3c4d5e6f7890重复出现4次——这就是ECB的指纹。

密钥"133457799BBCDFF1"需要转换成8字节二进制。这里有个易错点:十六进制字符串每2字符代表1字节,"13"→0x13→19,所以8字节密钥对应16字符。我最初误以为密钥是ASCII字符串"133457799BBCDFF1"(16字节),结果解密失败。正确做法是key_bytes = binascii.unhexlify("133457799BBCDFF1"),得到真正的8字节密钥。

DES的初始置换IP(Initial Permutation)表长64位,作用是把64位输入按固定顺序重排。原题lsb.py里实现了标准IP表,但新手常忽略一点:IP操作对象是64位比特串,而Python里得用int.to_bytes(8,'big')转成字节再处理。我调试时发现IP后输出全是0,最后查到是int转字节时没指定byteorder,默认小端序导致位序全反。

实操心得:DES解密不必从头实现。用pyDes库(Py2专用)最稳妥,但必须确认版本——pip install pyDes==2.0.1,新版已不支持Py2。初始化时写des = pyDes.des(key, pyDes.ECB, pad=None, padmode=pyDes.PAD_PKCS5),padmode必须显式指定,否则默认PAD_NORMAL会用空格填充,和题目不匹配。

3.3 lsb.py脚本的逆向逻辑:从执行流反推数据结构

原题附带的lsb.py不是拿来直接跑的黑盒,而是要你读懂它才能解题。我把它拆成三段分析:

第一段:图像加载与像素提取

from PIL import Image im = Image.open("picture.png") pixels = list(im.getdata())

这里im.getdata()返回的是扁平化像素元组列表,每个元组长度由im.mode决定。原图是RGB,所以len(pixels[0]) == 3。如果mode是RGBA,第四位alpha必须忽略,否则数据污染。

第二段:LSB提取与重组

bits = [] for pixel in pixels: for channel in pixel[:3]: # 强制取前3通道,防RGBA bits.append(channel & 1) # 拼成字节 bytes_data = [] for i in range(0, len(bits), 8): if i+8 <= len(bits): byte_val = 0 for j in range(8): byte_val = (byte_val << 1) | bits[i+j] bytes_data.append(byte_val)

注意channel & 1是取最低位,<< 1是左移构建字节。这里bits[i+j]的j从0到7,对应MSB到LSB顺序——即第一个bit是字节最高位。这和常见LSB实现一致。

第三段:DES解密

from pyDes import * key = "133457799BBCDFF1".decode('hex') cipher = des(key, ECB, pad=None, padmode=PAD_PKCS5) decrypted = cipher.decrypt(bytes(bytearray(bytes_data))) print(decrypted)

关键在bytes(bytearray(bytes_data)):bytearray把整数列表转成可变字节数组,bytes()转成不可变bytes对象,这才是DES接受的输入类型。Py3里bytes_data是list,bytes(bytes_data)会直接报错,必须bytes(bytearray(bytes_data))。

常见错误:decrypted解出来可能是乱码,但print(repr(decrypted))能看到\x00\x01...这样的原始字节。这是因为flag是ASCII文本,但解密后可能带不可见控制符,用decrypted.strip().decode('utf-8')清理后再输出。

4. 完整实操过程与关键环节实现

4.1 环境准备:Python2沙箱的搭建与验证

别跳过这步。我见过太多人卡在环境上,花3小时配环境,结果解题只要10分钟。最优解是用容器隔离:

# 拉取官方Python2镜像 docker pull python:2.7-slim # 创建工作目录 mkdir qctf-picture && cd qctf-picture wget https://example.com/picture.png # 替换为实际题目链接 wget https://example.com/lsb.py # 同上 # 启动交互式容器 docker run -it --rm -v $(pwd):/work -w /work python:2.7-slim bash

在容器内执行:

# 安装依赖(pyDes只支持Py2) pip install pyDes==2.0.1 # 验证环境 python -c "import pyDes; print('pyDes OK')" python -c "from PIL import Image; print('PIL OK')"

提示:如果无法用Docker,本地装Py2最简方案是用pyenv:

brew install pyenv # macOS pyenv install 2.7.18 pyenv local 2.7.18 pip install pyDes==2.0.1 pillow

4.2 LSB数据提取:手写脚本与自动化验证

别直接跑lsb.py,先自己写个最小验证脚本,确认LSB存在:

# verify_lsb.py from PIL import Image im = Image.open("picture.png") pixels = list(im.getdata()) print("Image mode:", im.mode) print("First 5 pixels:", pixels[:5]) # 提取前24位(3字节) bits = [] for i in range(8): # 取前8像素的R通道 r, g, b = pixels[i] bits.append(r & 1) print("First 8 R-channel LSBs:", bits) # 拼第一个字节 byte0 = 0 for b in bits: byte0 = (byte0 << 1) | b print("First byte (R-only):", hex(byte0))

运行后输出First byte (R-only): 0x46,对应ASCII 'F'——flag开头!这说明LSB数据真实存在,且R通道承载了有效载荷。

然后执行原题lsb.py:

python lsb.py

如果输出乱码,用xxd检查提取的二进制:

python lsb.py > raw.bin xxd -l 32 raw.bin

正常应看到类似00000000: 1a2b 3c4d 5e6f 7890 1a2b 3c4d 5e6f 7890 .+<M^ox...+<M^ox.的重复密文。

4.3 DES解密:从密文到flag的逐层剥离

假设raw.bin是提取的密文(8字节对齐),解密步骤:

步骤1:确认密文长度

wc -c raw.bin # 应为8的倍数,如128字节

步骤2:用pyDes解密

# decrypt.py from pyDes import * import binascii # 读取密文 with open("raw.bin", "rb") as f: ciphertext = f.read() # 密钥 key = binascii.unhexlify("133457799BBCDFF1") # 解密 des = des(key, ECB, pad=None, padmode=PAD_PKCS5) plaintext = des.decrypt(ciphertext) # 输出 print("Decrypted (raw):", plaintext) print("Decrypted (hex):", plaintext.hex()) print("Decrypted (ASCII):", plaintext.strip().decode('utf-8', errors='ignore'))

运行后,Decrypted (ASCII)应输出类似flag{QCTF2018_picture_stego_des}的字符串。

步骤3:处理padding残留如果末尾有b'\x04\x04\x04\x04',说明PKCS#5填充了4字节,用plaintext.rstrip(b'\x04')清理。原题flag无填充,但为防万一,加个通用清理:

# 清理PKCS#5 padding pad_len = plaintext[-1] if 1 <= pad_len <= 8 and plaintext[-pad_len:] == bytes([pad_len] * pad_len): plaintext = plaintext[:-pad_len]

4.4 全流程整合:一键解题脚本

把以上步骤合成一个健壮脚本,避免手动干预:

#!/usr/bin/env python # solve.py - QCTF2018 picture solver from PIL import Image from pyDes import * import binascii import sys def extract_lsb(image_path): im = Image.open(image_path) if im.mode != 'RGB': im = im.convert('RGB') pixels = list(im.getdata()) bits = [] for pixel in pixels: for channel in pixel[:3]: bits.append(channel & 1) # 转字节 bytes_data = bytearray() for i in range(0, len(bits), 8): if i + 8 <= len(bits): byte_val = 0 for j in range(8): byte_val = (byte_val << 1) | bits[i + j] bytes_data.append(byte_val) return bytes(bytes_data) def decrypt_des(ciphertext): key = binascii.unhexlify("133457799BBCDFF1") des = des(key, ECB, pad=None, padmode=PAD_PKCS5) plaintext = des.decrypt(ciphertext) # 移除PKCS#5 padding pad_len = plaintext[-1] if 1 <= pad_len <= 8 and plaintext[-pad_len:] == bytes([pad_len] * pad_len): plaintext = plaintext[:-pad_len] return plaintext if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python solve.py <image_path>") sys.exit(1) try: cipher = extract_lsb(sys.argv[1]) flag = decrypt_des(cipher) print("Flag:", flag.decode('utf-8')) except Exception as e: print("Error:", str(e))

用法:python solve.py picture.png

实操心得:这个脚本在Py2下100%通过,但要注意PIL在Py2里叫PIL,Py3里叫Pillow,所以脚本开头不能写from PIL import Image,必须确保环境是Py2。我在一次比赛中因队友误装Py3的Pillow,import PIL失败,耽误15分钟——后来我们约定:所有CTF脚本第一行加#!/usr/bin/env python2,强制指定解释器。

5. 常见问题与排查技巧实录

5.1 图像加载失败:PIL的隐式转换陷阱

现象:Image.open("picture.png")报错OSError: cannot identify image file,或打开后im.mode是P(调色板模式)而非RGB。

原因:PNG可能用了调色板(Palette)模式,PIL默认不自动转换。原题图虽是PNG,但内部是索引色,需手动转RGB。

解决:

im = Image.open("picture.png") if im.mode == 'P': # 尝试用调色板转换 if 'transparency' in im.info: im = im.convert('RGBA') else: im = im.convert('RGB') elif im.mode == 'RGBA': # 丢弃alpha通道 im = im.convert('RGB')

验证:print(im.mode)必须输出RGB,且len(list(im.getdata())[0]) == 3。

排查技巧:用file picture.png命令看文件头。PNG标准头是89 50 4e 47 0d 0a 1a 0a,如果开头是ffd8,那是JPEG伪装成PNG,需用convert转格式。

5.2 LSB提取为空:位操作的符号陷阱

现象:bits列表全为0,或长度远小于预期(如图有10000像素,bits只有几百位)。

原因:channel & 1在channel为负数时失效(Python里负数&1恒为1)。但PNG像素值不可能为负,所以真正原因是——你用了numpy读图,而numpy.array默认int32,读取时溢出变负。

解决:坚持用PIL原生getdata(),或用numpy时指定dtype=np.uint8:

import numpy as np arr = np.array(im, dtype=np.uint8) # 关键! pixels = arr.reshape(-1, 3) # 扁平化

验证:print(pixels[0])应输出[255 128 64]这类非负整数。

5.3 DES解密乱码:填充模式与字节对齐的双重校验

现象:解密后输出b'\x00\x01...',但decode('utf-8')报错UnicodeDecodeError。

排查路径:

  1. 检查密文长度:len(cipher) % 8 != 0?如果是,说明LSB提取时没对齐,需截断到8的倍数。
  2. 检查密钥:len(key) != 8?用binascii.unhexlify确保是8字节。
  3. 检查模式:确认是ECB而非CBC(CBC需要IV,原题无IV)。
  4. 检查padding:用xxd -p raw.bin | fold -w16看末尾8字节是否全等(ECB特征),如果不等,可能是密文损坏。

终极验证:用已知明文测试DES。例如明文b"12345678",密钥b"\x01\x02\x03\x04\x05\x06\x07\x08",标准DES加密后应为b"\x85\x9e\x4a\x8a\x95\x7e\x57\x1d"。在你的环境中跑一遍,匹配则DES模块正常。

独家技巧:如果pyDes安装失败,用在线DES工具(如https://www.devglan.com/online-tools/des-encryption-decryption)手动验证。把raw.bin用xxd -p转成hex,粘贴到工具里,选ECB、PKCS5、密钥133457799BBCDFF1,看输出是否可读。这能快速区分问题是出在LSB还是DES环节。

5.4 Flag格式错误:CTF flag的隐藏规则

现象:解密出QCTF2018{...},但提交系统提示Wrong Answer。

原因:CTF flag有严格格式。原题要求flag{...},而解密可能输出QCTF2018{...}或FLAG{...}。

解决:用正则提取:

import re flag_match = re.search(r'flag\{.*?\}', plaintext.decode('utf-8', errors='ignore'), re.I) if flag_match: print("Flag:", flag_match.group(0).lower()) # 统一转小写 else: print("No flag found")

注意:有些题flag含特殊字符(如_、-),需确认题目说明。QCTF2018的flag是纯字母数字,无特殊符号。

实操心得:我参赛时因flag末尾多了个换行符b'\n'被拒。后来加了strip():flag_match.group(0).strip()。CTF系统对flag格式极其敏感,多一个空格都不行。

6. 延伸思考与能力迁移

这道题的价值远不止于解出flag。它像一块磨刀石,把几个关键能力磨得锋利:

LSB隐写的工程化思维。真实场景中,LSB常被用于数字水印,要求抗剪裁、抗缩放。这道题的“纯LSB”是教学简化版,但启发你思考:如果图片被用户截图上传,LSB数据就没了——怎么办?答案是结合DCT域隐写(JPEG常用),或用鲁棒水印算法(如DWT)。我后来用OpenCV实现了基于DCT的LSB,把数据藏在中频系数里,截图后仍能恢复80%。

DES的现代启示。DES虽老,但它的Feistel结构、S盒设计思想,直接催生了AES。我用这道题的S盒逻辑,手写了简化版AES的SubBytes步骤,发现S盒本质是GF(2^8)上的仿射变换——这让我真正理解了“非线性层”的意义。软考里考的“线性S盒计算”,其实就是求这个变换矩阵,而QCTF这道题,就是最接地气的练习场。

Python2/3生态的实战认知。现在新项目当然用Py3,但维护老系统时,Py2的字节处理逻辑仍是必修课。我后来参与一个银行旧系统迁移,发现其加密模块用Py2的string.maketrans,迁移到Py3时,maketrans参数变了,导致整个签名验签失败。那次经历,让我把QCTF这道题的教训刻进了肌肉记忆。

最后分享个小技巧:下次看到CTF图片题,别急着跑工具。先用strings picture.png | grep -i "flag\|qctf\|ctf"扫一遍明文字符串——很多题把flag明文藏在注释里,LSB只是障眼法。这道题没这么做,但养成这个习惯,能帮你省下一半时间。毕竟,CTF的本质不是比谁工具多,而是比谁看得清。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 14:39:04

iOS银行卡OCR实战:Metal预处理+动态ROI+轻量Tesseract集成

简介&#xff1a;这是一份面向iOS开发者的技术实践资源&#xff0c;提供完整的银行卡OCR识别功能实现方案&#xff0c;适用于商户进件、实名认证等需快速提取银行卡信息的业务场景。资源基于自定义AVCapture相机封装&#xff0c;集成libexbankcardios.a与libbexbankcard.a两个免…

作者头像 李华
网站建设 2026/10/2 14:39:04

进销存实战:从主键外键到CHECK约束,吃透数据库完整性

最近不是流行把学习阶段整成修仙境界嘛&#xff0c;我加入了一个叫“东方仙盟”的学习社群&#xff0c;群里的修炼体系分练气、筑基、金丹&#xff0c;看着挺中二&#xff0c;但架不住干货多。我的账号卡在“练气期”&#xff0c;第一个修炼任务就是&#xff1a;用进销存业务把…

作者头像 李华
网站建设 2026/10/2 14:38:53

Intel集显OpenGL版本降级真相:Mesa驱动栈的上下文协商机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 14:38:21

智能车竞赛陀螺仪怎么选?MPU6050与IMU660RA硬核对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 14:38:19

Django新闻网站后台管理实战:从数据建模到权限设计与部署

简介&#xff1a;基于Django开发的新闻网站及后台管理系统源码&#xff0c;适合正在学习Python Web开发的初中级开发者&#xff0c;可用于掌握用户认证、内容发布、分类管理等完整业务场景。压缩包共2000个文件、13.62MB&#xff0c;包含大量png/svg图标资源、js/css前端样式、…

作者头像 李华