CTF-Misc要点


信息搜集技术

网络信息搜集技巧

  • 公开渠道
  • 目标 Web 网页、地理位置、相关组织
  • 组织结构和人员、个人资料、电话、电子邮件
  • 网络配置、安全防护机制的策略和技术细节
  • 通过搜索引擎查找特定安全漏洞或私密信息的方法
  • Google Hacking Database
  • 科学上网

基本搜索技巧

  • Google 基本搜索与挖掘技巧
  • 保持简单明了的关键词
  • 使用最可能出现在要查找的网页上的字词
  • 尽量简明扼要地描述要查找的内容
  • 选择独特性的描述字词
  • 社会公共信息库查询
  • 个人信息:人口统计局
  • 企业等实体:YellowPage、企业信用信息网
  • 网站、域名、IP:whois 等

地图和街景搜索

  • 国外:Google Map、Google Earth、Google Street View
  • 国内:百度地图、卫星地图、街景
  • 从网络世界到物理世界:IP2Location
  • whois 数据库
  • GeoIP
  • IP2Location
  • 纯真数据库(QQ IP 查询)

编码分析

通信领域常用编码

电话拨号编码

1-9 分别使用 1-9 个脉冲,0 则表示使用 10 个脉冲。

Morse 编码

参见 摩尔斯编码 - 维基百科,对应表如下

摩尔斯电码

特点

  • 只有 .-
  • 最多 6 位;
  • 也可以使用 01 串表示。

工具

敲击码

敲击码(Tap code)是一种以非常简单的方式对文本信息进行编码的方法。因该编码对信息通过使用一系列的点击声音来编码而命名,敲击码是基于 5 ×5 方格波利比奥斯方阵来实现的,不同点是是用 K 字母被整合到 C 中。

Tap Code 1 2 3 4 5
1 A B C/K D E
2 F G H I J
3 L M N O P
4 Q R S T U
5 V W X Y Z

曼彻斯特编码

格雷编码

计算机相关的编码

本节介绍一些计算机相关的编码。

字母表编码

  • A-Z/a-z 对应 1-26 或者 0-25

ASCII 编码

ascii

特点

我们一般使用的 ascii 编码的时候采用的都是可见字符,而且主要是如下字符

  • 0-9, 48-57
  • A-Z, 65-90
  • a-z, 97-122

变形

二进制编码

将 ascii 码对应的数字换成二进制表示形式。

  • 只有 0 和 1
  • 不大于 8 位,一般 7 位也可以,因为可见字符到 127。
  • 其实是另一种 ascii 编码。
十六进制编码

将 ascii 码对应的数字换成十六进制表示形式。

  • A-Z→41-5 A
  • a-z→61-7 A

工具

例子

ascii

2018 DEFCON Quals ghettohackers: Throwback

题目描述如下

Anyo!e!howouldsacrificepo!icyforexecu!!onspeedthink!securityisacomm!ditytop!urintoasy!tem!

第一直觉应该是我们去补全这些叹号对应的内容,从而得到 flag,但是补全后并不行,那么我们可以把源字符串按照 ! 分割,然后字符串长度 1 对应字母 a,长度 2 对应字母 b,以此类推

ori = 'Anyo!e!howouldsacrificepo!icyforexecu!!onspeedthink!securityisacomm!ditytop!urintoasy!tem!'
sp = ori.split('!')
print repr(''.join(chr(97 + len(s) - 1) for s in sp))

进而可以得到,这里同时需要假设 0 个字符为空格。因为这正好使得原文可读。

dark logic

Base 编码

base xx 中的 xx 表示的是采用多少个字符进行编码,比如说 base64 就是采用以下 64 个字符编码,由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节就有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节需要用 4 个可打印字符来表示。它可用来作为电子邮件的传输编码。在 Base64 中的可打印字符包括字母 A-Z、a-z、数字 0-9,这样共有 62 个字符,此外两个可打印符号在不同的系统中而不同。

base64

具体介绍参见 Base64 - 维基百科

编码 man

base64 编码 MAN

如果要编码的字节数不能被 3 整除,最后会多出 1 个或 2 个字节,那么可以使用下面的方法进行处理:先使用 0 值在末尾补足,使其能够被 3 整除,然后再进行 base64 的编码。在编码后的 base64 文本后加上一个或两个 = 号,代表补足的字节数。也就是说,当最后剩余一个八位字节(一个 byte)时,最后一个 6 位的 base64 字节块有四位是 0 值,最后附加上两个等号;如果最后剩余两个八位字节(2 个 byte)时,最后一个 6 位的 base 字节块有两位是 0 值,最后附加一个等号。参考下表:

base64 补 0

由于解码时补位的 0 并不参与运算,可以在该处隐藏信息。

与 base64 类似,base32 使用 32 个可见字符进行编码,2 的 5 次方为 32,所以每 5 bit 为 1 个分组。5 字节为 40 bit,对应于 8 个 base32 分组,即 5 个字节用 8 个 base32 中字符来表示。但如果不足 5 个字节,则会先对第一个不足 5 bit 的分组用 0 补足了 5 bit ,对后面剩余分组全部使用 “=” 填充,直到补满 5 个字节。由此可知,base32 最多只有 6 个等号出现。例如:

base32

特点

  • base64 结尾可能会有 = 号,但最多有 2 个
  • base32 结尾可能会有 = 号,但最多有 6 个
  • 根据 base 的不同,字符集会有所限制
  • 有可能需要自己加等号
  • = 也就是 3D
  • 更多内容请参见 base rfc

工具

例子

题目描述参见 ctf-challengemisc 分类的 base64-stego 目录中的 data.txt 文件。

使用脚本读取隐写信息。

import base64

def deStego(stegoFile):
    b64table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    with open(stegoFile,'r') as stegoText:
        message = ""
        for line in stegoText:
            try:
                text = line[line.index("=") - 1:-1]
                message += "".join([ bin( 0 if i == '=' else b64table.find(i))[2:].zfill(6) for i in text])[2 if text.count('=') ==2 else 4:6]  
            except:
                pass
    return "".join([chr(int(message[i:i+8],2)) for i in range(0,len(message),8)])

print(deStego("text.txt"))

输出:

     flag{BASE64_i5_amaz1ng}

霍夫曼编码

参见 霍夫曼编码

XXencoding

XXencode 将输入文本以每三个字节为单位进行编码。如果最后剩下的资料少于三个字节,不够的部份用零补齐。这三个字节共有 24 个 Bit,以 6bit 为单位分为 4 个组,每个组以十进制来表示所出现的数值只会落在 0 到 63 之间。以所对应值的位置字符代替。

           1         2         3         4         5         6
 0123456789012345678901234567890123456789012345678901234567890123
 |         |         |         |         |         |         |
 +-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

具体信息参见维基百科

特点

  • + 号,- 号。

工具

URL 编码

参见 URL 编码 - 维基百科

特点

  • 大量的百分号

工具

Unicode 编码

参见 Unicode - 维基百科

注意,它有四种表现形式。

例子

源文本: The

&#x [Hex]: The

&# [Decimal]: The

\U [Hex]: \U0054\U0068\U0065

\U+ [Hex]: \U+0054\U+0068\U+0065

工具

现实世界中常用的编码

条形码

  • 宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符
  • 国际标准
  • EAN-13 商品标准,13 位数字
  • Code-39:39 字符
  • Code-128:128 字符
  • 条形码在线识别

二维码

  • 用某种特定几何图形按一定规律在平面分步的黑白相间的图形记录数据符号信息

  • 堆叠式 / 行排式二维码:Code 16 k、Code 49、PDF417

  • 矩阵式二维码:QR CODE

取证隐写前置技术

大部分的 CTF 比赛中,取证及隐写两者密不可分,两者所需要的知识也相辅相成,所以这里也将对两者一起介绍。

任何要求检查一个静态数据文件从而获取隐藏信息的都可以被认为是隐写取证题(除非单纯地是密码学的知识),一些低分的隐写取证又常常与古典密码学结合在一起,而高分的题目则通常用与一些较为复杂的现代密码学知识结合在一起,很好地体现了 Misc 题的特点。

前置技能

  • 了解常见的编码

    能够对文件中出现的一些编码进行解码,并且对一些特殊的编码(Base64、十六进制、二进制等)有一定的敏感度,对其进行转换并得到最终的 flag。

  • 能够利用脚本语言(Python 等)去操作二进制数据

  • 熟知常见文件的文件格式,尤其是各类 文件头、协议、结构等

  • 灵活运用常见的工具

Python 操作二进制数据

struct 模块

有的时候需要用 Python 处理二进制数据,比如,存取文件,socket 操作时。这时候,可以使用 Python 的 struct 模块来完成。

struct 模块中最重要的三个函数是 pack()unpack()calcsize()

  • pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于 c 结构体的字节流)
  • unpack(fmt, string) 按照给定的格式(fmt)解析字节流 string,返回解析出来的 tuple
  • calcsize(fmt) 计算给定的格式(fmt)占用多少字节的内存

这里打包格式 fmt 确定了将变量按照什么方式打包成字节流,其包含了一系列的格式字符串。这里就不再给出不同格式字符串的含义了,详细细节可以参照 Python Doc

>>> import struct
>>> struct.pack('>I',16)
'\x00\x00\x00\x10'

pack 的第一个参数是处理指令,'>I' 的意思是:> 表示字节顺序是 Big-Endian,也就是网络序,I 表示 4 字节无符号整数。

后面的参数个数要和处理指令一致。

读入一个 BMP 文件的前 30 字节,文件头的结构按顺序如下

  • 两个字节:BM 表示 Windows 位图,BA 表示 OS/2 位图
  • 一个 4 字节整数:表示位图大小
  • 一个 4 字节整数:保留位,始终为 0
  • 一个 4 字节整数:实际图像的偏移量
  • 一个 4 字节整数:Header 的字节数
  • 一个 4 字节整数:图像宽度
  • 一个 4 字节整数:图像高度
  • 一个 2 字节整数:始终为 1
  • 一个 2 字节整数:颜色数
>>> import struct
>>> bmp = '\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'
>>> struct.unpack('<ccIIIIIIHH',bmp)
('B', 'M', 691256, 0, 54, 40, 640, 360, 1, 24)

bytearray 字节数组

将文件以二进制数组形式读取

data = bytearray(open('challenge.png', 'rb').read())

字节数组就是可变版本的字节

data[0] = '\x89'

常用工具

010 Editor

SweetScape 010 Editor 是一个全新的十六进位文件编辑器,它有别于传统的十六进位编辑器在于它可用「范本」来解析二进位文件,从而让你读懂和编辑它。它还可用来比较一切可视的二进位文件。

利用它的模板功能可以非常轻松的观察文件内部的具体结构并且依此快速更改内容。

file 命令

file 命令根据文件头(魔法字节)去识别一个文件的文件类型。

root in ~/Desktop/tmp λ file flag
flag: PNG image data, 450 x 450, 8-bit grayscale, non-interlaced

strings 命令

打印文件中可打印的字符,经常用来发现文件中的一些提示信息或是一些特殊的编码信息,常常用来发现题目的突破口。

  • 可以配合 grep 命令探测指定信息

    strings test|grep -i XXCTF
  • 也可以配合 -o 参数获取所有 ASCII 字符偏移

    root in ~/Desktop/tmp λ strings -o flag|head
        14 IHDR
        45 gAMA
        64  cHRM
        141 bKGD
        157 tIME
        202 IDATx
        223 NFdVK3
        361 |;*-
        410 Ge%<W
        431 5duX@%

binwalk 命令

binwalk 本是一个固件的分析工具,比赛中常用来发现多个文件粘合再在一起的情况。根据文件头去识别一个文件中夹杂的其他文件,有时也会存在误报率(尤其是对 Pcap 流量包等文件时)。

root in ~/Desktop/tmp λ binwalk flag

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 450 x 450, 8-bit grayscale, non-interlaced
134           0x86            Zlib compressed data, best compression
25683         0x6453          Zip archive data, at least v2.0 to extract, compressed size: 675, uncompressed size: 1159, name: readme.txt
26398         0x671E          Zip archive data, at least v2.0 to extract, compressed size: 430849, uncompressed size: 1027984, name: trid
457387        0x6FAAB         End of Zip archive

配合 -e 参数可以进行自动化提取。

也可以结合 dd 命令进行手动切割。

root in ~/Desktop/tmp λ dd if=flag of=1.zip bs=1 skip=25683
431726+0 records in
431726+0 records out
431726 bytes (432 kB, 422 KiB) copied, 0.900973 s, 479 kB/s

图片分析

图片分析简介

图像文件能够很好地包含黑客文化,因此 CTF 竞赛中经常会出现各种图像文件。

图像文件有多种复杂的格式,可以用于各种涉及到元数据、信息丢失和无损压缩、校验、隐写或可视化数据编码的分析解密,都是 Misc 中的一个很重要的出题方向。涉及到的知识点很多(包括基本的文件格式,常见的隐写手法及隐写用的软件),有的地方也需要去进行深入的理解。

元数据(Metadata)

元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(Data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。

元数据中隐藏信息在比赛中是最基本的一种手法,通常用来隐藏一些关键的 Hint 信息或者是一些重要的如 password 等信息。

这类元数据你可以 右键 --> 属性 去查看, 也可以通过 strings 命令去查看,一般来说,一些隐藏的信息(奇怪的字符串)常常出现在头部或者尾部。

接下来介绍一个 identify 命令,这个命令是用来获取一个或多个图像文件的格式和特性。

-format 用来指定显示的信息,灵活使用它的 -format 参数可以给解题带来不少方便。format 各个参数具体意义

例题

Break In 2017 - Mysterious GIF

这题的一个难点是发现并提取 GIF 中的元数据,首先 strings 是可以观察到异常点的。

GIF89a
   !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~
4d494945767749424144414e42676b71686b6947397730424151454641415343424b6b776767536c41674541416f4942415144644d4e624c3571565769435172
NETSCAPE2.0
ImageMagick
...

这里的一串 16 进制其实是藏在 GIF 的元数据区

接下来就是提取,你可以选择 Python,但是利用 identify 显得更加便捷

root in ~/Desktop/tmp λ identify -format "%s %c \n" Question.gif
0 4d494945767749424144414e42676b71686b6947397730424151454641415343424b6b776767536c41674541416f4942415144644d4e624c3571565769435172
1 5832773639712f377933536849507565707478664177525162524f72653330633655772f6f4b3877655a547834346d30414c6f75685634364b63514a6b687271
...
24 484b7735432b667741586c4649746d30396145565458772b787a4c4a623253723667415450574d35715661756278667362356d58482f77443969434c684a536f
25 724b3052485a6b745062457335797444737142486435504646773d3d

其他过程这里不在叙述,可参考链接中的 Writeup

像素值转化

看看这个文件里的数据,你能想到什么?

255,255,255,255,255...........

是一串 RGB 值,尝试着将他转化为图片

from PIL import Image
import re

x = 307 #x坐标  通过对txt里的行数进行整数分解
y = 311 #y坐标  x*y = 行数

rgb1 = [****]
print len(rgb1)/3
m=0
for i in xrange(0,x):
    for j in xrange(0,y):

        line = rgb1[(3*m):(3*(m+1))]#获取一行
        m+=1
        rgb = line

        im.putpixel((i,j),(int(rgb[0]),int(rgb[1]),int(rgb[2])))#rgb转化为像素
im.show()
im.save("flag.png")

而如果反过来的话,从一张图片提取 RGB 值,再对 RGB 值去进行一些对比,从而得到最终的 flag。

这类题目大部分都是一些像素块组成的图片,如下图

相关题目:

PNG

文件格式

对于一个 PNG 文件来说,其文件头总是由位固定的字节来描述的,剩余的部分由 3 个以上的 PNG 的数据块(Chunk)按照特定的顺序组成。

文件头 89 50 4E 47 0D 0A 1A 0A + 数据块 + 数据块 + 数据块……

数据块 CHUNk

PNG 定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了 4 个标准数据块,每个 PNG 文件都必须包含它们,PNG 读写软件也都必须要支持这些数据块。

数据块符号 数据块名称 多数据块 可选否 位置限制
IHDR 文件头数据块 第一块
cHRM 基色和白色点数据块 在 PLTE 和 IDAT 之前
gAMA 图像γ数据块 在 PLTE 和 IDAT 之前
sBIT 样本有效位数据块 在 PLTE 和 IDAT 之前
PLTE 调色板数据块 在 IDAT 之前
bKGD 背景颜色数据块 在 PLTE 之后 IDAT 之前
hIST 图像直方图数据块 在 PLTE 之后 IDAT 之前
tRNS 图像透明数据块 在 PLTE 之后 IDAT 之前
oFFs (专用公共数据块) 在 IDAT 之前
pHYs 物理像素尺寸数据块 在 IDAT 之前
sCAL (专用公共数据块) 在 IDAT 之前
IDAT 图像数据块 与其他 IDAT 连续
tIME 图像最后修改时间数据块 无限制
tEXt 文本信息数据块 无限制
zTXt 压缩文本数据块 无限制
fRAc (专用公共数据块) 无限制
gIFg (专用公共数据块) 无限制
gIFt (专用公共数据块) 无限制
gIFx (专用公共数据块) 无限制
IEND 图像结束数据 最后一个数据块

对于每个数据块都有着统一的数据结构,每个数据块由 4 个部分组成

名称 字节数 说明
Length(长度) 4 字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
Chunk Type Code(数据块类型码) 4 字节 数据块类型码由 ASCII 字母(A - Z 和 a - z)组成
Chunk Data(数据块数据) 可变长度 存储按照 Chunk Type Code 指定的数据
CRC(循环冗余检测) 4 字节 存储用来检测是否有错误的循环冗余码

CRC(Cyclic Redundancy Check)域中的值是对 Chunk Type Code 域和 Chunk Data 域中的数据进行计算得到的。

IHDR

文件头数据块 IHDR(Header Chunk):它包含有 PNG 文件中存储的图像数据的基本信息,由 13 字节组成,并要作为第一个数据块出现在 PNG 数据流中,而且一个 PNG 数据流中只能有一个文件头数据块

其中我们关注的是前 8 字节的内容

域的名称 字节数 说明
Width 4 bytes 图像宽度,以像素为单位
Height 4 bytes 图像高度,以像素为单位

我们经常会去更改一张图片的高度或者宽度使得一张图片显示不完整从而达到隐藏信息的目的。

这里可以发现在 Kali 中是打不开这张图片的,提示 IHDR CRC error,而 Windows 10 自带的图片查看器能够打开,就提醒了我们 IHDR 块被人为的篡改过了,从而尝试修改图片的高度或者宽度发现隐藏的字符串。

例题 WDCTF-FINALS-2017

观察文件可以发现, 文件头及宽度异常

00000000  80 59 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.YNG........IHDR|
00000010  00 00 00 00 00 00 02 f8  08 06 00 00 00 93 2f 8a  |............../.|
00000020  6b 00 00 00 04 67 41 4d  41 00 00 9c 40 20 0d e4  |k....gAMA...@ ..|
00000030  cb 00 00 00 20 63 48 52  4d 00 00 87 0f 00 00 8c  |.... cHRM.......|
00000040  0f 00 00 fd 52 00 00 81  40 00 00 7d 79 00 00 e9  |....R...@..}y...|
...

这里需要注意的是,文件宽度不能任意修改,需要根据 IHDR 块的 CRC 值爆破得到宽度, 否则图片显示错误不能得到 flag。

import os
import binascii
import struct


misc = open("misc4.png","rb").read()

for i in range(1024):
    data = misc[12:16] + struct.pack('>i',i)+ misc[20:29]
    crc32 = binascii.crc32(data) & 0xffffffff
    if crc32 == 0x932f8a6b:
        print i

得到宽度值为 709 后,恢复图片得到 flag。

PLTE

调色板数据块 PLTE(palette chunk):它包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。真彩色的 PNG 数据流也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

IDAT

图像数据块 IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。

  • 储存图像像数数据
  • 在数据流中可包含多个连续顺序的图像数据块
  • 采用 LZ77 算法的派生算法进行压缩
  • 可以用 zlib 解压缩

值得注意的是,IDAT 块只有当上一个块充满时,才会继续一个新的块。

pngcheck 去查看此 PNG 文件

λ .\pngcheck.exe -v sctf.png
File: sctf.png (1421461 bytes)
  chunk IHDR at offset 0x0000c, length 13
    1000 x 562 image, 32-bit RGB+alpha, non-interlaced
  chunk sRGB at offset 0x00025, length 1
    rendering intent = perceptual
  chunk gAMA at offset 0x00032, length 4: 0.45455
  chunk pHYs at offset 0x00042, length 9: 3780x3780 pixels/meter (96 dpi)
  chunk IDAT at offset 0x00057, length 65445
    zlib: deflated, 32K window, fast compression
  chunk IDAT at offset 0x10008, length 65524
...
  chunk IDAT at offset 0x150008, length 45027
  chunk IDAT at offset 0x15aff7, length 138
  chunk IEND at offset 0x15b08d, length 0
No errors detected in sctf.png (28 chunks, 36.8% compression).

可以看到,正常的块的 length 是在 65524 的时候就满了,而倒数第二个 IDAT 块长度是 45027,最后一个长度是 138,很明显最后一个 IDAT 块是有问题的,因为他本来应该并入到倒数第二个未满的块里.

利用 python zlib 解压多余 IDAT 块的内容,此时注意剔除 长度、数据块类型及末尾的 CRC 校验值

import zlib
import binascii
IDAT = "789...667".decode('hex')
result = binascii.hexlify(zlib.decompress(IDAT))
print result

IEND

图像结束数据 IEND(image trailer chunk):它用来标记 PNG 文件或者数据流已经结束,并且必须要放在文件的尾部。

00 00 00 00 49 45 4E 44 AE 42 60 82

IEND 数据块的长度总是 00 00 00 00,数据标识总是 IEND 49 45 4E 44,因此,CRC 码也总是 AE 42 60 82

其余辅助数据块

  • 背景颜色数据块 bKGD(background color)
  • 基色和白色度数据块 cHRM(primary chromaticities and white point),所谓白色度是指当 R=G=B=最大值 时在显示器上产生的白色度
  • 图像 γ 数据块 gAMA(image gamma)
  • 图像直方图数据块 hIST(image histogram)
  • 物理像素尺寸数据块 pHYs(physical pixel dimensions)
  • 样本有效位数据块 sBIT(significant bits)
  • 文本信息数据块 tEXt(textual data)
  • 图像最后修改时间数据块 tIME (image last-modification time)
  • 图像透明数据块 tRNS (transparency)
  • 压缩文本数据块 zTXt (compressed textual data)

LSB

LSB 全称 Least Significant Bit,最低有效位。PNG 文件中的图像像数一般是由 RGB 三原色(红绿蓝)组成,每一种颜色占用 8 位,取值范围为 0x000xFF,即有 256 种颜色,一共包含了 256 的 3 次方的颜色,即 16777216 种颜色。

而人类的眼睛可以区分约 1000 万种不同的颜色,意味着人类的眼睛无法区分余下的颜色大约有 6777216 种。

LSB 隐写就是修改 RGB 颜色分量的最低二进制位(LSB),每个颜色会有 8 bit,LSB 隐写就是修改了像数中的最低的 1 bit,而人类的眼睛不会注意到这前后的变化,每个像素可以携带 3 比特的信息。

如果是要寻找这种 LSB 隐藏痕迹的话,有一个工具 Stegsolve 是个神器,可以来辅助我们进行分析。

通过下方的按钮可以观察每个通道的信息,例如查看 R 通道的最低位第 8 位平面的信息。

LSB 的信息借助于 Stegsolve 查看各个通道时一定要细心捕捉异常点,抓住 LSB 隐写的蛛丝马迹。

例题

HCTF - 2016 - Misc

这题的信息隐藏在 RGB 三个通道的最低位中,借助 Stegsolve-->Analyse-->Data Extract 可以指定通道进行提取。

可以发现 zip 头,用 save bin 保存为压缩包后,打开运行其中的 ELF 文件就可以得到最后的 flag。

更多关于 LSB 的研究可以看 这里

隐写软件

Stepic

JPG

文件结构

  • JPEG 是有损压缩格式,将像素信息用 JPEG 保存成文件再读取出来,其中某些像素值会有少许变化。在保存时有个质量参数可在 0 至 100 之间选择,参数越大图片就越保真,但图片的体积也就越大。一般情况下选择 70 或 80 就足够了
  • JPEG 没有透明度信息

JPG 基本数据结构为两大类型:「段」和经过压缩编码的图像数据。

名 称 字节数 数据 说明
段 标识 1 FF 每个新段的开始标识
段类型 1 类型编码(称作标记码)
段长 度 2 包括段内容和段长度本身, 不包括段标识和段类型
段内容 2 ≤65533 字节
  • 有些段没有长度描述也没有内容,只有段标识和段类型。文件头和文件尾均属于这种段。
  • 段与段之间无论有多少 FF 都是合法的,这些 FF 称为「填充字节」,必须被忽略掉。

一些常见的段类型

0xffd80xffd9为 JPG 文件的开始结束的标志。

隐写软件

Stegdetect

通过统计分析技术评估 JPEG 文件的 DCT 频率系数的隐写工具, 可以检测到通过 JSteg、JPHide、OutGuess、Invisible Secrets、F5、appendX 和 Camouflage 等这些隐写工具隐藏的信息,并且还具有基于字典暴力破解密码方法提取通过 Jphide、outguess 和 jsteg-shell 方式嵌入的隐藏信息。

-q 仅显示可能包含隐藏内容的图像。
-n 启用检查JPEG文件头功能,以降低误报率。如果启用,所有带有批注区域的文件将被视为没有被嵌入信息。如果JPEG文件的JFIF标识符中的版本号不是1.1,则禁用OutGuess检测。
-s 修改检测算法的敏感度,该值的默认值为1。检测结果的匹配度与检测算法的敏感度成正比,算法敏感度的值越大,检测出的可疑文件包含敏感信息的可能性越大。
-d 打印带行号的调试信息。
-t 设置要检测哪些隐写工具(默认检测jopi),可设置的选项如下:
j 检测图像中的信息是否是用jsteg嵌入的。
o 检测图像中的信息是否是用outguess嵌入的。
p 检测图像中的信息是否是用jphide嵌入的。
i 检测图像中的信息是否是用invisible secrets嵌入的。

JPHS

JPEG 图像的信息隐藏软件 JPHS,它是由 Allan Latham 开发设计实现在 Windows 和 Linux 系统平台针对有损压缩 JPEG 文件进行信息加密隐藏和探测提取的工具。软件里面主要包含了两个程序 JPHIDE 和 JPSEEK。JPHIDE 程序主要是实现将信息文件加密隐藏到 JPEG 图像功能,而 JPSEEK 程序主要实现从用 JPHIDE 程序加密隐藏得到的 JPEG 图像探测提取信息文件,Windows 版本的 JPHS 里的 JPHSWIN 程序具有图形化操作界面且具备 JPHIDE 和 JPSEEK 的功能。

SilentEye

SilentEye is a cross-platform application design for an easy use of steganography, in this case hiding messages into pictures or sounds. It provides a pretty nice interface and an easy integration of new steganography algorithm and cryptography process by using a plug-ins system.

GIF

文件结构

一个 GIF 文件的结构可分为

  • 文件头(File Header)
    • GIF 文件署名(Signature)
    • 版本号(Version)
  • GIF 数据流(GIF Data Stream)
    • 控制标识符
    • 图象块(Image Block)
    • 其他的一些扩展块
  • 文件终结器(Trailer)

下表显示了一个 GIF 文件的组成结构:

中间的那个大块可以被重复任意次

文件头

GIF 署名(Signature)和版本号(Version)。GIF 署名用来确认一个文件是否是 GIF 格式的文件,这一部分由三个字符组成:GIF;文件版本号也是由三个字节组成,可以为 87a89a

逻辑屏幕标识符(Logical Screen Descriptor)

Logical Screen Descriptor(逻辑屏幕描述符)紧跟在 header 后面。这个块告诉 decoder(解码器)图片需要占用的空间。它的大小固定为 7 个字节,以 canvas width(画布宽度)和 canvas height(画布高度)开始。

全局颜色列表(Global Color Table)

GIF 格式可以拥有 global color table,或用于针对每个子图片集,提供 local color table。每个 color table 由一个 RGB(就像通常我们见到的(255,0,0)红色 那种)列表组成。

图像标识符(Image Descriptor)

一个 GIF 文件一般包含多个图片。之前的图片渲染模式一般是将多个图片绘制到一个大的(virtual canvas)虚拟画布上,而现在一般将这些图片集用于实现动画。

每个 image 都以一个 image descriptor block(图像描述块)作为开头,这个块固定为 10 字节。

图像数据(Image Data)

终于到了图片数据实际存储的地方。Image Data 是由一系列的输出编码(output codes)构成,它们告诉 decoder(解码器)需要绘制在画布上的每个颜色信息。这些编码以字节码的形式组织在这个块中。

文件终结器(Trailer)

该块为一个单字段块,用来指示该数据流的结束。取固定值 0x3b.

更多参见 gif 格式图片详细解析

空间轴

由于 GIF 的动态特性,由一帧帧的图片构成,所以每一帧的图片,多帧图片间的结合,都成了隐藏信息的一种载体。

对于需要分离的 GIF 文件, 可以使用convert命令将其每一帧分割开来

` sourceCode shell root in ~/Desktop/tmp λ convert cake.gif cake.png root in ~/Desktop/tmp λ ls cake-0.png cake-1.png cake-2.png cake-3.png cake.gif

### 例题

> WDCTF-2017:3-2

打开gif后,思路很清晰,分离每一帧图片后,将起合并得到完整的二维码即可

``` sourceCode python
from  PIL import Image


flag = Image.new("RGB",(450,450))

for i in range(2):
    for j in range(2):
        pot = "cake-{}.png".format(j+i*2)
        potImage = Image.open(pot)
        flag.paste(potImage,(j*225,i*225))
flag.save('./flag.png')

扫码后得到一串 16 进制字符串

03f30d0ab8c1aa5....74080006030908

开头03f3pyc文件的头,恢复为python脚本后直接运行得到 flag

时间轴

GIF 文件每一帧间的时间间隔也可以作为信息隐藏的载体。

例如在当时在 XMan 选拔赛出的一题

XMAN-2017:100.gif

通过identify命令清晰的打印出每一帧的时间间隔

$ identify -format "%s %T \n" 100.gif
0 66
1 66
2 20
3 10
4 20
5 10
6 10
7 20
8 20
9 20
10 20
11 10
12 20
13 20
14 10
15 10

推断 20 & 10 分别代表 0 & 1,提取每一帧间隔并进行转化。

$ cat flag|cut -d ' ' -f 2|tr -d '66'|tr -d '\n'|tr -d '0'|tr '2' '0'
0101100001001101010000010100111001111011001110010011011000110101001101110011010101100010011001010110010101100100001101000110010001100101011000010011000100111000011001000110010101100100001101000011011100110011001101010011011000110100001100110110000101100101011000110110011001100001001100110011010101111101#

最后转 ASCII 码得到 flag。

隐写软件

F5-steganography

音频隐写

与音频相关的 CTF 题目主要使用了隐写的策略,主要分为 MP3 隐写,LSB 隐写,波形隐写,频谱隐写等等。

常见手段

通过 binwalk 以及 strings 可以发现的信息不再详述。

MP3 隐写

原理

MP3 隐写主要是使用 Mp3Stego 工具进行隐写,其基本介绍及使用方法如下

MP3Stego will hide information in MP3 files during the compression process. The data is first compressed, encrypted and then hidden in the MP3 bit stream.

encode -E hidden_text.txt -P pass svega.wav svega_stego.mp3
decode -X -P pass svega_stego.mp3

例题

ISCC-2016: Music Never Sleep

初步观察后,由 strings 无发现,听音频无异常猜测使用隐写软件隐藏数据。

得到密码后使用 Mp3Stego 解密。

decode.exe -X ISCC2016.mp3 -P bfsiscc2016

得到文件 iscc2016.mp3.txt:

Flag is SkYzWEk0M1JOWlNHWTJTRktKUkdJTVpXRzVSV0U2REdHTVpHT1pZPQ== ???

Base64 && Base32 后得到 flag。

波形

原理

通常来说,波形方向的题,在观察到异常后,使用相关软件(Audacity, Adobe Audition 等)观察波形规律,将波形进一步转化为 01 字符串等,从而提取转化出最终的 flag。

例题

ISCC-2017: Misc-04

其实这题隐藏的信息在最开始的一段音频内,不细心听可能会误认为是隐写软件。

以高为 1 低为 0,转换得到 01 字符串。

110011011011001100001110011111110111010111011000010101110101010110011011101011101110110111011110011111101

转为 ASCII,摩斯密码解密,得到 flag。

Note

一些较复杂的可能会先对音频进行一系列的处理,如滤波等。例如 JarvisOJ - 上帝之音 Writeup

频谱

原理

音频中的频谱隐写是将字符串隐藏在频谱中,此类音频通常会有一个较明显的特征,听起来是一段杂音或者比较刺耳。

例题

Su-ctf-quals-2014:hear_with_your_eyes

LSB 音频隐写

原理

类似于图片隐写中的 LSB 隐写,音频中也有对应的 LSB 隐写。主要可以使用 Silenteye 工具,其介绍如下:

SilentEye is a cross-platform application design for an easy use of steganography, in this case hiding messages into pictures or sounds. It provides a pretty nice interface and an easy integration of new steganography algorithm and cryptography process by using a plug-ins system.

例题

2015 广东省强网杯 - Little Apple

直接使用 silenteye 即可。

延伸

流量包分析

流量包分析简介

CTF 比赛中, 流量包的取证分析是另一项重要的考察方向。

通常比赛中会提供一个包含流量数据的 PCAP 文件,有时候也会需要选手们先进行修复或重构传输文件后,再进行分析。

PCAP 这一块作为重点考察方向,复杂的地方在于数据包里充满着大量无关的流量信息,因此如何分类和过滤数据是参赛者需要完成的工作。

总的来说有以下几个步骤

  • 总体把握
    • 协议分级
    • 端点统计
  • 过滤赛选
    • 过滤语法
    • Host,Protocol,contains,特征值
  • 发现异常
    • 特殊字符串
    • 协议某字段
    • flag 位于服务器中
  • 数据提取
    • 字符串取
    • 文件提取

总的来说比赛中的流量分析可以概括为以下三个方向:

  • 流量包修复
  • 协议分析
  • 数据提取

PCAP 文件修复

PCAP 文件结构

一般来说, 对于 PCAP 文件格式考察较少,且通常都能借助于现成的工具如 pcapfix 直接修复,这里大致介绍下几个常见的块,详细可以翻看 Here

一般文件结构

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                          Block Type                           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                      Block Total Length                       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   /                          Block Body                           /
   /          /* variable length, aligned to 32 bits */            /
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                      Block Total Length                       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

目前所定义的常见块类型有

  1. Section Header Block: it defines the most important characteristics of the capture file.
  2. Interface Description Block: it defines the most important characteristics of the interface(s) used for capturing traffic.
  3. Packet Block: it contains a single captured packet, or a portion of it.
  4. Simple Packet Block: it contains a single captured packet, or a portion of it, with only a minimal set of information about it.
  5. Name Resolution Block: it defines the mapping from numeric addresses present in the packet dump and the canonical name counterpart.
  6. Capture Statistics Block: it defines how to store some statistical data (e.g. packet dropped, etc) which can be useful to undestand the conditions in which the capture has been made.

常见块

Section Header BlocK(文件头)

必须存在, 意味着文件的开始

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                Byte-Order Magic (0x1A2B3C4D)                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |   Major Version(主版本号)   |    Minor Version(次版本号)        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   |                          Section Length                       |
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   /                                                               /
   /                      Options (variable)                       /
   /                                                               /
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Interface Description Block(接口描述)

必须存在, 描述接口特性

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           LinkType            |           Reserved            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                  SnapLen(每个数据包最大字节数)                  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   /                                                               /
   /                      Options (variable)                       /
   /                                                               /
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Packet Block(数据块)

    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Interface ID          |          Drops Count          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                     Timestamp (High)   标准的Unix格式          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Timestamp (Low)                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                         Captured Len                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                          Packet Len                           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   /                          Packet Data                          /
   /          /* variable length, aligned to 32 bits */            /
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   /                      Options (variable)                       /
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

例题

题目:第一届 “百度杯” 信息安全攻防总决赛 线上选拔赛:find the flag

WP:https://www.cnblogs.com/ECJTUACM-873284962/p/9884447.html

首先我们拿到这样一道流量包的题目,题目名称为 find the flag 。这里面给了很多提示信息,要我们去找到 flag

第一步,搜索 flag 字样

我们先去搜索看看流量包里面有没有 flag 。我们使用 strings 命令去找一下流量包, Windows 的朋友可以用 notepad++ 的搜索功能去寻找。

搜索命令如下:

strings findtheflag.cap | grep flag

搜索结果如下:

strings-to-flag

我们发现搜出了一大堆的东西,我们通过管道去过滤出 flag 信息,似乎没有发现我们所需要找的答案。

第二步,流量包修复

我们用 wireshark 打开这个流量包

wireshark-to-error

我们发现这个流量包出现了异常现象,我们可以修复一下这个流量包。

这里我们用到一个在线工具:http://f00l.de/hacking/pcapfix.php

这个工具可以帮助我们快速地将其流量包修复为 pcap 包。

我们对其进行在线修复。

repaire-to-pcap

修复完毕后点击 Get your repaired PCAP-file here. 即可下载流量包,然后我们用 wireshark 打开。

既然还是要找 flag ,我们可以先看看这个流量包。

第三步,追踪 TCP 流

我们追踪一下 TCP 流,看看有没有什么突破?

wireshark-to-stream

我们通过追踪 TCP 流,可以看到一些版本信息, cookie 等等,我们还是发现了一些很有意思的东西。

tcp.stream eq 29tcp.stream eq 41 只显示了 where is the flag? 这个字样,难道这是出题人在告诉我们 flag 在这里嘛?

第四步,查找分组字节流

我们追踪到 tcp.stream eq 29 的时候,在 Identification 信息中看到了 flag 中的 lf 字样,我们可以继续追踪下一个流,在 tcp.stream eq 30Identification 信息中看到了 flag 中的 ga 字样,我们发现将两个包中 Identification 信息对应的字段从右至左组合,恰好就是 flag !于是我们可以大胆地猜测, flag 肯定是藏在这里面。

我们直接通过搜索 -> 字符串搜索 -> 分组字节流 -> 搜索关键字 flag 即可,按照同样的方式连接后面相连数据包的 Identification 信息对应的字段,即可找到最终的 flag!

下面是搜索的截图:

所以最终的 flag 为:flag{aha!_you_found_it!}

数据提取

这一块是流量包中另一个重点, 通过对协议分析, 找到了题目的关键点, 如何提取数据成了接下来的关键问题

wireshark

wireshark 自动分析

file -> export objects -> http

手动数据提取

file->export selected Packet Bytes

tshark

tshark 作为 wireshark 的命令行版, 高效快捷是它的优点, 配合其余命令行工具 (awk,grep) 等灵活使用, 可以快速定位, 提取数据从而省去了繁杂的脚本编写

再看Google CTF 2016 Forensic-200这一题, 可以通过 tshark 迅速完成解题

what@kali:/tmp$ tshark -r capture.pcapng -T fields -e usb.capdata > data2.txt
what@kali:/tmp$ # awk -F: 'function comp(v){if(v>127)v-=256;return v}{x+=comp(strtonum("0x"$2));y+=comp(strtonum("0x"$3))}$1=="01"{print x,y}' data.txt > data3.txt
what@kali:/tmp$ gnuplot
> plot "data3.txt"
  • Step 1 鼠标协议中数据提取
  • Step 2 通过 awk 进行位置坐标转换
  • Step 3 形成图形

常用方法

tshark -r **.pcap –Y ** -T fields –e ** | **** > data
Usage:
  -Y <display filter>      packet displaY filter in Wireshark display filter
                           syntax
  -T pdml|ps|psml|json|jsonraw|ek|tabs|text|fields|?
                           format of text output (def: text)
  -e <field>               field to print if -Tfields selected (e.g. tcp.port,
                           _ws.col.Info)

通过-Y过滤器 (与 wireshark 一致), 然后用-T filds -e配合指定显示的数据段 (比如 usb.capdata)

  • tips
    • -e后的参数不确定可以由 wireshark 右击需要的数据选中后得到

例题

题目:google-ctf-2016 : a-cute-stegosaurus-100

这题的数据隐藏的非常巧妙, 而且有一张图片混淆视听, 需要对tcp协议非常熟悉, 所以当时做出来的人并不多, 全球只有 26 支队伍

tcp报文段中有 6Bit 的状态控制码, 分别如下

  • URG:紧急比特(urgent), 当 URG=1 时,表明紧急指针字段有效, 代表该封包为紧急封包。它告诉系统此报文段中有紧急数据,应尽快传送 (相当于高优先级的数据)
  • ACK:确认比特(Acknowledge)。只有当 ACK=1 时确认号字段才有效, 代表这个封包为确认封包。当 ACK=0 时,确认号无效。
  • PSH:(Push function)若为 1 时,代表要求对方立即传送缓冲区内的其他对应封包,而无需等缓冲满了才送。
  • RST:复位比特 (Reset) , 当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
  • SYN:同步比特 (Synchronous),SYN 置为 1,就表示这是一个连接请求或连接接受报文, 通常带有 SYN 标志的封包表示『主动』要连接到对方的意思。。
  • FIN:终止比特 (Final),用来释放一个连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。

而这里的tcp.urg却为

urg

通过 tshark 提取tcp.urg然后去除 0 的字段, 换行符转,直接转换成 python 的列表, 转 ascii 即可得到 flag

⚡ root@kali:  tshark -r Stego-200_urg.pcap -T fields -e  tcp.urgent_pointer|egrep -vi "^0$"|tr '\n' ','
Running as user "root" and group "root". This could be dangerous.
67,84,70,123,65,110,100,95,89,111,117,95,84,104,111,117,103,104,116,95,73,116,95,87,97,115,95,73,110,95,84,104,101,95,80,105,99,116,117,114,101,125,#
...
>>> print "".join([chr(x) for x in arr]) #python转换ascii
CTF{And_You_Thought_It_Was_In_The_Picture}

题目:stego-150_ears.xz

Step 1

通过file命令不断解压得到 pcap 文件

➜  Desktop file ears
ears: XZ compressed data
➜  Desktop unxz < ears > file_1
➜  Desktop file file_1
file_1: POSIX tar archive
➜  Desktop 7z x file_1

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,1 CPU Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz (306C3),ASM,AES-NI)

    Scanning the drive for archives:
    1 file, 4263936 bytes (4164 KiB)

    Extracting archive: file_1
    --
    Path = file_1
    Type = tar
    Physical Size = 4263936
    Headers Size = 1536
    Code Page = UTF-8

    Everything is Ok

    Size:       4262272
    Compressed: 4263936

Step 2

通过 wireshark 发现 dns 中回应名字存在异常,组成 16 进制的 png 文件

采用 tshark 进行提取,提取 dns 中的数据, 筛选具体报文形式\w{4,}.asis.io

tshark -r forensic_175_d78a42edc01c9104653776f16813d9e5 -T fields -e dns.qry.name -e dns.flags|grep 8180|awk '{if ($1~/\w{4,}.asis.io/) print $1}'|awk -F '.' '{print $1}'|tr -d '\n' > png

Step 3

16 进制还原图片

xxd -p -r png flag

自定义协议

提取数据存在一类特殊情况,即传输的数据本身使用自定义协议,下面用 HITCON 2018 的两道 Misc 为例说明。

例题分析

ev3 basic

确定数据

对于这类题目,首先分析有效数据位于哪些包中。观察流量,通讯双方为 localhostLegoSystem 。其中大量标为 PKTLOG 的数据包都是日志,此题中不需关注。简单浏览其余各个协议的流量,发现仅 RFCOMM 协议中存在没有被 wireshark 解析的 data 段,而 RFCOMM 正是蓝牙使用的传输层协议之一。

由前述 tshark 相关介绍,可以通过以下命令提取数据:

tshark -r .\ev3_basic.pklg -T fields -e data -Y "btrfcomm"
分析协议

找到数据后,需要确定数据格式。如何查找资料可以参考 信息搜集技术 一节,此处不再赘述。总之由 ev3 这个关键词出发,我们最终知道这种通信方式传输的内容被称之为 Direct Command,所使用的是乐高自定义的一种[简单应用层协议](https://le-www-live-s.legocdn.com/sc/media/files/ev3-developer-kit/lego mindstorms ev3 communication developer kit-f691e7ad1e0c28a4cfb0835993d76ae3.pdf?la=en-us),Command 本身格式由乐高的手册 EV3 Firmware Developer Kit 定义。(查找过程并不像此处简单而直观,也是本题的关键点之一。)

在乐高的协议中,发送和回复遵从不同格式。在 ev3 basic 中,所有回复流量都相同,通过手册可知内容代表 ok ,没有实际含义,而发送的每个数据包都包含了一条指令。由协议格式解析出指令的 Opcode 均为 0x84 ,代表 UI_DRAW 函数,且 CMD0x05 ,代表 TEXT 。之后是四个参数,Color, X0, Y0, STRING 。此处需要注意乐高的单个参数字节数并不固定,即便手册上标明了数据类型是 DATA16 ,仍然可能使用一个字节长度的参数,需要参照手册中 Parameter encoding 一节及相关文章

尝试分析几个命令,发现每个指令都会在屏幕特定位置打印一个字符,这与提供的图片相符。

处理结果

理解数据内容后,通过脚本提取所有命令并解析参数,需要注意单个参数的字节数不固定。

得到所有命令的参数后,可以将每个字符其按照坐标绘制在屏幕上。较简单的做法是先按 X 后按 Y 排序,直接输出即可。

ev3 scanner

第二题的做法与第一题基本相同,难度增加的地方在于:

  • 发送的命令不再单一,包括读取传感器信息、控制 ev3 运动
  • 回复也包含信息,主要是传感器读取的内容
  • 函数的参数更复杂,解析难度更大
  • 解析命令得到的结果需要更多处理

ev3 scanner 此处不再提供详细方法,可作为练习加深对这一类型题目的理解。

协议分析

协议分析概述

网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。例如,网络中一个微机用户和一个大型主机的操作员进行通信,由于这两个数据终端所用字符集不同,因此操作员所输入的命令彼此不认识。为了能进行通信,规定每个终端都要将各自字符集中的字符先变换为标准字符集的字符后,才进入网络传送,到达目的终端之后,再变换为该终端字符集的字符。当然,对于不相容终端,除了需变换字符集字符外还需转换其他特性,如显示格式、行长、行数、屏幕滚动方式等也需作相应的变换。

相应的,我们在协议分析这一章节中,将会从以下几个方面来介绍这一部分的知识:

  • Wireshark 常用功能的介绍
  • HTTP 协议分析
  • HTTPS 协议分析
  • FTP 协议分析
  • DNS 协议分析
  • WIFI 协议分析
  • USB 协议分析

Wireshark

Wireshark 常用功能介绍

显示过滤器

显示过滤器可以用很多不同的参数来作为匹配标准,比如 IP 地址、协议、端口号、某些协议头部的参数。此外,用户也用一些条件工具和串联运算符创建出更加复杂的表达式。用户可以将不同的表达式组合起来,让软件显示的数据包范围更加精确。在数据包列表面板中显示的所有数据包都可以用数据包中包含的字段进行过滤。

[not] Expression [and|or] [not] Expression

经常要用到各种运算符

运算符 说明
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
and , &&
or , ||
! , not

配置方法

  1. 借助于过滤器窗口

​ 2. 借助于工具条的输入栏

  1. 将数据包某个属性值指定为过滤条件

复杂的过滤命令可以直接通过第三种方式得到过滤语法

信息统计

Protocol History(协议分级)

这个窗口现实的是捕捉文件包含的所有协议的树状分支

包含的字段

名称 含义
Protocol: 协议名称
% Packets: 含有该协议的包数目在捕捉文件所有包所占的比例
Packets: 含有该协议的包的数目
Bytes: 含有该协议的字节数
Mbit/s: 抓包时间内的协议带宽
End Packets: 该协议中的包的数目(作为文件中的最高协议层)
End Bytes: 该协议中的字节数(作为文件中的最高协议层)
End Mbit/s: 抓包时间内的协议带宽(作为文件中的最高协议层)

这一功能可以为分析数据包的主要方向提供依据

Conversation(对话)

发生于一特定端点的 IP 间的所有流量.

  • 查看收发大量数据流的 IP 地址。如果是你知道的服务器(你记得服务器的地址或地址范围),那问题就解决了;但也有可能只是某台设备正在扫描网络,或仅是一台产生过多数据的 PC。
  • 查看扫描模式(scan pattern)。这可能是一次正常的扫描,如 SNMP 软件发送 ping 报文以查找网络,但通常扫描都不是好事情
EndPoints(端点)

这一工具列出了 Wireshark 发现的所有 endpoints 上的统计信息

HTTP
  • Packet Counter

HTTP

HTTP ( Hyper Text Transfer Protocol ,也称为超文本传输协议) 是一种用于分布式、协作式和超媒体信息系统的应用层协议。 HTTP 是万维网的数据通信的基础。

例题

题目:江苏省领航杯 - 2017:hack

总体观察可以得出:

  • HTTP为主
  • 192.168.173.134为主
  • 不存在附件

从这张图, 基本可以判断初这是一个在sql注入-盲注时产生的流量包

到此为止, 基本可以判断 flag 的方向, 提取出所有的 url 后, 用python辅助即可得到 flag

  • 提取 url: tshark -r hack.pcap -T fields -e http.request.full_uri|tr -s '\n'|grep flag > log
  • 得到盲注结果
import re

with open('log') as f:
    tmp = f.read()
    flag = ''
    data = re.findall(r'=(\d*)%23',tmp)
    data = [int(i) for i in data]
    for i,num in enumerate(data):
        try:
            if num > data[i+1]:
                flag += chr(num)
        except Exception:
            pass
    print flag

HTTPS

HTTPs = HTTP + SSL / TLS. 服务端和客户端的信息传输都会通过 TLS 进行加密,所以传输的数据都是加密后的数据

例题

题目:hack-dat-kiwi-ctf-2015:ssl-sniff-2

打开流量包发现是 SSL 加密过的数据, 导入题目提供的server.key.insecure, 即可解密

GET /key.html HTTP/1.1
Host: localhost

HTTP/1.1 200 OK
Date: Fri, 20 Nov 2015 14:16:24 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Fri, 20 Nov 2015 14:15:54 GMT
ETag: "1c-524f98378d4e1"
Accept-Ranges: bytes
Content-Length: 28
Content-Type: text/html

The key is 39u7v25n1jxkl123

FTP

FTP ( File Transfer Protocol ,即文件传输协议) 是 TCP/IP 协议组中的协议之一。 FTP 协议包括两个组成部分,其一为 FTP 服务器,其二为 FTP 客户端。其中 FTP 服务器用来存储文件,用户可以使用 FTP 客户端通过 FTP 协议访问位于 FTP 服务器上的资源。在开发网站的时候,通常利用 FTP 协议把网页或程序传到 Web 服务器上。此外,由于 FTP 传输效率非常高,在网络上传输大的文件时,一般也采用该协议。

默认情况下 FTP 协议使用 TCP 端口中的 2021 这两个端口,其中 20 用于传输数据, 21 用于传输控制信息。但是,是否使用 20 作为传输数据的端口与 FTP 使用的传输模式有关,如果采用主动模式,那么数据传输端口就是 20 ;如果采用被动模式,则具体最终使用哪个端口要服务器端和客户端协商决定。

DNS

简介

DNS 通常为 UDP 协议, 报文格式

+-------------------------------+
| 报文头                         |
+-------------------------------+
| 问题 (向服务器提出的查询部分)    |
+-------------------------------+
| 回答 (服务器回复的资源记录)      |
+-------------------------------+
| 授权 (权威的资源记录)           |
+-------------------------------+
| 额外的 (额外的资源记录)         |
+-------------------------------+

查询包只有头部和问题两个部分, DNS 收到查询包后,根据查询到的信息追加回答信息、授权机构、额外资源记录,并且修改了包头的相关标识再返回给客户端。

每个 question 部分

   0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                                               |
 /                     QNAME                     /
 /                                               /
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                     QTYPE                     |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 |                     QCLASS                    |
 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • QNAME :为查询的域名,是可变长的,编码格式为:将域名用. 号划分为多个部分,每个部分前面加上一个字节表示该部分的长度,最后加一个 0 字节表示结束
  • QTYPE :占 16 位,表示查询类型,共有 16 种,常用值有:1 ( A 记录,请求主机 IP 地址)、2 ( NS ,请求授权 DNS 服务器)、5 ( CNAME 别名查询)

例题

题目:BSides San Francisco CTF 2017dnscap.pcap

我们通过 wireshark 打开发现全部为 DNS 协议, 查询名为大量字符串([\w\.]+)\.skullseclabs\.org

我们通过 tshark -r dnscap.pcap -T fields -e dns.qry.name > hex提取后,利用 python 转码:

import re


find = ""

with open('hex','rb') as f:
    for i in f:
        text = re.findall(r'([\w\.]+)\.skull',i)
        if text:
            find += text[0].replace('.','')
print find

我们发现了几条关键信息:

Welcome to dnscap! The flag is below, have fun!!
Welcome to dnscap! The flag is below, have fun!!
!command (sirvimes)
...
IHDR
gAMA
bKGD
        pHYs
IHDR
gAMA
bKGD
        pHYs
tIME
IDATx
...
2017-02-01T21:04:00-08:00
IEND
console (sirvimes)
console (sirvimes)
Good luck! That was dnscat2 traffic on a flaky connection with lots of re-transmits. Seriously,
Good luck! That was dnscat2 traffic on a flaky connection with lots of re-transmits. Seriously, d[
good luck. :)+

flag 确实包含在其中, 但是有大量重复信息, 一是应为question 。在 dns 协议中查询和反馈时都会用到,-Y "ip.src == 192.168.43.91"进行过滤后发现还是有不少重复部分。

%2A}
%2A}
%2A}q
%2A}x
%2A}
IHDR
gAMA
bKGD
        pHYs
tIME
IDATx
HBBH
CxRH!
C1%t
ceyF
i4ZI32
rP@1
ceyF
i4ZI32
rP@1
ceyF
i4ZI32
rP@1
ceyF
i4ZI32
rP@1

根据发现的 dnscat 找到 https://github.com/iagox86/dnscat2/blob/master/doc/protocol.md 这里介绍了 dnscat 协议的相关信息, 这是一种通过 DNS 传递数据的变种协议, 题目文件中应该未使用加密, 所以直接看这里的数据块信息

MESSAGE_TYPE_MSG: [0x01]
(uint16_t) packet_id
(uint8_t) message_type [0x01]
(uint16_t) session_id
(uint16_t) seq
(uint16_t) ack
(byte[]) data

qry.name中去除其余字段, 只留下 data 快, 从而合并数据, 再从 16 进制中检索89504e.....6082提取png, 得到 flag

import re


find = []

with open('hex','rb') as f:
    for i in f:
        text = re.findall(r'([\w\.]+)\.skull',i)
        if text:
            tmp =  text[0].replace('.','')
            find.append(tmp[18:])
last = []

for i in find:
    if i not in last:
        last.append(i)


print  ''.join(last)

flag

dnscat_flag

相关题目

WIFI

802.11 是现今无线局域网通用的标准, 常见认证方式

  • 不启用安全‍‍
  • WEP‍‍
  • WPA/WPA2-PSK(预共享密钥)‍‍
  • PA/WPA2 802.1Xradius 认证)

WPA-PSK

其中四次握手过程

  1. 4 次握手开始于验证器 (AP),它产生一个随机的值(ANonce) 发送给请求者
  2. 请求者也产生了它自己的随机 SNonce,然后用这两个 Nonces 以及 PMK 生成了 PTK。请求者回复消息 2 给验证器, 还有一个 MIC(message integrity code,消息验证码)作为 PMK 的验证
  3. 它先要验证请求者在消息 2 中发来的 MIC 等信息,验证成功后,如果需要就生成 GTK。然后发送消息 3
  4. 请求者收到消息 3,验证 MIC,安装密钥,发送消息 4,一个确认信息。验证器收到消息 4,验证 MIC,安装相同的密钥

例题

实验吧: shipin.cap

从大量的Deauth 攻击基本可以判断是一个破解 wifi 时的流量攻击

同时也成功发现了握手包信息

接下来跑密码

  • linuxaircrack 套件
  • windowswifipr ,速度比 esaw 快, GTX850 能将近 10w\s :)

得到密码88888888wiresharkEdit -> Preferences -> Protocols -> IEEE802.11 -> Editkey:SSID形式填入即可解密 wifi 包看到明文流量

KCARCK 相关: https://www.krackattacks.com/

USB

简介

USB 详述: https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf

  • 鼠标协议

鼠标移动时表现为连续性,与键盘击键的离散性不一样,不过实际上鼠标动作所产生的数据包也是离散的,毕竟计算机表现的连续性信息都是由大量离散信息构成的

每一个数据包的数据区有四个字节,第一个字节代表按键,当取 0x00 时,代表没有按键、为 0x01 时,代表按左键,为 0x02 时,代表当前按键为右键。第二个字节可以看成是一个 signed byte 类型,其最高位为符号位,当这个值为正时,代表鼠标水平右移多少像素,为负时,代表水平左移多少像素。第三个字节与第二字节类似,代表垂直上下移动的偏移。

得到这些点的信息后, 即可恢复出鼠标移动轨迹

键盘数据包的数据长度为 8 个字节,击键信息集中在第 3 个字节

根据 data 值与具体键位的对应关系

可从数据包恢复出键盘的案件信息

参考

例题

Xman`三期夏令营排位赛练习题:`AutoKey

WP:https://www.cnblogs.com/ECJTUACM-873284962/p/9473808.html

问题描述:

这道题是我参加 Xman 三期夏令营选拔赛出的一道题,我们如何对其进行分析?

流量包是如何捕获的?

首先我们从上面的数据包分析可以知道,这是个 USB 的流量包,我们可以先尝试分析一下 USB 的数据包是如何捕获的。

在开始前,我们先介绍一些 USB 的基础知识。 USB 有不同的规格,以下是使用 USB 的三种方式:

l USB UART
l USB HID
l USB Memory

UART 或者 Universal Asynchronous Receiver/Transmitter 。这种方式下,设备只是简单的将 USB 用于接受和发射数据,除此之外就再没有其他通讯功能了。

HID 是人性化的接口。这一类通讯适用于交互式,有这种功能的设备有:键盘,鼠标,游戏手柄和数字显示设备。

最后是 USB Memory ,或者说是数据存储。 External HDDthumb drive/flash drive 等都是这一类的。

其中使用的最广的不是 USB HID 就是 USB Memory 了。

每一个 USB 设备(尤其是 HID 或者 Memory )都有一个供应商 ID(Vendor ID) 和产品识别码(Product Id)Vendor ID 是用来标记哪个厂商生产了这个 USB 设备。 Product ID 用来标记不同的产品,他并不是一个特殊的数字,当然最好不同。如下图:

上图是我在虚拟机环境下连接在我电脑上的 USB 设备列表,通过 lsusb 查看命令。

例如说,我在 VMware 下有一个无线鼠标。它是属于 HID 设备。这个设备正常的运行,并且通过lsusb 这个命令查看所有 USB 设备,现在大家能找出哪一条是这个鼠标吗??没有错,就是第四个,就是下面这条:

Bus 002 Device 002: ID 0e0f:0003 VMware, Inc. Virtual Mouse

其中,ID 0e0f:0003 就是 Vendor-Product ID 对, Vendor ID 的值是 0e0f ,并且 Product ID 的值是 0003Bus 002 Device 002 代表 usb 设备正常连接,这点需要记下来。

我们用 root 权限运行 Wireshark 捕获 USB 数据流。但是通常来说我们不建议这么做。我们需要给用户足够的权限来获取 Linux 中的 usb 数据流。我们可以用 udev 来达到我们的目的。我们需要创建一个用户组 usbmon ,然后把我们的账户添加到这个组中。

addgroup usbmon
gpasswd -a $USER usbmon
echo 'SUBSYSTEM=="usbmon", GROUP="usbmon", MODE="640"' > /etc/udev/rules.d/99-usbmon.rules

接下来,我们需要 usbmon 内核模块。如果该模块没有被加载,我们可以通过以下命令加载该模块:

modprobe usbmon

打开 wireshark ,你会看到 usbmonX 其中 X 代表数字。下图是我们本次的结果(我使用的是root):

如果接口处于活跃状态或者有数据流经过的时候, wireshark 的界面就会把它以波形图的方式显示出来。那么,我们该选那个呢?没有错,就是我刚刚让大家记下来的,这个 X 的数字就是对应这 USB Bus 。在本文中是 usbmon0 。打开他就可以观察数据包了。

通过这些,我们可以了解到 usb 设备与主机之间的通信过程和工作原理,我们可以来对流量包进行分析了。

如何去分析一个 USB 流量包?

根据前面的知识铺垫,我们大致对 USB 流量包的抓取有了一个轮廓了,下面我们介绍一下如何分析一个 USB 流量包。

USB 协议的细节方面参考 wiresharkwikihttps://wiki.wireshark.org/USB

我们先拿 GitHub 上一个简单的例子开始讲起:

我们分析可以知道, USB 协议的数据部分在 Leftover Capture Data 域之中,在 MacLinux 下可以用 tshark 命令可以将 leftover capture data 单独提取出来,命令如下:

tshark -r example.pcap -T fields -e usb.capdata //如果想导入usbdata.txt文件中,后面加上参数:>usbdata.txt
Windows` 下装了 `wireshark` 的环境下,在 `wireshark`目录下有个 `tshark.exe` ,比如我的在 `D:\Program Files\Wireshark\tshark.exe

调用 cmd ,定位到当前目录下,输入如下命令即可:

tshark.exe -r example.pcap -T fields -e usb.capdata //如果想导入usbdata.txt文件中,后面加上参数:>usbdata.txt

有关 tshark 命令的详细使用参考 wireshark 官方文档:https://www.wireshark.org/docs/man-pages/tshark.html

运行命令并查看 usbdata.txt 发现数据包长度为八个字节

关于 USB 的特点应用我找了一张图,很清楚的反应了这个问题:

这里我们只关注 USB 流量中的键盘流量和鼠标流量。

键盘数据包的数据长度为 8 个字节,击键信息集中在第 3 个字节,每次 key stroke 都会产生一个 keyboard event usb packet

鼠标数据包的数据长度为 4 个字节,第一个字节代表按键,当取 0x00 时,代表没有按键、为 0x01 时,代表按左键,为 0x02 时,代表当前按键为右键。第二个字节可以看成是一个 signed byte 类型,其最高位为符号位,当这个值为正时,代表鼠标水平右移多少像素,为负时,代表水平左移多少像素。第三个字节与第二字节类似,代表垂直上下移动的偏移。

我翻阅了大量的 USB 协议的文档,在这里我们可以找到这个值与具体键位的对应关系:https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf

usb keyboard 的映射表 根据这个映射表将第三个字节取出来,对应对照表得到解码:

我们写出如下脚本:

mappings = { 0x04:"A",  0x05:"B",  0x06:"C", 0x07:"D", 0x08:"E", 0x09:"F", 0x0A:"G",  0x0B:"H", 0x0C:"I",  0x0D:"J", 0x0E:"K", 0x0F:"L", 0x10:"M", 0x11:"N",0x12:"O",  0x13:"P", 0x14:"Q", 0x15:"R", 0x16:"S", 0x17:"T", 0x18:"U",0x19:"V", 0x1A:"W", 0x1B:"X", 0x1C:"Y", 0x1D:"Z", 0x1E:"1", 0x1F:"2", 0x20:"3", 0x21:"4", 0x22:"5",  0x23:"6", 0x24:"7", 0x25:"8", 0x26:"9", 0x27:"0", 0x28:"n", 0x2a:"[DEL]",  0X2B:"    ", 0x2C:" ",  0x2D:"-", 0x2E:"=", 0x2F:"[",  0x30:"]",  0x31:"\\", 0x32:"~", 0x33:";",  0x34:"'", 0x36:",",  0x37:"." }
nums = []
keys = open('usbdata.txt')
for line in keys:
    if line[0]!='0' or line[1]!='0' or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0':
         continue
    nums.append(int(line[6:8],16))
    # 00:00:xx:....
keys.close()
output = ""
for n in nums:
    if n == 0 :
        continue
    if n in mappings:
        output += mappings[n]
    else:
        output += '[unknown]'
print('output :n' + output)

结果如下:

我们把前面的整合成脚本,得:

#!/usr/bin/env python

import sys
import os

DataFileName = "usb.dat"

presses = []

normalKeys = {"04":"a", "05":"b", "06":"c", "07":"d", "08":"e", "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j", "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o", "13":"p", "14":"q", "15":"r", "16":"s", "17":"t", "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y", "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4", "22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

shiftKeys = {"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T", "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y", "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$", "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"","34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

def main():
    # check argv
    if len(sys.argv) != 2:
        print "Usage : "
        print "        python UsbKeyboardHacker.py data.pcap"
        print "Tips : "
        print "        To use this python script , you must install the tshark first."
        print "        You can use `sudo apt-get install tshark` to install it"
        print "        Thank you for using."
        exit(1)

    # get argv
    pcapFilePath = sys.argv[1]

    # get data of pcap
    os.system("tshark -r %s -T fields -e usb.capdata > %s" % (pcapFilePath, DataFileName))

    # read data
    with open(DataFileName, "r") as f:
        for line in f:
            presses.append(line[0:-1])
    # handle
    result = ""
    for press in presses:
        Bytes = press.split(":")
        if Bytes[0] == "00":
            if Bytes[2] != "00":
                result += normalKeys[Bytes[2]]
        elif Bytes[0] == "20": # shift key is pressed.
            if Bytes[2] != "00":
                result += shiftKeys[Bytes[2]]
        else:
            print "[-] Unknow Key : %s" % (Bytes[0])
    print "[+] Found : %s" % (result)

    # clean the temp data
    os.system("rm ./%s" % (DataFileName))


if __name__ == "__main__":
    main()

效果如下:

另外贴上一份鼠标流量数据包转换脚本:

nums = [] 
keys = open('usbdata.txt','r') 
posx = 0 
posy = 0 
for line in keys: 
if len(line) != 12 : 
     continue 
x = int(line[3:5],16) 
y = int(line[6:8],16) 
if x > 127 : 
    x -= 256 
if y > 127 : 
    y -= 256 
posx += x 
posy += y 
btn_flag = int(line[0:2],16)  # 1 for left , 2 for right , 0 for nothing 
if btn_flag == 1 : 
    print posx , posy 
keys.close()

键盘流量数据包转换脚本如下:

nums=[0x66,0x30,0x39,0x65,0x35,0x34,0x63,0x31,0x62,0x61,0x64,0x32,0x78,0x33,0x38,0x6d,0x76,0x79,0x67,0x37,0x77,0x7a,0x6c,0x73,0x75,0x68,0x6b,0x69,0x6a,0x6e,0x6f,0x70]
s=''
for x in nums:
    s+=chr(x)
print s
mappings = { 0x41:"A",  0x42:"B",  0x43:"C", 0x44:"D", 0x45:"E", 0x46:"F", 0x47:"G",  0x48:"H", 0x49:"I",  0x4a:"J", 0x4b:"K", 0x4c:"L", 0x4d:"M", 0x4e:"N",0x4f:"O",  0x50:"P", 0x51:"Q", 0x52:"R", 0x53:"S", 0x54:"T", 0x55:"U",0x56:"V", 0x57:"W", 0x58:"X", 0x59:"Y", 0x5a:"Z", 0x60:"0", 0x61:"1", 0x62:"2", 0x63:"3", 0x64:"4",  0x65:"5", 0x66:"6", 0x67:"7", 0x68:"8", 0x69:"9", 0x6a:"*", 0x6b:"+",  0X6c:"separator", 0x6d:"-",  0x6e:".", 0x6f:"/" }
output = ""
for n in nums:
    if n == 0 :
        continue
    if n in mappings:
        output += mappings[n]
    else:
        output += '[unknown]'
print 'output :\n' + output

那么对于 xman 三期夏令营排位赛的这道题,我们可以模仿尝试如上这个例子:

首先我们通过 tsharkusb.capdata 全部导出:

tshark -r task_AutoKey.pcapng -T fields -e usb.capdata //如果想导入usbdata.txt文件中,后面加上参数:>usbdata.txt

结果如下:

我们用上面的 python 脚本将第三个字节取出来,对应对照表得到解码:

mappings = { 0x04:"A",  0x05:"B",  0x06:"C", 0x07:"D", 0x08:"E", 0x09:"F", 0x0A:"G",  0x0B:"H", 0x0C:"I",  0x0D:"J", 0x0E:"K", 0x0F:"L", 0x10:"M", 0x11:"N",0x12:"O",  0x13:"P", 0x14:"Q", 0x15:"R", 0x16:"S", 0x17:"T", 0x18:"U",0x19:"V", 0x1A:"W", 0x1B:"X", 0x1C:"Y", 0x1D:"Z", 0x1E:"1", 0x1F:"2", 0x20:"3", 0x21:"4", 0x22:"5",  0x23:"6", 0x24:"7", 0x25:"8", 0x26:"9", 0x27:"0", 0x28:"n", 0x2a:"[DEL]",  0X2B:"    ", 0x2C:" ",  0x2D:"-", 0x2E:"=", 0x2F:"[",  0x30:"]",  0x31:"\\", 0x32:"~", 0x33:";",  0x34:"'", 0x36:",",  0x37:"." }
nums = []
keys = open('usbdata.txt')
for line in keys:
    if line[0]!='0' or line[1]!='0' or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0':
         continue
    nums.append(int(line[6:8],16))
    # 00:00:xx:....
keys.close()
output = ""
for n in nums:
    if n == 0 :
        continue
    if n in mappings:
        output += mappings[n]
    else:
        output += '[unknown]'
print('output :n' + output)

运行结果如下:

output :n[unknown]A[unknown]UTOKEY''.DECIPHER'[unknown]MPLRVFFCZEYOUJFJKYBXGZVDGQAURKXZOLKOLVTUFBLRNJESQITWAHXNSIJXPNMPLSHCJBTYHZEALOGVIAAISSPLFHLFSWFEHJNCRWHTINSMAMBVEXO[DEL]PZE[DEL]IZ'

我们可以看出这是自动密匙解码,现在的问题是在我们不知道密钥的情况下应该如何解码呢?

我找到了如下这篇关于如何爆破密匙:http://www.practicalcryptography.com/cryptanalysis/stochastic-searching/cryptanalysis-autokey-cipher/

爆破脚本如下:

from ngram_score import ngram_score
from pycipher import Autokey
import re
from itertools import permutations

qgram = ngram_score('quadgrams.txt')
trigram = ngram_score('trigrams.txt')

ctext = 'MPLRVFFCZEYOUJFJKYBXGZVDGQAURKXZOLKOLVTUFBLRNJESQITWAHXNSIJXPNMPLSHCJBTYHZEALOGVIAAISSPLFHLFSWFEHJNCRWHTINSMAMBVEXPZIZ'

ctext = re.sub(r'[^A-Z]','',ctext.upper())
# keep a list of the N best things we have seen, discard anything else

class nbest(object):
    def __init__(self,N=1000):
        self.store = []
        self.N = N

    def add(self,item):
        self.store.append(item)
        self.store.sort(reverse=True)
        self.store = self.store[:self.N]

    def __getitem__(self,k):
        return self.store[k]

    def __len__(self):
        return len(self.store)

#init
N=100
for KLEN in range(3,20):
    rec = nbest(N)
    for i in permutations('ABCDEFGHIJKLMNOPQRSTUVWXYZ',3):
        key = ''.join(i) + 'A'*(KLEN-len(i))
        pt = Autokey(key).decipher(ctext)
        score = 0
        for j in range(0,len(ctext),KLEN):
            score += trigram.score(pt[j:j+3])
        rec.add((score,''.join(i),pt[:30]))

    next_rec = nbest(N)
    for i in range(0,KLEN-3):
        for k in xrange(N):
            for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
                key = rec[k][1] + c
                fullkey = key + 'A'*(KLEN-len(key))
                pt = Autokey(fullkey).decipher(ctext)
                score = 0
                for j in range(0,len(ctext),KLEN):
                    score += qgram.score(pt[j:j+len(key)])
                next_rec.add((score,key,pt[:30]))
        rec = next_rec
        next_rec = nbest(N)
    bestkey = rec[0][1]
    pt = Autokey(bestkey).decipher(ctext)
    bestscore = qgram.score(pt)
    for i in range(N):
        pt = Autokey(rec[i][1]).decipher(ctext)
        score = qgram.score(pt)
        if score > bestscore:
            bestkey = rec[i][1]
            bestscore = score       
    print bestscore,'autokey, klen',KLEN,':"'+bestkey+'",',Autokey(bestkey).decipher(ctext)

跑出来的结果如下:

我们看到了 flag 的字样,整理可得如下:

-674.914569565 autokey, klen 8 :"FLAGHERE", HELLOBOYSANDGIRLSYOUARESOSMARTTHATYOUCANFINDTHEFLAGTHATIHIDEINTHEKEYBOARDPACKAGEFLAGISJHAWLZKEWXHNCDHSLWBAQJTUQZDXZQPF

我们把字段进行分割看:

HELLO
BOYS
AND
GIRLS
YOU
ARE
SO
SMART
THAT
YOU
CAN
FIND
THE
FLAG
THAT
IH
IDE
IN
THE
KEY
BOARD
PACKAGE
FLAG
IS
JHAWLZKEWXHNCDHSLWBAQJTUQZDXZQPF

最后的 flag 就是 flag{JHAWLZKEWXHNCDHSLWBAQJTUQZDXZQPF}

压缩包分析

ZIP 格式

文件结构

ZIP 文件主要由三部分构成,分别为

压缩源文件数据区 核心目录 目录结束
local file header + file data + data descriptor central directory end of central directory record
  • 压缩源文件数据区中每一个压缩的源文件或目录都是一条记录,其中

    • local file header :文件头用于标识该文件的开始,记录了该压缩文件的信息,这里的文件头标识由固定值 50 4B 03 04 开头,也是 ZIP 的文件头的重要标志
    • file data :文件数据记录了相应压缩文件的数据
    • data descriptor :数据描述符用于标识该文件压缩结束,该结构只有在相应的 local file header 中通用标记字段的第 3 bit 设为 1 时才会出现,紧接在压缩文件源数据后
  • Central directory 核心目录

  • 记录了压缩文件的目录信息,在这个数据区中每一条纪录对应在压缩源文件数据区中的一条数据。

    Offset Bytes Description
    0 4 Central directory file header signature = 0x02014b50 核心目录文件 header 标识 =(0x02014b50)
    4 2 Version made by 压缩所用的 pkware 版本
    6 2 Version needed to extract (minimum) 解压所需 pkware 的最低版本
    8 2 General purpose bit flag 通用位标记伪加密
    10 2 Compression method 压缩方法
    12 2 File last modification time 文件最后修改时间
    14 2 File last modification date 文件最后修改日期
    16 4 CRC-32 CRC-32 校验码
    20 4 Compressed size 压缩后的大小
    24 4 Uncompressed size 未压缩的大小
    28 2 File name length (n) 文件名长度
    30 2 Extra field length (m) 扩展域长度
    32 2 File comment length (k) 文件注释长度
    34 2 Disk number where file starts 文件开始位置的磁盘编号
    36 2 Internal file attributes 内部文件属性
    38 4 External file attributes 外部文件属性
    42 4 relative offset of local header 本地文件头的相对位移
    46 n File name 目录文件名
    46+n m Extra field 扩展域
    46+n+m k File comment 文件注释内容
  • End of central directory record(EOCD) 目录结束标识

  • 目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个 EOCD 记录。

更加详细参见 官方文档

主要攻击

爆破

这里主要介绍两款爆破使用的工具

  • Windows下的神器 ARCHPR

    暴力枚举,跑字典,明文攻击,应有尽有。

  • Linux 下的命令行工具 fcrackzip

    # -b 指定模式为暴破,-c1指定密码类型为纯数字,其它类型可以rtfm,-u这个参数非常重要不然不显示破解出来的密码,-l 5-6可以指定长度
    root@kali:fcrackzip -b -c1 -u test.zip

CRC32

原理

CRC 本身是「冗余校验码」的意思,CRC32 则表示会产生一个 32 bit ( 8 位十六进制数) 的校验值。由于 CRC32 产生校验值时源数据块的每一个 bit (位) 都参与了计算,所以数据块中即使只有一位发生了变化,也会得到不同的 CRC32 值。

CRC32 校验码出现在很多文件中比如 png 文件,同样 zip 中也有 CRC32 校验码。值得注意的是 zip 中的 CRC32 是未加密文件的校验值。

这也就导致了基于 CRC32 的攻击手法。

  • 文件内内容很少 (一般比赛中大多为 4 字节左右)
  • 加密的密码很长

我们不去爆破压缩包的密码,而是直接去爆破源文件的内容 (一般都是可见的字符串),从而获取想要的信息。

比如我们新建一个 flag.txt,其中内容为 123,使用密码 !QAZXSW@#EDCVFR$ 去加密。

而我们去计算文件的 CRC32 值发现和上图中的 CRC32 值吻合。

文件: flag.txt
大小: 3
时间: Tue, 29 Aug 2017 10:38:10 +0800
MD5: 202cb962ac59075b964b07152d234b70
SHA1: 40bd001563085fc35165329ea1ff5c5ecbdbbeef
CRC32: 884863D2

Note

在爆破时我们所枚举的所有可能字符串的 CRC32 值是要与压缩源文件数据区中的 CRC32 值所对应

# -*- coding: utf-8 -*-

import binascii
import base64
import string
import itertools
import struct

alph = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='

crcdict = {}
print "computing all possible CRCs..."
for x in itertools.product(list(alph), repeat=4):
    st = ''.join(x)
    testcrc = binascii.crc32(st)
    crcdict[struct.pack('<i', testcrc)] = st
print "Done!"

f = open('flag.zip')
data = f.read()
f.close()
crc = ''.join(data[14:18])
if crc in crcdict:
    print crcdict[crc]
else:
    print "FAILED!"
例题

题目:Abctf-2016:Zippy

根据每个压缩包内的文件大小可以推断使用 CRC32 攻击手法,获得每个压缩包内的内容后连在一起 Base64 解码后是一个加密的压缩包,爆破获得 flag

明文攻击

原理
  • 一个加密的压缩文件
  • 压缩文件的压缩工具,比如 2345 好压, WinRAR7zzip 版本号等,可以通过文件属性了解。如果是 Linux 平台,用 zipinfo -v 可以查看一个 zip 包的详细信息,包括加密算法等
  • 知道压缩包里某个文件的部分连续内容 (至少 12 字节)

如果你已经知道加密文件的部分内容,比如在某个网站上发现了它的 readme.txt 文件,你就可以开始尝试破解了。

首先,将这个明文文件打包成 zip 包,比如将 readme.txt 打包成 readme.zip

打包完成后,需要确认二者采用的压缩算法相同。一个简单的判断方法是用 WinRAR 打开文件,同一个文件压缩后的体积是否相同。如果相同,基本可以说明你用的压缩算法是正确的。如果不同,就尝试另一种压缩算法。

工具

Note

建议使用 WindowsARCHPR,一是速度较快,二是较稳定(之前出题时遇到过用 PKCrack 爆不出来的情况)。

例题

2015 广州强网杯:爆破?

WP:https://www.cnblogs.com/ECJTUACM-873284962/p/9884416.html

首先我们拿到这样一道题,题目标题为爆破?,很明显这题肯定是要用到一个破解工具,很暴力的说。

第一步、分析压缩包文件

我们下载了这个压缩包以后,我们看到文件名是 *.zip 结尾,我们可以立即想到破解压缩包常用的几种方式,我们将其压缩包解压出来,发现里面有两个文件,分别为 Desktop.zipreadme.txt ,我们看看 readme.txt 里面写了什么?

打开以后竟然是qianwanbuyaogeixuanshoukandao!!! ,出题人不想让选手看到,这出题人还是有点意思。我们再看看那个 Desktop.zip ,我们可以看到里面有个 readme.txt 文件和 answer 文件夹, answer 文件夹下有 key.txt 文件, flag 应该就藏在这里了。

第二步、分析破解方式

这题目拿到手上,我们首先发现解压出来的文件和 Desktop.zip 压缩包中都含有同样一个文件 readme.txt ,而且并没有给出其他相关信息,且文件大小大于 12Byte ,我们再对比压缩包中的 readme.txt 和原压缩包中的 readme.txtCRC32 的值,我们发现两个值相同,这说明解压出的 readme.txt 是加密压缩包里的 readme.txt 的明文,于是我们可以大胆地猜测这极可能是个明文加密。

第三步、尝试明文攻击

既然我们已经知道了它是明文攻击的话,我们将对其压缩包进行破解,由于解压出的 readme.txt 是加密压缩包里的 readme.txt 的明文,将 readme.txt 压缩成 .zip 文件,然后在软件中填入相应的路径即可开始进行明文攻击,这里我们将介绍 WindowsUbuntu 下使用不同的方式进行明文攻击。

方法一、 pkcrack 进行明文攻击

pkcrack 下载链接:https://www.unix-ag.uni-kl.de/~conrad/krypto/pkcrack.html

我们可以直接写个 shell 脚本下载就好了:

#!/bin/bash -ex

wget https://www.unix-ag.uni-kl.de/~conrad/krypto/pkcrack/pkcrack-1.2.2.tar.gz
tar xzf pkcrack-1.2.2.tar.gz
cd pkcrack-1.2.2/src
make

mkdir -p ../../bin
cp extract findkey makekey pkcrack zipdecrypt ../../bin
cd ../../

把文件保存,改为 pkcrack-install.sh ,然后跑到当前目录下,给它加一个执行权限 x

chmod 777 install.sh

或者直接可以:

chmod u+x install.sh

然后运行 ./pkcrack-install.sh

install-pkcrack

然后当前目录下会生成一个 bin 的文件夹,我们直接进入 bin 文件夹下,看到有 pkcrack 文件,直接对文件进行明文破解。

./pkcrack -c "readme.txt" -p readme.txt  -C ~/下载/misc/Desktop.zip -P ~/下载/misc/readme.zip -d ~/decrypt.zip

我们所用到的参数选项如下:

-C:要破解的目标文件(含路径)

-c:破解文件中的明文文件的名字(其路径不包括系统路径,从zip文件一层开始)

-P:压缩后的明文文件

-p:压缩的明文文件中明文文件的名字(也就是readme.txt在readme.zip中的位置)
-d:指定文件名及所在的绝对路径,将解密后的zip文件输出

至于其他选项参看 ./pkcrack --help

解密后的结果如下:

我们可以看到,我们下午 1:10 开始跑的,下午 3:27 才求解出秘钥。

我们得出了最终的 flag 为:flag{7ip_Fi13_S0m3tim3s_s0_3a5y@}

坑点来了

看起来一切都很顺利的样子,同样花了两个多小时,为啥我在博客园上写了我跑了两个小时都没跑出来呢?或者说有朋友遇到了和我一样的问题,我明明和你是一样的,为啥我跑不出结果?

你们可能忽略了一些细节问题,有人曾想过原压缩包是通过什么方式压缩的嘛?还有就是我们生成的 readme.zip 又该以哪种方式去生成呢?我就是因为这个问题卡了整整三个月没做出来,不信的话我们可以看看第二种方法,在 Windows 下用 ARCHPR 进行明文攻击。

方法二、 ARCHPR 进行明文攻击

首先这题我建议大家下 ARCHPR 4.53 版本,我是在这个版本下测试成功的。成功截图如下:

我相信很多朋友在用 ARCHPR 的时候遇到以下这种情况:

我当时内心是崩溃的,为啥会出现这种情况。

在后来的学习中发现,用 7z 压缩的文件得用 7z 来解压缩, 7z 是一种使用多种压缩算法进行数据压缩的档案格式,和传统的 ziprar 相比,它的压缩比率更大,采用的压缩算法不同,自然而然就可能出现不匹配这种情况,所以我们在解压缩原压缩包和对文件进行加密的时候得先分析出题人是用什么方式进行加解密的,所以这题的问题显而易见就出来了,经过验证,我发现出题人是用 7z 进行压缩的。

再尝试

我们已经发现了这个问题,我们去官网下载 7ziphttps://www.7-zip.org/

然后我们对原压缩文件用 7z 进行解压缩,然后将 readme.txt 用 7z 进行压缩即可。然后我们就可以用 ARCHPR 进行明文攻击了。

结果如下:

我们将 Desktop_decrypted.zip 解压出来,查看 answer 目录下的 key.txt 即可。

所以最终的 flag 为:flag{7ip_Fi13_S0m3tim3s_s0_3a5y@}

伪加密

原理

在上文 ZIP 格式中的 核心目录区 中,我们强调了一个叫做通用位标记 (General purpose bit flag)2 字节,不同比特位有着不同的含义。

Bit 0: If set, indicates that the file is encrypted.

(For Method 6 - Imploding)
Bit 1: If the compression method used was type 6,
     Imploding, then this bit, if set, indicates
     an 8K sliding dictionary was used.  If clear,
     then a 4K sliding dictionary was used.
...
Bit 6: Strong encryption.  If this bit is set, you should
     set the version needed to extract value to at least
     50 and you must also set bit 0.  If AES encryption
     is used, the version needed to extract value must
     be at least 51.
...

010Editor 中我们尝试着将这 1 位修改 0 --> 1

再打开文件发现已要求输入密码。

修改伪加密的方法:

  • 16 进制下修改通用位标记
  • binwalk -e 无视伪加密
  • Mac OS 及部分 Linux(如 Kali ) 系统中,可以直接打开伪加密的 ZIP 压缩包
  • 检测伪加密的小工具 ZipCenOp.jar
  • 有时候用 WinRar 的修复功能(此方法有时有奇效,不仅针对伪加密)
例题

SSCTF-2017 :我们的秘密是绿色的

WPhttp://bobao.360.cn/ctf/detail/197.html

我们在得到两个 readme.txt,且一个加密,一个已知,很容易想到明文攻击的手法。

注意在用明文攻击时的操作。

得到密码 Y29mZmVl 后,解压缩文件,得到另一个压缩包。

观察通用位标记位,猜测伪加密,修改后解压得到 flag。

这一题,基本涵盖了比赛中 ZIP 的常见考察手法,爆破,伪加密,明文攻击等,都在本题中出现。

RAR 格式

文件格式

RAR 文件主要由标记块,压缩文件头块,文件头块,结尾块组成。

其每一块大致分为以下几个字段:

名称 大小 描述
HEAD_CRC 2 全部块或块部分的 CRC
HEAD_TYPE 1 块类型
HEAD_FLAGS 2 阻止标志
HEAD_SIZE 2 块大小
ADD_SIZE 4 可选字段 - 添加块大小

Rar 压缩包的文件头为 0x 52 61 72 21 1A 07 00

紧跟着文件头(0x526172211A0700)的是标记块(MARK_HEAD),其后还有文件头(File Header)。

名称 大小 描述
HEAD_CRC 2 CRC of fields from HEAD_TYPE to FILEATTR and file name
HEAD_TYPE 1 Header Type: 0x74
HEAD_FLAGS 2 Bit Flags (Please see ‘Bit Flags for File in Archive’ table for all possibilities)(伪加密)
HEAD_SIZE 2 File header full size including file name and comments
PACK_SIZE 4 Compressed file size
UNP_SIZE 4 Uncompressed file size
HOST_OS 1 Operating system used for archiving (See the ‘Operating System Indicators’ table for the flags used)
FILE_CRC 4 File CRC
FTIME 4 Date and time in standard MS DOS format
UNP_VER 1 RAR version needed to extract file (Version number is encoded as 10 * Major version + minor version.)
METHOD 1 Packing method (Please see ‘Packing Method’ table for all possibilities
NAME_SIZE 2 File name size
ATTR 4 File attributes
HIGH_PACK_SIZ 4 High 4 bytes of 64-bit value of compressed file size. Optional value, presents only if bit 0x100 in HEAD_FLAGS is set.
HIGH_UNP_SIZE 4 High 4 bytes of 64-bit value of uncompressed file size. Optional value, presents only if bit 0x100 in HEAD_FLAGS is set.
FILE_NAME NAME_SIZE bytes File name - string of NAME_SIZE bytes size
SALT 8 present if (HEAD_FLAGS & 0x400) != 0
EXT_TIME variable size present if (HEAD_FLAGS & 0x1000) != 0

每个 RAR 文件的结尾快(Terminator)都是固定的。

Field Name Size (bytes) Possibilities
HEAD_CRC 2 Always 0x3DC4
HEAD_TYPE 1 Header type: 0x7b
HEAD_FLAGS 2 Always 0x4000
HEAD_SIZE 2 Block size = 0x0007

更多详见 https://forensicswiki.xyz/wiki/index.php?title=RAR

主要攻击

爆破

伪加密

RAR 文件的伪加密在文件头中的位标记字段上,用 010 Editor 可以很清楚的看见这一位,修改这一位可以造成伪加密。

其余明文攻击等手法依旧同 ZIP 中介绍的一样。

磁盘内存分析

常用工具

  • EasyRecovery
  • MedAnalyze
  • FTK
  • Elcomsoft Forensic Disk Decryptor
  • Volatility

磁盘

常见的磁盘分区格式有以下几种

  • Windows: FAT12 -> FAT16 -> FAT32 -> NTFS

  • Linux: EXT2 -> EXT3 -> EXT4

  • FAT 主磁盘结构

  • 删除文件:目录表中文件名第一字节 e5

VMDK

VMDK 文件本质上是物理硬盘的虚拟版,也会存在跟物理硬盘的分区和扇区中类似的填充区域,我们可以利用这些填充区域来把我们需要隐藏的数据隐藏到里面去,这样可以避免隐藏的文件增加了 VMDK 文件的大小(如直接附加到文件后端),也可以避免由于 VMDK 文件大小的改变所带来的可能导致的虚拟机错误。而且 VMDK 文件一般比较大,适合用于隐藏大文件。

内存

  • 解析 Windows / Linux / Mac OS X 内存结构
  • 分析进程,内存数据
  • 根据题目提示寻找线索和思路,提取分析指定进程的特定内存数据

题目

2018 网鼎杯第一场 clip

通过 010 editor 可以看到文件的头部包含有 cloop 字样,搜了搜发现这是一个古老的 linux 压缩后的设备,题目中又说这个设备损坏了,所以就想办法找一个正常的。于是搜索如何压缩得到一个 cloop 文件,如下

mkisofs -r test | create_compressed_fs - 65536 > test.cloop

参考 https://github.com/KlausKnopper/cloop,于是压缩一个文件,然后发现源文件文件头存在问题,于是进行修复,从而考虑如何从 cloop 文件中提取文件,即使用

extract_compressed_fs test.cloop now

参考 https://manned.org/create_compressed_fs/f2f838da。

得到一个 ext4 类型的文件,进一步想办法获取这个文件系统的内容

➜  clip losetup -d /dev/loop0
losetup: /dev/loop0: detach failed: Permission denied
➜  clip sudo losetup -d /dev/loop0
➜  clip sudo losetup /dev/loop0 now                                                 
losetup: now: failed to set up loop device: Device or resource busy
➜  clip sudo losetup /dev/loop0 /home/iromise/ctf/2018/0820网鼎杯/misc/clip/now        
losetup: /home/iromise/ctf/2018/0820网鼎杯/misc/clip/now: failed to set up loop device: Device or resource busy
➜  clip losetup -f           
/dev/loop10
➜  clip sudo losetup /dev/loop10 /home/iromise/ctf/2018/0820网鼎杯/misc/clip/now
➜  clip sudo mount /dev/loop10 /mnt/now
➜  clip cd /mnt/now 
➜  now ls        
clip-clip.png  clip-clop.png  clop-clip.png  clop-clop.jpg  flag.png

最后一步就是修复 flag 了。就是少了文件头那几个字符。

pyc 文件

在我们导入 python 脚本时在目录下会生成个一个相应的 pyc 文件,是 pythoncodeobj 的持久化储存形式, 加速下一次的装载。

文件结构

pyc 文件由三大部分组成

  • 最开始 4 个字节是一个 Maigc int, 标识此 pyc 的版本信息
  • 接下来四个字节还是个 int, 是 pyc 产生的时间
  • 序列化的 PyCodeObject, 结构参照 include/code.h, 序列化方法 python/marshal

pyc 完整的文件解析可以参照

关于 co_code

一串二进制流, 代表着指令序列, 具体定义在 include/opcode.h 中, 也可以参照 python opcodes

python3.6 以上参数永远占 1 字节, 如果指令不带参数的话则以0x00代替, 在运行过程中被解释器忽略, 也是 Stegosaurus 技术原理; 而低于 python3.5 的版本中指令不带参数的话却没有0x00填充

例题

Hackover CTF 2016 : img-enc

首先尝试 pycdc 反编译失败

# Source Generated with Decompyle++
# File: imgenc.pyc (Python 2.7)

import sys
import numpy as np
from scipy.misc import imread, imsave

def doit(input_file, output_file, f):
Unsupported opcode: STOP_CODE
    img = imread(input_file, flatten = True)
    img /= 255
    size = img.shape[0]
# WARNING: Decompyle incomplete

注意到是 python2.7, 也就是说指令序列共占 1 字节或 3 字节 (有参数无参数)

使用 pcads 得到

imgenc.pyc (Python 2.7)
...
                67      STOP_CODE               
                68      STOP_CODE               
                69      BINARY_DIVIDE           
                70      JUMP_IF_TRUE_OR_POP     5
                73      LOAD_CONST              3: 0
                76      LOAD_CONST              3: 0
                79      BINARY_DIVIDE       

定位到出错的地方, 观察发现 LOAD_CONST LOAD_CONST BINARY_DIVIDE STORE_FAST opcodes (64 03 00 64 03 00 15 7d 05 00)被破坏了, 根据上下文线索修复后

00000120  64 04 00 6b 00 00 72 ce  00 64 03 00 64 03 00 15  |d..k..r..d..d...|
00000130  7d 05 00 64 03 00 64 03  00 15 7d 05 00 64 03 00  |}..d..d...}..d..|
00000140  64 03 00 15 7d 05 00 64  03 00 64 03 00 15 7d 05  |d...}..d..d...}.|
00000150  00 64 03 00 64 03 00 15  7d 05 00 64 03 00 64 03  |.d..d...}..d..d.|
00000160  00 15 7d 05 00 64 03 00  64 03 00 15 7d 05 00 64  |..}..d..d...}..d|
00000170  03 00 64 03 00 15 7d 05  00 64 03 00 64 03 00 15  |..d...}..d..d...|
00000180  7d 05 00 64 03 00 64 03  00 15 7d 05 00 64 03 00  |}..d..d...}..d..|
00000190  64 03 00 15 7d 05 00 64  03 00 64 03 00 15 7d 05  |d...}..d..d...}.|
000001a0  00 64 03 00 64 03 00 15  7d 05 00 64 03 00 64 03  |.d..d...}..d..d.|
000001b0  00 15 7d 05 00 64 03 00  64 03 00 15 7d 05 00 6e  |..}..d..d...}..n|

接下来根据修复好的 python 源代码得到 flag 即可

延伸:

Tools

pycdc

将 python 字节码转换为可读的 python 源代码, 包含了反汇编 (pycads) 和反编译 (pycdc) 两种工具

Stegosaurus

Stegosaurus 是一款隐写工具,它允许我们在 Python 字节码文件 (pyc 或 pyo) 中嵌入任意 Payload。由于编码密度较低,因此我们嵌入 Payload 的过程既不会改变源代码的运行行为,也不会改变源文件的文件大小。 Payload 代码会被分散嵌入到字节码之中,所以类似 strings 这样的代码工具无法查找到实际的 Payload。 Python 的 dis 模块会返回源文件的字节码,然后我们就可以使用 Stegosaurus 来嵌入 Payload 了。

原理是在 python 的字节码文件中,利用冗余空间,将完整的 payload 代码分散隐藏到这些零零碎碎的空间中。

具体用法可参看 ctf-tools

例题

Bugku QAQ

赛题链接如下:

http://ctf.bugku.com/files/447e4b626f2d2481809b8690613c1613/QAQ
http://ctf.bugku.com/files/5c02892cd05a9dcd1c5a34ef22dd9c5e/cipher.txt

首先拿到这道题,用 010Editor 乍一眼看过去,我们可以看到一些特征信息:

可以判断这是个跟 python 有关的东西,通过查阅相关资料可以判断这是个 python 经编译过后的 pyc 文件。这里可能很多小伙伴们可能不理解了,什么是 pyc 文件呢?为什么会生成 pyc 文件? pyc 文件又是何时生成的呢?下面我将一一解答这些问题。

简单来说, pyc 文件就是 Python 的字节码文件,是个二进制文件。我们都知道 Python 是一种全平台的解释性语言,全平台其实就是 Python 文件在经过解释器解释之后 (或者称为编译) 生成的 pyc 文件可以在多个平台下运行,这样同样也可以隐藏源代码。其实, Python 是完全面向对象的语言, Python 文件在经过解释器解释后生成字节码对象 PyCodeObjectpyc 文件可以理解为是 PyCodeObject 对象的持久化保存方式。而 pyc 文件只有在文件被当成模块导入时才会生成。也就是说, Python 解释器认为,只有 import 进行的模块才需要被重用。 生成 pyc 文件的好处显而易见,当我们多次运行程序时,不需要重新对该模块进行重新的解释。主文件一般只需要加载一次,不会被其他模块导入,所以一般主文件不会生成 pyc 文件。

我们举个例子来说明这个问题:

为了方便起见,我们事先创建一个 test 文件夹作为此次实验的测试:

mkdir test && cd test/

假设我们现在有个 test.py 文件,文件内容如下:

def print_test():
    print('Hello,Kitty!')

print_test()

我们执行以下命令:

python3 test.py

不用说,想必大家都知道打印出的结果是下面这个:

Hello,Kitty!

我们通过下面命令查看下当前文件夹下有哪些文件:

ls -alh

我们可以发现,并没有 pyc 文件生成。

‘我们再去创建一个文件为 import_test.py 文件,文件内容如下:

注: test.pyimport_test.py 应当放在同一文件夹下

import test

test.print_test()

我们执行以下命令:

python3 import_test.py

结果如下:

Hello,Kitty!
Hello,Kitty!

诶,为啥会打印出两句相同的话呢?我们再往下看,我们通过下面命令查看下当前文件夹下有哪些文件:

ls -alh

结果如下:

总用量 20K
drwxr-xr-x 3 python python 4.0K 11月  5 20:38 .
drwxrwxr-x 4 python python 4.0K 11月  5 20:25 ..
-rw-r--r-- 1 python python   31 11月  5 20:38 import_test.py
drwxr-xr-x 2 python python 4.0K 11月  5 20:38 __pycache__
-rw-r--r-- 1 python python   58 11月  5 20:28 test.py

诶,多了个 __pycache__ 文件夹,我们进入文件夹下看看有什么?

cd __pycache__ && ls

我们可以看到生成了一个 test.cpython-36.pyc 。为什么是这样子呢?

我们可以看到,我们在执行 python3 import_test.py 命令的时候,首先开始执行的是 import test ,即导入 test 模块,而一个模块被导入时, PVM(Python Virtual Machine) 会在后台从一系列路径中搜索该模块,其搜索过程如下:

  • 在当前目录下搜索该模块
  • 在环境变量 PYTHONPATH 中指定的路径列表中依次搜索
  • python 安装路径中搜索

事实上, PVM 通过变量 sys.path 中包含的路径来搜索,这个变量里面包含的路径列表就是上面提到的这些路径信息。

模块的搜索路径都放在了 sys.path 列表中,如果缺省的 sys.path 中没有含有自己的模块或包的路径,可以动态的加入 (sys.path.apend) 即可。

事实上, Python 中所有加载到内存的模块都放在 sys.modules 。当 import 一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用 import 的模块的 Local 名字空间中。如果没有加载则从 sys.path 目录中按照模块名称查找模块文件,模块文件可以是 pypycpyd ,找到后将模块载入内存,并加入到 sys.modules 中,并将名称导入到当前的 Local 名字空间。

可以看出来,一个模块不会重复载入。多个不同的模块都可以用 import 引入同一个模块到自己的 Local 名字空间,其实背后的 PyModuleObject 对象只有一个。

在这里,我还要说明一个问题,import 只能导入模块,不能导入模块中的对象 (类、函数、变量等)。例如像上面这个例子,我在 test.py 里面定义了一个函数 print_test() ,我在另外一个模块文件 import_test.py不能直接通过 import test.print_testprint_test 导入到本模块文件中,只能用 import test 进行导入。如果我想只导入特定的类、函数、变量,用 from test import print_test 即可。

既然说到了 import 导入机制,再提一提嵌套导入和 Package 导入。

import 嵌套导入

嵌套,不难理解,就是一个套着一个。小时候我们都玩过俄罗斯套娃吧,俄罗斯套娃就是一个大娃娃里面套着一个小娃娃,小娃娃里面还有更小的娃娃,而这个嵌套导入也是同一个意思。假如我们现在有一个模块,我们想要导入模块 A ,而模块 A 中有含有其他模块需要导入,比如模块 B ,模块 B 中又含有模块 C ,一直这样延续下去,这种方式我们称之为 import 嵌套导入。

对这种嵌套比较容易理解,我们需要注意的一点就是各个模块的 Local 名字空间是独立的,所以上面的例子,本模块 import A 完了后,本模块只能访问模块 A ,不能访问 B 及其它模块。虽然模块 B 已经加载到内存了,如果要访问,还必须明确在本模块中导入 import B

那如果我们有以下嵌套这种情况,我们该怎么处理呢?

比如我们现在有个模块 A

# A.py
from B import D
class C:
    pass

还有个模块 B

# B.py
from A import C
class D:
    pass

我们简单分析一下程序,如果程序运行,应该会去从模块 B 中调用对象 D。

我们尝试执行一下 python A.py

ImportError 的错误,似乎是没有加载到对象 D ,而我们将 from B import D 改成 import B ,我们似乎就能执行成功了。

这是怎么回事呢?这其实是跟 Python 内部 import 的机制是有关的,具体到 from B import DPython 内部会分成以下几个步骤:

  • sys.modules 中查找符号 B
  • 如果符号 B 存在,则获得符号 B 对应的 module 对象 <module B> 。从 <module B>__dict__ 中获得符号 D 对应的对象,如果 D 不存在,则抛出异常
  • 如果符号 B 不存在,则创建一个新的 module 对象 <module B> ,注意,此时 module 对象的 __dict__ 为空。执行 B.py 中的表达式,填充 <module B>__dict__ 。从 <module B>__dict__ 中获得 D 对应的对象。如果 D 不存在,则抛出异常。

所以,这个例子的执行顺序如下:

1、执行 A.py 中的 from B import D

注:由于是执行的 python A.py ,所以在 sys.modules 中并没有 <module B> 存在,首先为 B.py 创建一个 module 对象 ( <module B> ),注意,这时创建的这个 module 对象是空的,里边啥也没有,在 Python 内部创建了这个 module 对象之后,就会解析执行 B.py ,其目的是填充 <module B> 这个 dict

2、执行 B.py 中的 from A import C

注:在执行 B.py 的过程中,会碰到这一句,首先检查 sys.modules 这个 module 缓存中是否已经存在 <module A> 了,由于这时缓存还没有缓存 <module A> ,所以类似的, Python 内部会为 A.py 创建一个 module 对象 ( <module A> ),然后,同样地,执行 A.py 中的语句。

3、再次执行 A.py 中的 from B import D

注:这时,由于在第 1 步时,创建的 <module B> 对象已经缓存在了 sys.modules 中,所以直接就得到了 <module B> ,但是,注意,从整个过程来看,我们知道,这时 <module B> 还是一个空的对象,里面啥也没有,所以从这个 module 中获得符号 D 的操作就会抛出异常。如果这里只是 import B ,由于 B 这个符号在 sys.modules 中已经存在,所以是不会抛出异常的。

我们可以从下图很清楚的看到 import 嵌套导入的过程:

Package 导入

(Package) 可以看成模块的集合,只要一个文件夹下面有个 __init__.py 文件,那么这个文件夹就可以看做是一个包。包下面的文件夹还可以成为包 (子包)。更进一步的讲,多个较小的包可以聚合成一个较大的包。通过包这种结构,我们可以很方便的进行类的管理和维护,也方便了用户的使用。比如 SQLAlchemy 等都是以包的形式发布给用户的。

包和模块其实是很类似的东西,如果查看包的类型: import SQLAlchemy type(SQLAlchemy) ,可以看到其实也是 <type 'module'>import 包的时候查找的路径也是 sys.path

包导入的过程和模块的基本一致,只是导入包的时候会执行此包目录下的 __init__.py ,而不是模块里面的语句了。另外,如果只是单纯的导入包,而包的 __init__.py 中又没有明确的其他初始化操作,那么此包下面的模块是不会自动导入的。

假设我们有如下文件结构:

.
└── PA
    ├── __init__.py
    ├── PB1
    │   ├── __init__.py
    │   └── pb1_m.py
    ├── PB2
    │   ├── __init__.py
    │   └── pb2_m.py
    └── wave.py

wave.pypb1_m.pypb2_m.py 文件中我们均定义了如下函数:

def getName():
    pass

__init__.py 文件内容均为空。

我们新建一个 test.py ,内容如下:

import sys
import PA.wave #1
import PA.PB1 #2
import PA.PB1.pb1_m as m1 #3
import PA.PB2.pb2_m #4
PA.wave.getName() #5
m1.getName() #6
PA.PB2.pb2_m.getName() #7

我们运行以后,可以看出是成功执行成功了,我们再看看目录结构:

.
├── PA
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── PB1
│   │   ├── __init__.py
│   │   ├── __init__.pyc
│   │   ├── pb1_m.py
│   │   └── pb1_m.pyc
│   ├── PB2
│   │   ├── __init__.py
│   │   ├── __init__.pyc
│   │   ├── pb2_m.py
│   │   └── pb2_m.pyc
│   ├── wave.py
│   └── wave.pyc
└── test.py

我们来分析一下这个过程:

  • 当执行#1 后, sys.modules 会同时存在 PAPA.wave 两个模块,此时可以调用 PA.wave 的任何类或函数了。但不能调用 PA.PB1(2) 下的任何模块。当前 Local 中有了 PA 名字。
  • 当执行 #2 后,只是将 PA.PB1 载入内存, sys.modules 中会有 PAPA.wavePA.PB1 三个模块,但是 PA.PB1 下的任何模块都没有自动载入内存,此时如果直接执行 PA.PB1.pb1_m.getName() 则会出错,因为 PA.PB1 中并没有 pb1_m 。当前 Local 中还是只有 PA 名字,并没有 PA.PB1 名字。
  • 当执行 #3 后,会将 PA.PB1 下的 pb1_m 载入内存, sys.modules 中会有 PAPA.wavePA.PB1PA.PB1.pb1_m 四个模块,此时可以执行 PA.PB1.pb1_m.getName() 了。由于使用了 as ,当前 Local 中除了 PA 名字,另外添加了 m1 作为 PA.PB1.pb1_m 的别名。
  • 当执行 #4 后,会将 PA.PB2PA.PB2.pb2_m 载入内存, sys.modules 中会有 PAPA.wavePA.PB1PA.PB1.pb1_mPA.PB2PA.PB2.pb2_m 六个模块。当前 Local 中还是只有 PAm1
  • 下面的 #5#6#7 都是可以正确运行的。

注:需要注意的问题是如果 PA.PB2.pb2_m 想导入 PA.PB1.pb1_mPA.wave 是可以直接成功的。最好是采用明确的导入路径,对于 ../.. 相对导入路径还是不推荐使用。

既然我们已经知道 pyc 文件的产生,再回到那道赛题,我们尝试将 pyc 文件反编译回 python 源码。我们使用在线的开源工具进行尝试:

部分代码没有反编译成功???我们可以尝试分析一下,大概意思就是读取 cipher.txt 那个文件,将那个文件内容是通过 base64 编码的,我们的目的是将文件内容解码,然后又已知 key ,通过 encryt 函数进行加密的,我们可以尝试将代码补全:

def encryt(key, plain):
    cipher = ''
    for i in range(len(plain)):
        cipher += chr(ord(key[i % len(key)]) ^ ord(plain[i]))

    return cipher


def getPlainText():
    plain = ''
    with open('cipher.txt') as (f):
        while True:
            line = f.readline()
            if line:
                plain += line
            else:
                break

    return plain.decode('base_64')


def main():
    key = 'LordCasser'
    plain = getPlainText()
    cipher = encryt(key, plain)
    with open('xxx.txt', 'w') as (f):
        f.write(cipher)


if __name__ == '__main__':
    main()

结果如下:

YOU ARE FOOLED
THIS IS NOT THAT YOU WANT
GO ON DUDE
CATCH THAT STEGOSAURUS

提示告诉我们用 STEGOSAURUS 工具进行隐写的,我们直接将隐藏的 payload 分离出来即可。

python3 stegosaurus.py -x QAQ.pyc

我们得到了最终的 flag 为:flag{fin4lly_z3r0_d34d}

既然都说到这个份子上了,我们就来分析一下我们是如何通过 Stegosaurus 来嵌入 Payload

我们仍然以上面这个代码为例子,我们设置脚本名称为 encode.py

第一步,我们使用 Stegosaurus 来查看在不改变源文件 (Carrier) 大小的情况下,我们的 Payload 能携带多少字节的数据:

python3 -m stegosaurus encode.py -r

现在,我们可以安全地嵌入最多 24 个字节的 Payload 了。如果不想覆盖源文件的话,我们可以使用 -s 参数来单独生成一个嵌入了 Payloadpy 文件:

python3 -m stegosaurus encode.py -s --payload "flag{fin4lly_z3r0_d34d}"

现在我们可以用 ls 命令查看磁盘目录,嵌入了 Payload 的文件 ( carrier 文件) 和原始的字节码文件两者大小是完全相同的:

注:如果没有使用 -s 参数,那么原始的字节码文件将会被覆盖。

我们可以通过向 Stegosaurus 传递 -x 参数来提取出 Payload

python3 -m stegosaurus __pycache__/encode.cpython-36-stegosaurus.pyc -x

我们构造的 Payload 不一定要是一个 ASCII 字符串, shellcode 也是可以的:

我们重新编写一个 example.py 模块,代码如下:

import sys
import os
import math
def add(a,b):
    return int(a)+int(b)
def sum1(result):
    return int(result)*3

def sum2(result):
    return int(result)/3

def sum3(result):
    return int(result)-3

def main():
    a = 1
    b = 2
    result = add(a,b)
    print(sum1(result))
    print(sum2(result))
    print(sum3(result))

if __name__ == "__main__":
    main()

我们让它携带 Payloadflag_is_here

我们可以查看嵌入 Payload 之前和之后的 Python 代码运行情况:

通过 strings 查看 Stegosaurus 嵌入了 Payload 之后的文件输出情况 ( payload 并没有显示出来):

接下来使用 Pythondis 模块来查看 Stegosaurus 嵌入 Payload 之前和之后的文件字节码变化情况:

嵌入 payload 之前:

#( 11/29/18@ 5:14下午 )( python@Sakura ):~/桌面
   python3 -m dis example.py 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  2           8 LOAD_CONST               0 (0)
             10 LOAD_CONST               1 (None)
             12 IMPORT_NAME              1 (os)
             14 STORE_NAME               1 (os)

  3          16 LOAD_CONST               0 (0)
             18 LOAD_CONST               1 (None)
             20 IMPORT_NAME              2 (math)
             22 STORE_NAME               2 (math)

  4          24 LOAD_CONST               2 (<code object add at 0x7f90479778a0, file "example.py", line 4>)
             26 LOAD_CONST               3 ('add')
             28 MAKE_FUNCTION            0
             30 STORE_NAME               3 (add)

  6          32 LOAD_CONST               4 (<code object sum1 at 0x7f9047977810, file "example.py", line 6>)
             34 LOAD_CONST               5 ('sum1')
             36 MAKE_FUNCTION            0
             38 STORE_NAME               4 (sum1)

  9          40 LOAD_CONST               6 (<code object sum2 at 0x7f9047977ae0, file "example.py", line 9>)
             42 LOAD_CONST               7 ('sum2')
             44 MAKE_FUNCTION            0
             46 STORE_NAME               5 (sum2)

 12          48 LOAD_CONST               8 (<code object sum3 at 0x7f9047977f60, file "example.py", line 12>)
             50 LOAD_CONST               9 ('sum3')
             52 MAKE_FUNCTION            0
             54 STORE_NAME               6 (sum3)

 15          56 LOAD_CONST              10 (<code object main at 0x7f904798c300, file "example.py", line 15>)
             58 LOAD_CONST              11 ('main')
             60 MAKE_FUNCTION            0
             62 STORE_NAME               7 (main)

 23          64 LOAD_NAME                8 (__name__)
             66 LOAD_CONST              12 ('__main__')
             68 COMPARE_OP               2 (==)
             70 POP_JUMP_IF_FALSE       78

 24          72 LOAD_NAME                7 (main)
             74 CALL_FUNCTION            0
             76 POP_TOP
        >>   78 LOAD_CONST               1 (None)
             80 RETURN_VALUE

嵌入 payload 之后:

#( 11/29/18@ 5:31下午 )( python@Sakura ):~/桌面
   python3 -m dis example.py                                 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  2           8 LOAD_CONST               0 (0)
             10 LOAD_CONST               1 (None)
             12 IMPORT_NAME              1 (os)
             14 STORE_NAME               1 (os)

  3          16 LOAD_CONST               0 (0)
             18 LOAD_CONST               1 (None)
             20 IMPORT_NAME              2 (math)
             22 STORE_NAME               2 (math)

  4          24 LOAD_CONST               2 (<code object add at 0x7f146e7038a0, file "example.py", line 4>)
             26 LOAD_CONST               3 ('add')
             28 MAKE_FUNCTION            0
             30 STORE_NAME               3 (add)

  6          32 LOAD_CONST               4 (<code object sum1 at 0x7f146e703810, file "example.py", line 6>)
             34 LOAD_CONST               5 ('sum1')
             36 MAKE_FUNCTION            0
             38 STORE_NAME               4 (sum1)

  9          40 LOAD_CONST               6 (<code object sum2 at 0x7f146e703ae0, file "example.py", line 9>)
             42 LOAD_CONST               7 ('sum2')
             44 MAKE_FUNCTION            0
             46 STORE_NAME               5 (sum2)

 12          48 LOAD_CONST               8 (<code object sum3 at 0x7f146e703f60, file "example.py", line 12>)
             50 LOAD_CONST               9 ('sum3')
             52 MAKE_FUNCTION            0
             54 STORE_NAME               6 (sum3)

 15          56 LOAD_CONST              10 (<code object main at 0x7f146e718300, file "example.py", line 15>)
             58 LOAD_CONST              11 ('main')
             60 MAKE_FUNCTION            0
             62 STORE_NAME               7 (main)

 23          64 LOAD_NAME                8 (__name__)
             66 LOAD_CONST              12 ('__main__')
             68 COMPARE_OP               2 (==)
             70 POP_JUMP_IF_FALSE       78

 24          72 LOAD_NAME                7 (main)
             74 CALL_FUNCTION            0
             76 POP_TOP
        >>   78 LOAD_CONST               1 (None)
             80 RETURN_VALUE

注: Payload 的发送和接受方法完全取决于用户个人喜好, Stegosaurus 只提供了一种向 Python 字节码文件嵌入或提取 Payload 的方法。但是为了保证嵌入之后的代码文件大小不会发生变化,因此 Stegosaurus 所支持嵌入的 Payload 字节长度十分有限。因此 ,如果你需要嵌入一个很大的 Payload ,那么你可能要将其分散存储于多个字节码文件中了。

为了在不改变源文件大小的情况下向其嵌入 Payload ,我们需要识别出字节码中的无效空间 ( Dead Zone )。这里所谓的无效空间指的是那些即使被修改也不会改变原 Python 脚本正常行为的那些字节数据。

需要注意的是,我们可以轻而易举地找出 Python3.6 代码中的无效空间。 Python 的引用解释器 CPython 有两种类型的操作码:即无参数的和有参数的。在版本号低于 3.5Python 版本中,根据操作码是否带参,字节码中的操作指令将需要占用 1 个字节或 3 个字节。在 Python3.6 中就不一样了, Python3.6 中所有的指令都占用 2 个字节,并会将无参数指令的第二个字节设置为 0 ,这个字节在其运行过程中将会被解释器忽略。这也就意味着,对于字节码中每一个不带参数的操作指令, Stegosaurus 都可以安全地嵌入长度为 1 个字节的 Payload 代码。

我们可以通过 Stegosaurus-vv 选项来查看 Payload 是如何嵌入到这些无效空间之中的:

#( 11/29/18@10:35下午 )( python@Sakura ):~/桌面
   python3 -m stegosaurus example.py -s -p "ABCDE" -vv          
2018-11-29 22:36:26,795 - stegosaurus - DEBUG - Validated args
2018-11-29 22:36:26,797 - stegosaurus - INFO - Compiled example.py as __pycache__/example.cpython-36.pyc for use as carrier
2018-11-29 22:36:26,797 - stegosaurus - DEBUG - Read header and bytecode from carrier
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_SUBTRACT (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - INFO - Found 14 bytes available for payload
Payload embedded in carrier
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (65) ----A
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (66) ----B
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (67) ----C
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (68) ----D
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_SUBTRACT (69) ----E
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - Creating new carrier file name for side-by-side install
2018-11-29 22:36:26,799 - stegosaurus - INFO - Wrote carrier file as __pycache__/example.cpython-36-stegosaurus.pyc

Challenges: WHCTF-2017:Py-Py-Py

MISC Tools

图片隐写

  • Stegsolve

  • Stegdetect amd64 deb

    Stegdetect 的主要选项如下:

    q – 仅显示可能包含隐藏内容的图像

    n – 启用检查 JPEG 文件头功能,以降低误报率。如果启用,所有带有批注区域的文件将被视为没有被嵌入信息。如果 JPEG 文件的 JFIF 标识符中的版本号不是 1.1,则禁用 OutGuess 检测。

    s – 修改检测算法的敏感度,该值的默认值为 1。检测结果的匹配度与检测算法的敏感度成正比,算法敏感度的值越大,检测出的可疑文件包含敏感信息的可能性越大。

    d – 打印带行号的调试信息。

    t – 设置要检测哪些隐写工具(默认检测 jopi),可设置的选项如下:

    j – 检测图像中的信息是否是用 jsteg 嵌入的。

    o – 检测图像中的信息是否是用 outguess 嵌入的。

    p – 检测图像中的信息是否是用 jphide 嵌入的。

    i – 检测图像中的信息是否是用 invisible secrets 嵌入的。

  • Steghide 0.5.1 win32

  • Outguess amd64 deb

  • PNGCheck 2.3.0 win32

  • JPHS win32

  • OurSecret

压缩包

无线密码

编辑器

NTFS 文件流

音频隐写

取证

条形码、二维码

GIF

pyc

Stegosaurus 是一款隐写工具,它允许我们在 Python 字节码文件( pyc 或 pyo )中嵌入任意 Payload 。由于编码密度较低,因此我们嵌入 Payload 的过程既不会改变源代码的运行行为,也不会改变源文件的文件大小。 Payload 代码会被分散嵌入到字节码之中,所以类似 strings 这样的代码工具无法查找到实际的 Payload 。 Python 的 dis 模块会返回源文件的字节码,然后我们就可以使用 Stegosaurus 来嵌入 Payload 了。

Tips: Stegosaurus 仅支持 Python3.6 及其以下版本

Stegosaurus 的基本用法如下:

$ python3 -m stegosaurus -h
usage: stegosaurus.py [-h] [-p PAYLOAD] [-r] [-s] [-v] [-x] carrier

positional arguments:
  carrier               Carrier py, pyc or pyo file

optional arguments:
  -h, --help            show this help message and exit
  -p PAYLOAD, --payload PAYLOAD
                        Embed payload in carrier file
  -r, --report          Report max available payload size carrier supports
  -s, --side-by-side    Do not overwrite carrier file, install side by side
                        instead.
  -v, --verbose         Increase verbosity once per use
  -x, --extract         Extract payload from carrier file

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录