提取QQ原创gif表情

教程

本文告诉你如何提取QQ加密过的gif

基本原理

本文基于 提取QQ原创动画表情 , 该文章介绍了原创动画表情的位置以及如何对文件进行基本的解谜。 然而文章中存在一个尚未解决的问题 “这个我也在纠结,miku的表情100k以上的均无法照此实现提取,但是100k以下的都可以。等有了新进展我将会在这里跟进。”, 对于这个问题,评论区一位叫做海马的朋友提出了一个新的解决方案

“以四位16进制为一组,遇到偶数+1,遇到奇数-1。比如4748是偶数,+1变成4749,322F是奇数,-1变成322E。整个文件头都要一一改变,直到不加密的地方为止。”

这个就是本文的基础解决思路。显然,这是一个可以编程解决而不用手动修改的问题。首先,让我们来查看一下gif的头文件格式: gif格式

1
2
3
4
5
6
7
8
9
10
11
12

struct _gif_header
{
uint32 SignatureHi; // "GIF8"
uint16 SignatureLo; // GIF version: "7a" or "9a"
uint16 Width; // logical screen width in pixels
uint16 Height; // logical screen height in pixels
uint8 Flags; // Global Color Table specification
uint8 Background; // background color
uint8 Range; // default pixel aspect ratio
};

实际上这里关键的只有前4个属性,后面的uint8的flag,background和range相比于整个图片来说并没有那么重要(

因此,这里的实际上的操作是读取gif文件中的前 32+16+16+16个bytes,即10*8个bytes。 对于第1,3,5,7,9个byte (从0开始数),假如它是奇数则-1,是偶数则+1。当把前4个属性写入新文件后,剩下的bytes可以原封不动地输出。

使用方法以及代码

这是一份python代码,使用说明附加在注释里面了。

你需要准备一个待解密的gif文件或者文件夹,带参数地运行程序后,程序会自动地在当前目录或者你指定的目录生成解密后的gif文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

########################
#
# 使用说明
# decodeGif input [outputFolder] [-p prefix]
# input: 输入文件路径,可以是单独的文件或者是文件夹。假如是文件夹则会处理该文件下的所有文件
# outputFolder: 可选项,指定输出的路径,输出在指定的文件下,默认为当前文件夹
# -p prefix: -p是必须的,后面的prefix指定生成gif的前缀,默认为空。生成的gif默认从1开始编号
##### Examples ########
#
# ./decodeGif.py foo ./gifs/
# 会读取foo文件,然后生成1.gif在./gifs/文件夹下
#
# ./decodeGif.py ./allGifs/ ./result/ -p test
# 会读取./allGifs/下的所有文件,生成test1.gif, test2.gif ...... testn.gif在 ./result/ 下
#
#######################

import sys
import os
from struct import *

INPUT_PATH=""
OUTPUT_PATH="."
OUTPUT_PREFIX=""

INPUTS=[]
OUTPUTS=[]


def handleArgv():
global INPUT_PATH
global OUTPUT_PREFIX
global OUTPUT_PATH
INPUT_PATH=sys.argv[1]
if os.path.exists(INPUT_PATH):
INPUT_PATH=sys.argv[1]
else:
raise Exception("cannot find input file!")

if len(sys.argv)>=2 and os.path.isdir(sys.argv[2]):
OUTPUT_PATH=sys.argv[2]
if not OUTPUT_PATH.endswith("/"):
OUTPUT_PATH+="/"
else:
raise Exception("cannot find output folder!")

if "-p" in sys.argv:
idx=sys.argv.index("-p")
if idx+1< len(sys.argv):
OUTPUT_PREFIX=sys.argv[idx+1]

def getInput():
global INPUT_PATH
global INPUTS
if os.path.isfile(INPUT_PATH):
INPUTS=[INPUT_PATH]
elif os.path.isdir(INPUT_PATH):
if not INPUT_PATH.endswith("/"):
INPUT_PATH+="/"
INPUTS=[INPUT_PATH+s for s in os.listdir(INPUT_PATH)]
else:
raise Exception("Input is not a file or directory")

def getOutput():
global OUTPUT_PATH
for i in range(1,1+len(INPUTS)):
OUTPUTS.append(OUTPUT_PATH+OUTPUT_PREFIX+str(i)+".gif")

def decode():
assert len(INPUTS)==len(OUTPUTS)
for input,output in zip(INPUTS,OUTPUTS):
with open(input,"rb") as fin:
with open(output,"wb") as fout:
header=[]
for i in range(0,5):
header.append(fin.read(2))
unpackHeader=[unpack(">H",i)[0]for i in header]
for i in range(0,5):
if(unpackHeader[i]&1==1):
unpackHeader[i]-=1
else:
unpackHeader[i]+=1
for i in unpackHeader:
fout.write(pack(">H",i))
fout.write(fin.read())

def main():
handleArgv()
getInput()
getOutput()
decode()

main()