JS逆向|猿人学逆向反混淆练习平台第四题逆向分析
·
关注它,不迷路。
-
本文章中所有内容仅供学习交流,不可用于任何商业用途和非法用途,否则后果自负,如有侵权,请联系作者立即删除
一.题目地址
https://match.yuanrenxue.cn/match/4
二.抓包分析
打开控制台后,抓包分析:

翻页请求数据后,加载了一堆图片。并且接口返回的不是我们直接想要的数据。
逆向分析我不是很在行,直接看别人怎么做的吧:
看他用了ocr识别库。古法逆向,到此结束。
三.AI工具
我这里使用codeX的cli工具 + Gpt-5.4 xhigh 的AI模型。
使用的MCP则是 JSReverser-MCP
四.提示词
使用:JSReverse MCP方式:插桩采集完整的输入输出及中间态数据,与本地算法进行逻辑一致性和结果正确性对比分析。URL: 【https://match.yuanrenxue.cn/match/4】目标:【目标接口,https://match.yuanrenxue.cn/api/question/4,纯雪碧图,请识别】 触发方式: 【翻面】 约束:不使用playwright等浏览器自动化工具,不能联网搜索公开案例 cookie:将{"sessionid":"XXXxxx"}加入到请求代码中,表示当前登录UA设置:yuanrenxue交付:可运行的python脚本,运行后打印1-5页的响应数据,并计算总和
五.AI提供的Python纯算源码
import base64import collectionsimport hashlibimport reimport timefrom collections import dequefrom io import BytesIO
import numpy as npimport requestsfrom PIL import Image, ImageFilter
API_URL = "https://match.yuanrenxue.cn/api/question/4"SESSION_ID = "XXXXXX"DISPLAY_WIDTH = 8.5PAGE_SIZE = 10PAGE_COUNT = 5
TD_RE = re.compile(r"<td>(.*?)</td>", re.S)IMG_RE = re.compile( r'<img[^>]+src="data:image/png;base64,([^"]+)"[^>]+class="([^"]+)"[^>]+style="([^"]+)"')LEFT_RE = re.compile(r"left:([\-0-9.]+)px")
# These templates were collected by browser-side instrumentation after the page# hid the md5-selected noise class and the remaining images were scaled to 8.5px.EXACT_FEATURE_MAP = { "0000000001f03b83b83f01f03f83383b83f00e0000000000": "8", "0000000001f01f03803e03f03b83383b81f00c0000000000": "6", "0000000000300700700f01b03b03f83f8030000000000000": "4", "0000000003f83f80380700700e00e00c01c0000000000000": "7", "0000000001f01f01801803f00780380383f01c0000000000": "5", "0000000000701f01f0070070070070070070000000000000": "1", "0000000001f03f80380380700e01c03f83f8000000000000": "2", "0000000001f03f83383b83f81f80380703f01c0000000000": "9", "0000000003f03f00300701f00700382383f01c0000000000": "3", "0000000001f03f83383383183183383b81f00c0000000000": "0", "0000000001f01f00380700f00780381381f00e0000000000": "3", "0000000000f01f81981f00f01f81981b81f0060000000000": "8", "0000000000f01f01801f01f81981981f80f0060000000000": "6", "0000000000f01f81981981f80f80180381f00e0000000000": "9", "0000000001f81f80380300700600600e00e0000000000000": "7", "0000000000700f00f0030030030030030030000000000000": "1", "0000000000300380780780f81b81f81f8038000000000000": "4", "0000000000f01f81981983981981981f80f0060000000000": "0", "0000000001f01f80180380700f00c01f81f8000000000000": "2", "0000000000f01f01801c01f00780180381f00e0000000000": "5",}
def build_session() -> requests.Session: session = requests.Session() session.headers.update( { "User-Agent": "yuanrenxue", "X-Requested-With": "XMLHttpRequest", } ) session.cookies.update({"sessionid": SESSION_ID}) return session
def hidden_class(key: str, value: str) -> str: raw = base64.b64encode((key + value).encode()).decode().replace("=", "") return hashlib.md5(raw.encode()).hexdigest()
def mask_to_hex(mask: np.ndarray) -> str: bits = "".join("1" if x else "0" for x in mask.reshape(-1).tolist()) return hex(int(bits, 2))[2:].zfill((len(bits) + 3) // 4)
def hex_to_mask(feature_hex: str) -> np.ndarray: bits = bin(int(feature_hex, 16))[2:].zfill(len(feature_hex) * 4) return np.array([bit == "1" for bit in bits], dtype=bool).reshape(16, 12)
def render_digit_mask(image_b64: str) -> np.ndarray: image = Image.open(BytesIO(base64.b64decode(image_b64))).convert("RGB") arr = np.asarray(image) flat = arr.reshape(-1, 3) bg = np.array(collections.Counter(map(tuple, flat)).most_common(1)[0][0], dtype=np.float32) dist = np.sqrt(((arr.astype(np.float32) - bg) ** 2).sum(axis=2)) mask = (dist > 35).astype(np.uint8) * 255
rendered_w = int(round(DISPLAY_WIDTH)) rendered_h = max(1, int(round(image.height * DISPLAY_WIDTH / image.width))) rendered = Image.fromarray(mask).resize( (rendered_w, rendered_h), Image.Resampling.BILINEAR, ) rendered_arr = np.asarray(rendered) if not np.any(rendered_arr > 100): raise ValueError("digit mask is empty")
canvas = np.zeros((16, 12), dtype=np.uint8) offset_y = (canvas.shape[0] - rendered_arr.shape[0]) // 2 offset_x = (canvas.shape[1] - rendered_arr.shape[1]) // 2 canvas[ offset_y : offset_y + rendered_arr.shape[0], offset_x : offset_x + rendered_arr.shape[1], ] = rendered_arr return canvas > 100
def crop_mask(mask: np.ndarray) -> np.ndarray: ys, xs = np.where(mask) if len(xs) == 0: raise ValueError("empty digit mask") return mask[ys.min() : ys.max() + 1, xs.min() : xs.max() + 1]
def normalize_mask(mask: np.ndarray) -> np.ndarray: cropped = crop_mask(mask) image = Image.fromarray(cropped.astype(np.uint8) * 255) resized = image.resize((14, 18), Image.Resampling.NEAREST) return np.asarray(resized) > 0
def augmented_masks(mask: np.ndarray) -> list[np.ndarray]: image = Image.fromarray(mask.astype(np.uint8) * 255) variants: list[np.ndarray] = []
for dx in (-1, 0, 1): for dy in (-1, 0, 1): canvas = Image.new("L", image.size, 0) canvas.paste(image, (dx, dy)) variants.append(np.asarray(canvas) > 0)
variants.append(np.asarray(image.filter(ImageFilter.MaxFilter(3))) > 0) variants.append(np.asarray(image.filter(ImageFilter.MinFilter(3))) > 0) return variants
def build_fallback_library() -> dict[str, list[np.ndarray]]: library: dict[str, list[np.ndarray]] = {} for feature_hex, digit in EXACT_FEATURE_MAP.items(): normalized = normalize_mask(hex_to_mask(feature_hex)) library.setdefault(digit, []).extend(augmented_masks(normalized)) return library
FALLBACK_LIBRARY = build_fallback_library()
def hole_stats(mask: np.ndarray) -> tuple[list[tuple[int, float]], tuple[int, int]]: cropped = crop_mask(mask) height, width = cropped.shape seen = np.zeros_like(cropped, dtype=bool) holes: list[tuple[int, float]] = []
for y in range(height): for x in range(width): if cropped[y, x] or seen[y, x]: continue
queue = deque([(y, x)]) seen[y, x] = True points: list[tuple[int, int]] = [] touches_border = False
while queue: cy, cx = queue.popleft() points.append((cy, cx)) if cy in (0, height - 1) or cx in (0, width - 1): touches_border = True
for ny, nx in ((cy - 1, cx), (cy + 1, cx), (cy, cx - 1), (cy, cx + 1)): if 0 <= ny < height and 0 <= nx < width and not cropped[ny, nx] and not seen[ny, nx]: seen[ny, nx] = True queue.append((ny, nx))
if not touches_border: center_y_ratio = (sum(py for py, _ in points) / len(points)) / height holes.append((len(points), center_y_ratio))
return holes, (height, width)
def nearest_digit(mask: np.ndarray, candidates: set[str]) -> str: normalized = normalize_mask(mask) best_digit = "" best_distance = None
for digit in candidates: for template in FALLBACK_LIBRARY[digit]: distance = int(np.count_nonzero(normalized != template)) if best_distance is None or distance < best_distance: best_distance = distance best_digit = digit
if not best_digit: raise ValueError("no fallback candidate matched") return best_digit
def classify_digit(mask: np.ndarray) -> str: feature_hex = mask_to_hex(mask) digit = EXACT_FEATURE_MAP.get(feature_hex) if digit is not None: return digit
holes, (_, width) = hole_stats(mask) if len(holes) == 2: return "8" if len(holes) == 1: area, center_y_ratio = holes[0] if area <= 2: return "4" if area >= 8: return "0" if center_y_ratio < 0.4: return "9" return "6" if width <= 5: return "1"
return nearest_digit(mask, {"2", "3", "5", "7"})
def decode_rows(page_json: dict) -> list[int]: hidden = hidden_class(page_json["key"], page_json["value"]) numbers: list[int] = []
for td_html in TD_RE.findall(page_json["info"]): visible_parts = [] for dom_idx, match in enumerate(IMG_RE.finditer(td_html)): image_b64, classes, style = match.groups() css_class = [x for x in classes.split() if x != "img_number"][0] if css_class == hidden: continue
left = float(LEFT_RE.search(style).group(1)) visible_idx = len(visible_parts) visible_parts.append((visible_idx * DISPLAY_WIDTH + left, dom_idx, image_b64))
visible_parts.sort(key=lambda x: (x[0], x[1]))
digits = [classify_digit(render_digit_mask(image_b64)) for _, _, image_b64 in visible_parts] numbers.append(int("".join(digits)))
return numbers
def fetch_page(session: requests.Session, page: int) -> dict: response = session.get( API_URL, params={"page": page, "pageSize": PAGE_SIZE, "kw": ""}, timeout=20, ) response.raise_for_status() return response.json()
def main() -> None: session = build_session() total = 0
for page in range(1, PAGE_COUNT + 1): page_json = fetch_page(session, page) numbers = decode_rows(page_json) page_sum = sum(numbers) total += page_sum display_numbers = [f"{number:06d}" for number in numbers] print(f"page {page}: {display_numbers} | sum={page_sum}") if page != PAGE_COUNT: time.sleep(1)
print(f"total={total}")
if __name__ == "__main__": main()
运行结果:

从AI开始分析到结束,大约耗时50分钟左右。
今天的分享就到这里,感谢阅读。
欢迎加入知识星球,学习更多AST和爬虫技巧。

更多推荐


所有评论(0)