T7 css偏移挑战

一:抓包分析

直接请求下一页看请求信息

image-20260506200003731

结合题目,能知道是css偏移,无其他的加密

二:偏移分析

如何偏移

1
2
3
4
{
"display_html": "<style>:root { --offset-0-1952: calc((2 * 3 * 5px) * 1); --offset-1-7812: calc((2 * 3 * 5px) - 0px + 0px); --offset-2-8634: calc(((2 * 3 * 5px) + 1px - 1px)); --offset-3-8763: calc((2 * 3 * 5px) + 0px); }</style><span title=\"position data\" data-calc=\"\u4e8c * \u4e09 * \u4e94\" style=\"position:relative;left:var(--offset-0-1952)\">4</span><span title=\"position data\" data-calc=\"0b10 * 0b11 * 0b101\" style=\"position:relative;left:var(--offset-1-7812)\">5</span><span title=\"position data\" data-calc=\"b * c * e\" style=\"position:relative;right:var(--offset-2-8634)\">2</span><span data-offset=\"43\" data-calc=\"0x2 * 0x3 * 0x5\" style=\"position:relative;right:var(--offset-3-8763)\">2</span>",
"has_offset": true
},

以如上图的为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root { 
--offset-0-1952: calc((2 * 3 * 5px) * 1);
--offset-1-7812: calc((2 * 3 * 5px) - 0px + 0px);
--offset-2-8634: calc(((2 * 3 * 5px) + 1px - 1px));
--offset-3-8763: calc((2 * 3 * 5px) + 0px);
}

<span title=\"position data\" data-calc=\"\u4e8c * \u4e09 * \u4e94\" style=\"position:relative;
left:var(--offset-0-1952)\">4</span>
<span title=\"position data\" data-calc=\"0b10 * 0b11 * 0b101\" style=\"position:relative;
left:var(--offset-1-7812)\">5</span>
<span title=\"position data\" data-calc=\"b * c * e\" style=\"position:relative;
right:var(--offset-2-8634)\">2</span>
<span data-offset=\"43\" data-calc=\"0x2 * 0x3 * 0x5\" style=\"position:relative;
right:var(--offset-3-8763)\">2</span>

根据返回值能看出,root下为计算变量数值、span里为数字和偏移样式

1、步骤 1:计算所有 CSS 变量的数值(像素)
变量名 表达式 计算结果
--offset-0-1952 (2 * 3 * 5px) * 1 30px
--offset-1-7812 (2 * 3 * 5px) - 0px + 0px 30px
--offset-2-8634 ((2 * 3 * 5px) + 1px - 1px) 30px
--offset-3-8763 (2 * 3 * 5px) + 0px 30px
2、步骤 2:提取每个数字及其偏移样式

按 HTML 中的出现顺序(DOM 顺序),依次处理:

  1. 第一个 <span>
    数字:4
    样式:left:var(--offset-0-1952)left:30px
    偏移含义:left 使元素向右移动 30px。
  2. 第二个 <span>
    数字:5
    样式:left:var(--offset-1-7812)left:30px
    同样向右移动 30px。
  3. 第三个 <span>
    数字:2
    样式:right:var(--offset-2-8634)right:30px
    偏移含义:right 使元素向左移动 30px(相当于负的 X 方向偏移)。
  4. 第四个 <span>
    数字:2
    样式:right:var(--offset-3-8763)right:30px
    同样向左移动 30px。
3、步骤 3:设定基准字符宽度

在之前的例子中,所有 calc 表达式的基础都是 3*5=15,且实际观察验证了字符宽度为 15px
因此我们假设每个数字字符占据 15px 的宽度,并按 DOM 顺序给每个字符分配一个初始的 X 坐标(从左到右,间距 15px)。

索引 DOM 顺序 数字 初始 X 坐标 = 索引 × 15px
0 第 1 个 4 0
1 第 2 个 5 15
2 第 3 个 2 30
3 第 4 个 2 45
4、步骤 4:应用偏移,计算最终 X 坐标
  • 字符 4:初始 0 + 向右 30px → 30
  • 字符 5:初始 15 + 向右 30px → 45
  • 字符 2(第三个):初始 30 + 向左 30px(即 -30) → 0
  • 字符 2(第四个):初始 45 + 向左 30px(-30) → 15
5、按X坐标大小拼接

排序后顺序:
X=0 → 2
X=15 → 2
X=30 → 4
X=45 → 5

拼接得到 2245

image-20260506203218438

与第二个一致。

三:AI分析

当然你懒得分析,直接丢给AI,我将APi接口返回值+正确的值都丢给他,让他给我分析

image-20260506203416787

当然代码直接也帮我们生成了

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import re
import ast
from bs4 import BeautifulSoup

def compute_calc_value(expr: str, variables: dict = None) -> int:
"""
计算 calc() 内部的表达式,例如 (3 * 3 * 5px) + 0px
支持 min/max,支持 0x / 0b 数字,自动去除 px 单位。
variables 用于替换 var(--name)
"""
if variables is None:
variables = {}
# 去掉 calc( 和末尾的 )
expr = expr.strip()
if expr.startswith('calc('):
expr = expr[5:-1].strip()
# 递归替换 var(--name)
var_pattern = r'var\(--([\w-]+)\)'
def repl_var(m):
name = m.group(1)
if name in variables:
return str(variables[name])
else:
# 未解析到时保留原样,后续处理(实际上应在调用前解析好)
return m.group(0)
while re.search(var_pattern, expr):
expr = re.sub(var_pattern, repl_var, expr)
# 去掉所有 px 单位
expr = re.sub(r'px', '', expr)
# 处理 min(...) 和 max(...),取第一个参数(因为参数相同)
expr = re.sub(r'min\(([^,]+),[^)]+\)', r'\1', expr)
expr = re.sub(r'max\(([^,]+),[^)]+\)', r'\1', expr)
# 安全求值(数据可信)
try:
# 使用 ast.literal_eval 不安全,因为表达式含运算符,改用 eval
# 限制全局/局部命名空间为空,避免危险调用
result = eval(expr, {'__builtins__': {}}, {})
return int(result)
except Exception:
# 降级:手动提取数字乘积(简单场景)
nums = re.findall(r'(\d+)', expr)
if nums:
product = 1
for n in nums:
product *= int(n)
return product
return 0

def parse_css_variables(style_text: str) -> dict:
"""
从 <style>:root { ... }</style> 中解析所有 CSS 变量名及其数值
"""
# 提取 :root 块内的内容
root_match = re.search(r':root\s*{([^}]*)}', style_text, re.DOTALL)
if not root_match:
return {}
content = root_match.group(1)
# 匹配所有变量定义: --name: value;
pattern = r'--([\w-]+)\s*:\s*([^;]+);'
defs = re.findall(pattern, content)
var_values = {}
# 先解析不依赖 var() 的定义(普通正变量)
for name, raw_val in defs:
if 'var(' not in raw_val:
var_values[name] = compute_calc_value(raw_val, var_values)
# 再解析依赖 var() 的定义(如负变量)
for name, raw_val in defs:
if name not in var_values:
var_values[name] = compute_calc_value(raw_val, var_values)
return var_values

def extract_digits_and_offsets(html: str, var_values: dict, char_width: int = 15):
"""
遍历 HTML,提取所有数字字符及其最终 X 坐标。
返回列表 [(digit, x_final), ...]
"""
soup = BeautifulSoup(html, 'html.parser')
# 移除 style 标签避免干扰遍历
for style in soup.find_all('style'):
style.decompose()
digits_info = [] # 元素: (digit, offset_px, initial_index)
index = 0

def process_text(text):
nonlocal index
# 提取所有数字字符(仅保留 0-9)
digits = re.findall(r'\d', text)
for d in digits:
# 纯文本无偏移,初始坐标 = index * char_width,偏移 0
digits_info.append((d, 0, index))
index += 1

def process_tag(tag):
nonlocal index
if tag.name == 'span':
# 提取数字
text = tag.get_text(strip=True)
if not text:
return
# 只取第一个数字(span 中一般仅一个数字)
digit = re.search(r'\d', text)
if not digit:
return
digit = digit.group()
# 解析偏移样式
style_attr = tag.get('style', '')
offset = 0
# left: var(--xxx)
left_match = re.search(r'left:\s*var\(--([\w-]+)\)', style_attr)
if left_match:
var_name = left_match.group(1)
offset = var_values.get(var_name, 0)
else:
right_match = re.search(r'right:\s*var\(--([\w-]+)\)', style_attr)
if right_match:
var_name = right_match.group(1)
offset = -var_values.get(var_name, 0)
digits_info.append((digit, offset, index))
index += 1
else:
# 其他标签,递归处理子节点
for child in tag.children:
if child.name is None: # NavigableString
process_text(str(child))
else:
process_tag(child)

# 从 body 开始遍历,如果没有 body 则遍历顶层
body = soup.body
if body:
for child in body.children:
if child.name is None:
process_text(str(child))
else:
process_tag(child)
else:
# 直接遍历 soup 顶层
for child in soup.children:
if child.name is None:
process_text(str(child))
else:
process_tag(child)

# 计算最终 X 坐标并排序
items = []
for digit, offset, idx in digits_info:
x = idx * char_width + offset
items.append((x, digit))
items.sort(key=lambda t: t[0])
return ''.join(d[1] for d in items)

def restore_display_html(display_html: str) -> str:
"""还原单个 display_html 中的数字"""
# 提取 style 部分(可能存在多个 style,取第一个包含 :root 的)
style_match = re.search(r'<style>(.*?)</style>', display_html, re.DOTALL)
var_values = {}
if style_match:
style_text = style_match.group(1)
var_values = parse_css_variables(style_text)
# 去掉原 style 标签后的 html
html_no_style = re.sub(r'<style>.*?</style>', '', display_html, flags=re.DOTALL)
result = extract_digits_and_offsets(html_no_style, var_values, char_width=15)
return result

image-20260506203709527

更多内容也在公众号更新:码字的秃猴

tuhou