前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] [post cid="1634"/] 前情提要 已经毕业半年多了，有关于小黑盒的开发也搁置很久了，感觉也没有那个精力继续开发了，记录一下最近的算法研究，就当给大学生活做个小结。 @> 本文适用于小黑盒 1.3.262，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] [post cid="1383"/] 前情提要 距离上一篇文章已经过去一年多了，前两天心血来潮找了最新版本研究了一下，熬了两天夜也没整明白，不打算继续研究下去了，做个笔记记录下分析过程。 @> 本文适用于小黑盒 1.3.191，小黑盒历史版本参考我的仓库 @> 全文评论后可见
前言 总共花了4天休息时间做的小玩意，支持显示通用TOTP令牌(6位数字)和STEAM令牌(5位)，同时也是个桌面时钟。 源码获取：GitHub 令牌原理 i> 简单的介绍一下两个令牌的原理，不感兴趣的可以参考下一节 原理 令牌类似于短信验证码，是一种一次性密码OTP，核心原理很简单，服务器和用户事先约定好一个Secret，然后用户通过特殊的算法（一般用Secret + 时间戳或者计数器作为参数），得到一串字符，服务器也通过同样的算法得到一串字符，如果两者相同，那么就证明了我是我。 常见的令牌大多是基于时间的令牌TOTP，也就是用当前的时间戳作为输入，只要保证和服务端时间差不超过令牌有效期（常见的实现是30秒）就能确保通过验证。优点是令牌客户端可以不需要联网，缺点是可能因为时间差导致验证失败。 另一种实现是基于HMAC的令牌HOTP，这种令牌需要和服务器约定一个计数器，客户端刷新一次令牌，就告诉服务器，让两边的计数器同步，这样用户和服务器得到的结果才能一样，才能通过验证。缺点显而易见，就是令牌客户端需要联网。 STEAM的令牌系统类似于TOTP，也是基于时间戳的，只是算法略有区别，最明显的区别是普通TOTP计算得到的是6位数字，STEAM令牌是5位数字或字母。 x> Secret一定要保管好，千万不要向他人透露。 i> 完整代码位于utils/twofa.py TOTP详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【MNUHE6DXGEZDGNBVGY======】(Base32编码，也有的实现采用Base64编码） 1. 取当前时间戳/30，取整后记为C【53914423】 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变) 5. 把I转成10进制数字【17602563747】，最低6位就是结果【643943】 Python实现 import hmac import hashlib from base64 import b32decode from time import time import struct def get_totp_auth_code(secret: str, t: int = None): if not t: t = int(time() / 30) key = b32decode(secret) msg = struct.pack(">Q", t) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff code = str(binary)[-6:].zfill(6) return code # 演示DEMO print(get_totp_auth_code('MNUHE6DXGEZDGNBVGY======',53914423)) # 输出结果为 643943 Steam令牌详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【Y2hyeHcxMjM0NTY=】(Base64编码） 1. 取当前时间戳/30，先取整【53914423】，然后转换成8字节，前方补0，记为C【000000000336ab37】(16进制) 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变)，为了方便计算转成10进制为【2114643943】 5. 创建一个字母表G【23456789BCDFGHJKMNPQRTVWXY】(去掉了一些容易混淆的字符) 6. 用I【2114643943】对26取余，记为M【9】，然后将I整除26，重复5次，得到5个M【9,13,7,12,25】(10进制) 7. 把5个M作为下标，从G中取出对应位置的字符，最后拼接起来就是结果【CH9GY】 i> 两个算法0~4步都是一模一样的，只是steam的算法多了一步处理过程 Python实现 import hmac import hashlib from base64 import b64decode from time import time import struct def get_steam_auth_code(secret: str, t: int = None) -> str: if not t: t = int(time()/30) msg = struct.pack(">Q", t) key = b64decode(secret) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff codestr = list('23456789BCDFGHJKMNPQRTVWXY') chars = [] for _ in range(5): chars.append(codestr[binary % 26]) binary //= 26 code = ''.join(chars) return code # 演示DEMO print(get_steam_auth_code('Y2hyeHcxMjM0NTY=',53914423)) # 输出结果为 CH9GY 屏幕显示 i> 我使用的是微雪的2.7寸墨水屏，也可以使用别的尺寸的屏幕，自行修改图片生成函数进行适配即可。 买屏幕的时候直接买适配树莓派的就行了，基本上都提供Python的驱动，直接用就行，非常方便。 !> 常见的屏幕一般是SPI接口，树莓派默认是不开启SPI接口的，可以使用官方的工具rasbpi-config开启，或者手动编辑/boot/config.txt，在最后一行加一个dtparam=spi=on，重启后生效。 img.py i> 如果对PIL绘图不是很熟悉可以看下一节。 ''' # @Author : Chr_ # @Date : 2021-03-30 17:04:24 # @LastEditors : Chr_ # @LastEditTime : 2021-04-03 00:26:15 # @Description : 图片生成器 ''' import time from os import path from typing import List, Tuple from PIL import Image, ImageDraw, ImageFont SCREEN_W = 264 # 屏幕显示宽度 SCREEN_H = 176 # 屏幕显示高度 CODE_W = 115 # 中间区域宽度 CODE_H = 27 # 中间区域一条令牌的高度 ICON_W = 20 # 左边图标区域宽度 ICON_H = 44 # 左边图标区域一个图标的高度(包含空白) CLOCK_W = SCREEN_W-ICON_W - CODE_W CLOCK_H = SCREEN_H FONT_CODE = ImageFont.truetype(path.join('font', 'Courier Prime Code.ttf'), 25) # 令牌字体 FONT_TAG = ImageFont.truetype(path.join('font', 'CascadiaMono.ttf'), 12) # 令牌名称的字体 FONT_PAGE = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 15) # 页码字体 FONT_ICO = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 22) # 图标区域的字体 FONT_CLOCK = ImageFont.truetype(path.join('font', 'hartland.ttf'), 50) # 时钟字体 FONT_DATE = ImageFont.truetype(path.join('font', 'hartland.ttf'), 23) # 日期字体 FONT_TIPS = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 13) # 日期下方文字的字体 def generate_2fa_img(data: List[Tuple[str, str]], page: Tuple[int, int] = (1, 1), active: bool = True, tips: List[str] = None) -> Image: def draw_icon() -> Image: '''绘制图标''' img = Image.new('L', (ICON_W, SCREEN_H), 0xff) w, h = FONT_ICO.getsize('▲') draw = ImageDraw.Draw(img) ICO_TXT = ['▲', '▼', '●', '■'] if active else ['△', '▽', '○', '□'] for i in range(4): x, y = (ICON_W-w)/2+1, i*44+(44-h)/2 draw.text((x, y), ICO_TXT[i], font=FONT_ICO, fill=0x00) return img def draw_auth() -> Image: '''绘制令牌''' def draw_auth_single(name: str, code: str) -> Image: '''绘制单个令牌''' # 计算尺寸 w1, h1 = FONT_CODE.getsize(code) w2, h2 = FONT_TAG.getsize(name[:3]) W2, H2 = CODE_H, 18 W1, H1 = CODE_W-H2, CODE_H # 画令牌 im_code = Image.new('L', (CODE_W, CODE_H), 0xff) draw_code = ImageDraw.Draw(im_code) draw_code.text(((W1-w1)/2, (H1-h1)/2), code, font=FONT_CODE, fill=0x00) # 画名称 im_name = Image.new('L', (W2, H2), 0xff) draw_name = ImageDraw.Draw(im_name) draw_name.text(((W2-w2)/2, (H2-h2)/2-2), name[:3], font=FONT_TAG, fill=0x00) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) im_name = im_name.rotate(angle=90, expand=True) im_code.paste(im_name, (W1, 0)) draw_code.rectangle((0, 0, CODE_W-1, CODE_H), outline=0) return im_code def draw_page_info() -> Image: '''绘制页码''' PAGE_H = SCREEN_H-6*CODE_H+2 im_page = Image.new('L', (CODE_W, PAGE_H), 0xff) draw = ImageDraw.Draw(im_page) draw.rectangle((0, 0, CODE_W-1, PAGE_H), outline=0) curr, total = page curr -= 1 if total < 1: total = 1 if curr > total: curr = 0 pages = ['-'] * total if active: pages[curr] = '≡' else: pages[curr] = '≡' page_str = ' '.join(pages) wp, hp = FONT_PAGE.getsize(page_str) draw.text(((CODE_W-wp)/2, (PAGE_H-hp)/2-2), page_str, font=FONT_PAGE, fill=0x00) return im_page img = Image.new('L', (CODE_W, SCREEN_H), 0xff) y = -1 for name, code in data[:6]: img_code = draw_auth_single(name, code) img.paste(img_code, (0, y)) y += img_code.height img_page = draw_page_info() img.paste(img_page, (0, y)) return img def draw_clock() -> Image: '''绘制时钟''' img = Image.new('L', (CLOCK_W, CLOCK_H), 0xff) draw = ImageDraw.Draw(img) time_str = time.strftime("%H:%M", time.localtime()) date_str = time.strftime("%Y-%m-%d", time.localtime()) # 画时钟 wc, hc = FONT_CLOCK.getsize(time_str) wd, hd = FONT_DATE.getsize(date_str) draw.text(((CLOCK_W-wc)/2+3, 10), time_str, font=FONT_CLOCK, fill=0x00) draw.text(((CLOCK_W-wd)/2+3, 65), date_str, font=FONT_DATE, fill=0x00) for i, tip in enumerate(tips, 0): wt, ht = FONT_TIPS.getsize(tip) draw.text(((CLOCK_W-wt)/2, 110+(ht+5)*i), tip, font=FONT_TIPS, fill=0x00) return img if not active: data = [('---', '-----')] * 6 if len(data) < 6: data += [('---', '-----')] * 6 if not tips: tips = ['By Chr_', 'Ver 0.01', 'chrxw.com'] full_img = Image.new('L', (SCREEN_W, SCREEN_H), 0xff) img_auth = draw_auth() full_img.paste(img_auth, (ICON_W, 0)) img_icon = draw_icon() full_img.paste(img_icon, (0, 0)) img_clock = draw_clock() full_img.paste(img_clock, (ICON_W+CODE_W, 0)) with open('o.png', 'wb') as f: full_img.save(f, 'png') return full_img PIL绘图简介 创建图片('L'代表灰度模式，0xff表示默认颜色是白色) img = Image.new('L', (width, height), 0xff) 图片拼贴(把img_icon粘贴到img(30,0)的位置) img.paste(img_icon, (30, 0)) 创建画笔对象 draw = ImageDraw.Draw(img) 绘制矩形(左上角为(x1,y1)，右下角为(x2,y2)) draw.rectangle((x1, y1, x2, y2), outline=0) 绘制直线(端点为(x1,y1)和(x2,y2)，0x00表示线条颜色为黑色，宽度为1) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) 创建字体对象('font/CascadiaMono.ttf'是字体路径，12是字号) font = ImageFont.truetype('font/CascadiaMono.ttf'), 12) 获取文字的尺寸(获取用指定字体把'text'显示出来的尺寸，高为h，宽为w) w, h = font.getsize('text') 绘制文本(把'text'用指定字体显示在(20,10)的位置，0x00表示字体颜色是黑色) draw.text((20, 10), 'text', font=font, fill=0x00) 按键响应 原理 树莓派有2种引脚编号方式，BCM和BOARD，两者的区别只是对引脚的标记不同，使用GPIO前申明一下引脚的编号方式即可，使用命令gpio readall可以查看所有引脚的编号、状态、电平状态 类似于51单片机的中断系统，树莓派也可以设定中断，不过先要把指定引脚设定为输入模式，设定完后，再创建一个“监听器”，当指定引脚的电平从高电平变为低电平的时候，就会自动调用指定函数，参数为引脚号。 我买的屏幕左边4个按键使用BCM编码的引脚编号为【5,6,13,19】，前两个用来控制翻页，第三个用来切换时钟下方显示的内容，第四个用来控制令牌的显示/隐藏。 实现 i> 这里只列出了事件响应的部分，具体功能的实现请参考obj.py import RPi.GPIO as GPIO from obj import Pi_2FA # 使用的引脚序号 KEYS = [5, 6, 13, 19] pi_2fa = Pi_2FA() # 按键响应回调函数 def btn_callback(key): if pi_2fa.Ready: if key == KEYS[0]: pi_2fa.page_down() elif key == KEYS[1]: pi_2fa.page_up() elif key ==KEYS[2]: pi_2fa.switch_mode() elif key ==KEYS[3]: pi_2fa.none_mode() else: print(key) return # 设置引脚编号方式为BCM GPIO.setmode(GPIO.BCM) for key in KEYS: # 设定指定引脚为输入模式 GPIO.setup(key, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 为指定引脚添加事件响应函数 GPIO.add_event_detect(key, GPIO.RISING, callback=btn_callback, bouncetime=200)
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] [post cid="1347"/] [post cid="1365"/] 前情提要 就在我写完上一篇文章后没过一个礼拜，小黑盒就又改 hkey 计算方法了，断断续续花了一礼拜终于搞清楚它的算法。 在这里分享一下逆向的过程。 @> 本文适用于小黑盒 1.3.121 ~ 1.3.124 ，小黑盒历史版本参考我的仓库 Smali 分析 使用Jadx打开安装包，找到hkey的计算函数 可以看到两个版本的 hkey 计算方式略有区别，最明显的就是替换的字符改了 修改以后还是提示参数错误，肯定是 encode 函数也改了，接下来换用IDA So 分析 先贴一下IDA生成的反汇编代码，也不用看的很仔细 int __fastcall encode(int *a1, int a2, int a3, int a4, int a5) { int *v5; // r9 int v6; // r11 _DWORD *v7; // r4 char *v8; // r5 char *v9; // r0 size_t v10; // r0 int v11; // r10 int v12; // r0 int v13; // r0 _DWORD *v14; // r2 const char *v15; // r1 char *v16; // r10 const char *v17; // r11 size_t v18; // r4 size_t v19; // r4 signed int v20; // r8 char *v21; // r6 char *v22; // r0 int v23; // r12 int i; // lr int v25; // r5 int v26; // r0 unsigned int v27; // r4 int v28; // r3 unsigned int v29; // r1 int v30; // r8 const char *v31; // r4 int j; // r1 int k; // r0 int v34; // r0 size_t v35; // r5 size_t v36; // r6 char *v37; // r5 char *v38; // r0 int v39; // r1 int v40; // r12 int l; // lr int v42; // r6 int v43; // r0 unsigned int v44; // r4 int v45; // r3 unsigned int v46; // r2 int result; // r0 int v48; // [sp+0h] [bp-48h] int v49; // [sp+4h] [bp-44h] int *v50; // [sp+8h] [bp-40h] char *nptr; // [sp+Ch] [bp-3Ch] int v52; // [sp+10h] [bp-38h] _DWORD *v53; // [sp+14h] [bp-34h] char s; // [sp+19h] [bp-2Fh] int v55; // [sp+24h] [bp-24h] v5 = a1; v6 = a4; v7 = &_stack_chk_guard; if ( j_check_signature(a1) != 1 ) goto LABEL_28; v53 = &_stack_chk_guard; v8 = (char *)&v48 - ((strlen(KEYTWO[0]) + 62) & 0xFFFFFFF8); v9 = strcpy(v8, KEYTWO[0]); v10 = strlen(v9); _aeabi_memcpy(&v8[v10], "/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=", 56); v7 = 0; v11 = (*(int (__fastcall **)(int *, int, _DWORD))(*v5 + 676))(v5, v6, 0); v12 = *v5; v52 = a5; v13 = (*(int (__fastcall **)(int *, int, _DWORD))(v12 + 676))(v5, a5, 0); if ( v11 ) { v14 = v53; if ( !v13 ) goto LABEL_26; v49 = v6; v50 = &v48; v15 = (const char *)v11; v16 = (char *)v13; v17 = v15; v18 = strlen(v15); v19 = v18 + strlen(v8); v20 = v19 + strlen(v16); v21 = (char *)&v48 - ((v20 + 7) & 0xFFFFFFF8); strcpy((char *)&v48 - ((v20 + 7) & 0xFFFFFFF8), v17); v22 = strcat(v21, v8); nptr = v16; strcat(v22, v16); v23 = v20 - 1; for ( i = 0; v23 > i; ++i ) { v25 = 0; while ( v25 < v23 - i ) { v26 = (int)&v21[v25]; v27 = (unsigned __int8)v21[v25]; v28 = v25; v29 = (unsigned __int8)v21[v25++ + 1]; if ( v27 > v29 ) { v21[v28] = v29; *(_BYTE *)(v26 + 1) = v27; } } } v30 = v20 / 3; v31 = (const char *)malloc(v30 + 2); for ( j = 0; j <= v30 + 1; ++j ) v31[j] = 0; for ( k = 0; k <= v30; ++k ) v31[k] = v21[3 * k]; v34 = atoi(nptr); sprintf(&s, "%#x", v34); v35 = strlen(v31); v36 = strlen(&s) + v35; v37 = (char *)&v48 - ((v36 + 7) & 0xFFFFFFF8); v38 = strcpy(v37, v31); strcat(v38, &s); v39 = v49; v40 = v36 - 1; for ( l = 0; v40 > l; ++l ) { v42 = 0; while ( v42 < v40 - l ) { v43 = (int)&v37[v42]; v44 = (unsigned __int8)v37[v42]; v45 = v42; v46 = (unsigned __int8)v37[v42++ + 1]; if ( v44 > v46 ) { v37[v45] = v46; *(_BYTE *)(v43 + 1) = v44; } } } (*(void (__fastcall **)(int *, int, const char *))(*v5 + 680))(v5, v39, v17); (*(void (__fastcall **)(int *, int, char *))(*v5 + 680))(v5, v52, nptr); v7 = (_DWORD *)j_charToJstring(v5, v37); } v14 = v53; LABEL_26: if ( *v14 == v55 ) return (int)v7; LABEL_28: result = *v7 - v55; if ( *v7 == v55 ) result = j_j_charToJstring(v5, UNSIGNATURE[0]); return result; } 撇开变量部分，稍微整理了一下变量名，以_p结尾的都是指针，具体变量类型参考原始代码 首先可以看到调用了_stack_chk_guard，看名字应该是栈溢出保护，不管他 第58~61行看起来复杂实际上是新建了一个数组，然后把字符串KWYTWO和/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=拼起来存进数组buffer1中 最后有两个__fastcall，根据变量名称，猜测跟传入的参数有关 首先检查了是否传入了urlpath和timestamp这两个变量，如果传入值为空就跳出 第77~84行看起来挺复杂，实际上就是计算出urlpath+buffer1+timestamp的长度(用于开辟内存空间)，然后把这三个字符串拼起来，存进buffer2中 最后是一个循环，看上去蛮复杂，实际上就是冒泡排序，让buffer2中的字符按照Ascii顺序排序 首先取buffer2长度的 1/3+2，创建数组buffer3，然后清空数组，然后是一个循环，buffer3[k] = buffer2[k*3]，数组长度要加2是因为字符串结尾有个\0，以及避免特殊情况数组越界 第109~116行，先把时间戳timestamp转换成十六进制，然后跟buffer3拼接起来，存进buffer4 第118~134行跟刚刚很像，是对buffer4的冒泡排序 最后就是调用charToJstring然后返回 Frida Hook 这里就以验证为主，Hook了3个函数charToJstring,strcpy,strcat，hook.js如下： console.log("脚本载入成功"); Java.perform(function() { var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring"); console.log(charToJstringAddr); if (charToJstringAddr != null) { Interceptor.attach(charToJstringAddr, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('charToJstring - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcat = Module.findExportByName("libc.so", "strcat"); console.log(strcat); if (strcat != null) { Interceptor.attach(strcat, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcat - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } var strcpy = Module.findExportByName("libc.so", "strcpy"); console.log(strcpy); if (strcpy != null) { Interceptor.attach(strcpy, { onEnter: function(args) { var arg0 = args[0]; var arg1 = args[1]; console.log('strcpy - Enter'); console.log(Memory.readCString(arg0)); console.log(Memory.readCString(arg1)) }, onLeave: function(retval) { console.log('======') } }) } }); 可以配合上面的分析一起看，还是比较好理解的 算法实现 C语言实现 半抄半改写出来的，buffer4存放的是计算结果 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { // 输入1：/bbs/app/link/view/time // 输入2：1597893810 // 输出1：$()++-/////01245789=CGKMPRUZ_aaabeefhiijklmnopqrtuvwxz （中间结果） // 输出2：$()++-/////001223455789=CGKMPRUZ_aaabbcdeeeffhiijklmnopqrtuvwxxz char buffer1[200] = "//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time="; //密钥字符串 char buffer2[200] = "\0"; char buffer3[200] = "\0"; char buffer4[200] = "\0"; char buffer5[200] = "\0"; int timestamp = 1597893810; char timestamp_s[] = "1597893810"; char urlpath[] = "/bbs/app/link/view/time"; int i, m = 0; int count = 0; int t1, t2 = 0; int buffer2_len; int buffer3_len; strcpy(buffer2, urlpath); strcat(buffer2, buffer1); strcat(buffer2, timestamp_s); // buffer2内容是 url路径+buffer1+时间戳 buffer2_len = strlen(buffer2); count = buffer2_len - 1; for (i = 0; i < count; ++i) { m = 0; while (m < count - i) { t1 = buffer2[m]; t2 = buffer2[m + 1]; if (t1 > t2) { buffer2[m] = t2; buffer2[m + 1] = t1; } m++; } } buffer3_len = buffer2_len / 3; for (i = 0; i <= buffer3_len; ++i) buffer3[i] = buffer2[3 * i]; sprintf(buffer5, "%#x", timestamp); strcpy(buffer4, buffer3); strcat(buffer4, buffer5); count = strlen(buffer4) - 1; for (i = 0; i < count; i++) { m = 0; while (m < count - i) { t1 = buffer4[m]; t2 = buffer4[m + 1]; if (t1 > t2) { buffer4[m] = t2; buffer4[m + 1] = t1; } m++; } } puts(buffer4); } Python实现 import hashlib # 密钥字符串 ENC_STATIC = '//Z1q/Gb/R///+9xZ561TtoHjPrv2ew0Ln8vZnI5oObw+++oa3zw++1yd7wMqU/eNKahfmji5/xDu7EuCQfjaRk4TBKXrnhrlnkz@%$^&*(-_-)hahaha(-_-)_time=' # 测试样例 test = [ ('/account/data_report', 1597919951, '$()++-////0112235555789=CGKMPRUZ__aaaccdeefffhijkmnooqrrtuvwxxz'), ('/game/get_game_name', 1597920084, '$()++-////001233445555679=CGKMPRUZ__aaabeeeffghhjkmmnnortuvwxxz'), ('/bbs/app/feeds/news', 1597920132, '$()++-/////011233345556789@DHKNQTXZ_aabbeeeffhhijlnnoprstuwwxxz'), ('/bbs/app/profile/subscribed/events', 1597920180, '$()++-/////00113334555679=CGKMPRUZ_aaabbbdeeeffhhijkmnnopqrsstuvwxxz'), ('/bbs/app/link/view/time', 1597920216, '$()++-/////00123335556789=CGKMPRUZ_aaabdeeeffhiijklmnopqrtuvwxxz') ] # 计算MD5 def md5_calc(data: str) -> str: md5 = hashlib.md5() md5.update(data.encode('utf-8')) result = md5.hexdigest() return(result) # encode实现 def encode(url: str, t: int) -> str: enc = list(f'{url}{ENC_STATIC}{t}') count = len(enc) - 1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] l = len(enc) // 3 + 1 enc += '\0' enc = [enc[3*i] for i in range(0, l)] if enc[-1] == '\0': enc = enc[:-1] enc += list(hex(t)) count = len(enc)-1 for i in range(0, count): for j in range(0, count-i): if (enc[j] > enc[j+1]): enc[j], enc[j+1] = enc[j+1], enc[j] result = ''.join(enc) return(result) # 算法验证 for i, j, k in test: s = encode(i, j) if (s == k): print(i, j, '测试通过') print(k) else: print(i, j, '测试失败') print(k) print(s) 后记 @> 全文完，完整项目在GitHub上开源：链接 (求Star)