TextSeek 2.8.4120 离线版记录

2026-02-07 07:24:36 星期六

TextSeek 2.8.4120 离线版记录

仅作技术分析记录,请支持正版软件!!!

没有整理,都是随手记的,0.0,建议直接跳到 “py ” 和 “总结”

基本信息

python2 cx_freeze 打包程序,(Python 2.7.18rc1)

类、方法、成员、变量名混淆,(也许是某种加密,未深入)

GUI 使用PySide库(qt4)

界面文本字符串aes+b64加密存储,查表实现多语言

主要思路:

“猴子补丁”(Monkey Patching),缺陷:无法干涉 Cyfunction的内部逻辑

python27.dll hook ,(底层,噪声数据多,后面并没有用到)

PyCharm 调试python代码

x64dbg 调试 Cyfunction

字符串引用 (第三方库的类、方法名称并未混淆,可以快速理解 Cyfunction内部逻辑)

[0]启动时 _tsLib1.MyApp.launch

drjuqh==>u'C:\Users\xxxx\AppData\Roaming\TextSeek_off\'

_tsLib1.ioxgl func 解析本地文件

//_tsLib1.Cn24rbl.ikrw25l

cm.ikrw25l(self,bm,im,idkht)

Cn24rbl_ikrw25l

解析config配置

[1]注册; _tsLib1.Akpf2.sovfn42 点击事件

http://ring0.textseek.net:90/d/register2?i=

_tsLib1.Tshsulm.sk6pelg 弹窗

[2]验证码; Qb8m0g 二维码扫描验证

构造函数(self,parent,text,option)

验证码

a_reg_val

valCode

_tsLib1.Qb8m0g.se8lz6t

se8lz6t(guiobj,qr_data)

生成二维码

提交按钮点击事件==》oqkv0qt

_tsLib1.Qb8m0g.oqkv0qt

调用callback

_tsLib1.Qb8m0g.sbmlk 设置callback

tsLib1_Akpf2_sovfn42_1802914D0 调用_tsLib1.Qb8m0g.sbmlk

设置callback 为_tsLib1.Akpf2.awot62

tsLib1_Arp6g_squ8g 调用_tsLib1.Qb8m0g.sbmlk

设置callback 为_tsLib1.Arp6g.ae0tzqg

[y]_tsLib1.Arp6g.ae0tzqg

验证码==》

设置valCode==>mycode[:0x11]

//详见后面python脚本
    

    
?? rbcp1vt   授权数  
ryljiwe  ==>name
rjuhl    ==>serial
rz9jk1y  ==>verifyCode
rh1gol   ==>email
rx5cad   ==>nationId

Togc7yl.dngsb5g

ra ==》_tsLib1.Arp6g 类

tsLib1_Arp6g_squ8g\中设置该属性

//_tsLib1_Ai3rp73_szev03 [x]

//_tsLib1_Akpf2_sovfn42

//tsLib1_Aszdkp4.skqt4

status_code

这里好像是分析在线版时记录的,离线版应该一致

<0  ==>账号选择
2==>
3==》你提交的注册邮箱或序列号有 误。
4==》f_reg_submitError2 The licence type  ( personal / business ) of your email account is different from that of your serial code. 账户(邮箱)的用户类型(个人 / 商业)和序列号的用户类型不一致。
5==》f_reg_submitError3 The email account has been activated by another serial code. Please use another new email as account. 你使用的账户(邮箱)已被其他序列号激活了。请使用另外一个新的邮箱作为账户。
7==》f_reg_submitError5 The licensed machine count of your email account is different from that of your serial code.  账户(邮箱)的授权机器数和序列号的授权机器数不一致。
8==》f_reg_submitError The registration email or serial you submitted has something wrong. 你提交的注册邮箱或序列号有 误。

9==》f_reg_submitError4 The serial code is expired or invalid. 你使用的序列号已过期或已失效。

>=10d  ==>f_reg_submitError


0xc==>f_reg_submitError3 The email account has been activated by another serial code. Please use another new email as account. 你使用的账户(邮箱)已被其他序列号激活了。请使用另外一个新的邮箱作为账户。
0xd==>f_reg_submitError6 The number of machines activated with this serial number has exceeded the limit. Please purchase more licenses for activation. 使用此序列号激活的机器数已超过限制,请购买更多授权进行激活。
0x15==>f_reg_submitError
0x18==>f_reg_submitError8 Offline version only supports business license. 离线版仅支持商业永久授权。


0x51\0x52\0x53==>f_reg_submitError

[x] _tsLib1.Akpf2.awot62

(self,code,_tsLib1.Qb8m0g_obj)

check 验证码

ryljiwe  ==>name
rjuhl    ==>serial
rz9jk1y  ==>verifyCode
rh1gol   ==>email
rx5cad   ==>nationId

保存 按钮点击事件==》op8xe

取消按钮点击事件==》omxml

_tsLib1.Tp7erzw.mphv3pn (校验valCode[5:9])

栈回溯定位到注册对话框

进行启动时的校验

验证流程详见后面python脚本

_tsLib1.Ufs8dql.gcy8crd ==》get uuid

wmic csproduct get uuid

举例:

11223344-5566-7788-99AA-BBCCDDEEFF00

==>
//返回
“112233445566”

_tsLib1.Togc7yl.riuawe(校验valCode[9:0xc])

验证流程详见后面python脚本

_tsLib1.Togc7yl.cecmk( 设置mssuwqd)

解压、反序化 sink 文件

other

界面文本字符串解密,(g.ac0,g.ac1,g.ac2……) , 然后在语言字典中获取指定语言的文本,g.lm

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
import base64
import hashlib

class AaevdqC:
    def __init__(self,key:bytes):
        if isinstance(key,str):
            key=key.encode()
        self.key=hashlib.sha256(key).digest()
    def dec(self,b64data:bytes):
        if isinstance(b64data,str):
            b64data=b64data.encode()
        data=base64.standard_b64decode(b64data)
        cipher=AES.new(key=self.key,iv=data[:16],mode=AES.MODE_CBC)
        x=cipher.decrypt(data[16:])
        x=unpad(x,16)
        print(x)
        return x
        
ac0=AaevdqC('pd2f')
ac1=AaevdqC('kdas')
ac2=AaevdqC('asdd')
ac3=AaevdqC('cvd3')
ac4=AaevdqC('fdsf')
ac5=AaevdqC('cftj')
ac6=AaevdqC('s4gd')
ac7=AaevdqC('db7h')
ac8=AaevdqC('df4f')
ac9=AaevdqC('ar3n')

示例

ac8.dec('JOwoD7X0JvaQuwd4MYJAOQLhBmYRp7Y3E0s8AL9FcJK8dr5RkJX7QfqWYQAHzCZX')

==>
w_start_errorConfig

==>
w_start_errorConfig Local setting file is corrupted, please reset the configuration. 本地配置文件已损坏,请重新进行设置。

py

main.py (python2)

反编译textseek_main_.pyc ,修正环境(主要是将lib目录下的library.zip 解压到library目录,配置环境变量以及sys.path,具体自行探索),

可实现脱离TextSeek.exe,直接使用Python 2.7.18rc1 运行脚本,

(我是直接拷贝python.exe和main.py 到TextSeek.exe目录下,如果脚本报错找不到模块,一般是由于lib目录和library的问题以及包依赖的问题,自行探索)

# -*- coding: utf-8 -*-
# Source Generated with Decompyle++
# File: textseek__main__.pyc (Python 2.7)

import os
import sys




sys.frozen = True
# FILE_NAME = sys.executable
DIR_NAME = os.path.dirname(__file__)
os.environ['TCL_LIBRARY'] = os.path.join(DIR_NAME, 'tcl')
os.environ['TK_LIBRARY'] = os.path.join(DIR_NAME, 'tk')

sys.path.append(DIR_NAME)
sys.path.append(os.path.join(DIR_NAME, 'dlls'))
sys.path.append(os.path.join(DIR_NAME, 'res'))
LIB_PATH = os.path.join(DIR_NAME, 'lib')

sys.path.append(os.path.join(LIB_PATH, 'library'))
sys.path.append(LIB_PATH)
# importer = zipimport.zipimporter(os.path.join(LIB_PATH, 'library.zip'))
# sys.path.insert(0,os.path.join(LIB_PATH, 'library'))


os.environ['PATH'] = DIR_NAME + os.pathsep + os.path.join(DIR_NAME, 'dlls') + os.pathsep + os.environ['PATH']


import os
import zlib
import marshal

#模拟获取mssuwqd ,其实应该通过访问_tsLib1/_tsLib2 库中的对象或方法来获取
roaming_path = os.environ.get('APPDATA')
def parse_data(fpath):
    with open(fpath,'rb') as f:
        data=f.read()
        data=b'\x78\xDA'+data[2:]
    cfg=zlib.decompress(data)
    obj=marshal.loads(cfg)
    print obj
    return obj
sink_path=roaming_path+'\\TextSeek_off\\sink'
sink_data=parse_data(sink_path)
mssuwqd=sink_data[1]
print ('[###]mssuwqd:',mssuwqd)


def print_obj_info(obj):
    all = dir(obj)
    # f=open('log.txt','w')
    for attr in all:
        value = getattr(obj, attr, None)
        s = "attr:%s==>value:%s %s" % (attr, value, type(value))
        print s
        # f.write(s+'\n')
    # f.close()



import requests
import marshal

original_post = requests.post
original_get = requests.get

original_dump = marshal.dump
original_dumps = marshal.dumps
original_load = marshal.load
original_loads = marshal.loads


def my_dump(*args, **kwargs):
    print ('call_dump:%s %s' % (args, kwargs))
    ret = original_dump(*args, **kwargs)
    print ('dump_ret:' , ret)
    return ret


def my_dumps(*args, **kwargs):
    print ('call_dumps:%s %s' % (args, kwargs))
    ret = original_dumps(*args, **kwargs)
    print ('dumps_ret:' , ret)
    return ret


def my_load(*args, **kwargs):
    print ('call_load:%s %s' % (args, kwargs))
    ret = original_load(*args, **kwargs)
    print ('load_ret:%s' % ret)
    return ret


def my_loads(*args, **kwargs):
    print ('call_loads:%s %s' % (args, kwargs))
    ret = original_loads(*args, **kwargs)
    print ('loads_ret:%s' % ret)
    return ret

#弃用,分析在线版时用到
class CustomResponse(object):
    """完全自定义的Response对象"""

    def __init__(self, **kwargs):
        # 基本属性
        self.status_code = kwargs.get('status_code', 200)
        self.url = kwargs.get('url', '')
        self.headers = kwargs.get('headers', {})
        self.history = kwargs.get('history', [])
        self.reason = kwargs.get('reason', 'OK')

        # 内容相关
        text = kwargs.get('text', '')
        content = kwargs.get('content', text)
        json_data = kwargs.get('json', None)

        self.text = text
        self.content = content
        self._json = json_data

        # 编码
        self.encoding = kwargs.get('encoding', 'utf-8')
        self.apparent_encoding = kwargs.get('apparent_encoding', 'utf-8')

        # Cookies
        self.cookies = kwargs.get('cookies', {})

        # 请求信息
        self.request = kwargs.get('request', None)

        # 布尔属性
        self.ok = 200 <= self.status_code < 300
        self.is_redirect = 300 <= self.status_code < 400
        self.is_permanent_redirect = self.status_code == 308

    def json(self, **kwargs):
        if self._json is not None:
            return self._json
        try:
            import json
            return json.loads(self.text)
        except ValueError:
            raise ValueError("No JSON object could be decoded")

    def raise_for_status(self):
        if not self.ok:
            raise requests.exceptions.HTTPError(
                '{} Client Error: {} for url: {}'.format(
                    self.status_code, self.reason, self.url
                )
            )

    def __repr__(self):
        return '<Response [{}]>'.format(self.status_code)

    def __bool__(self):
        return self.ok

    __nonzero__ = __bool__  # Python 2兼容

import json
def my_post(*args, **kwargs):
    # d/register2
    # d/query
    print('call_post:%s %s' % (args, kwargs))
    url = args[0]
    t=''
    if url.endswith('register2'):
        # 0,-1,2,999,2999-12-31,1
        t = '{"regList": [0,-1,2,999,"2999-12-31",1], "retInfo": [0, 0, 1], "retCode": 100}'
    #弃用,分析在线版时用到
    # elif url.endswith('query'):   #todo

    response = CustomResponse(
        status_code=200,
        text=t,
        headers={'Content-Type': 'application/json'},
        url=url
    )
    if not t:
        return None
    return response
    # return original_post(*args, **kwargs)


def my_get(*args, **kwargs):
    # /d/ping
    print('call_get:%s %s' % (args, kwargs))
    url=args[0]
    t=''
    if url.endswith('ping'):
        # t = '{"o": [1, 0, ["ring0.textseek.net:90", "ring1.textseek.net:90", "ring2.textseek.net:90", "ring3.textseek.net:90", "ring4.textseek.net:90"], 1]}'
        t = '{"o": [0, 0, ["ring0.textseek.net:90", "ring1.textseek.net:90", "ring2.textseek.net:90", "ring3.textseek.net:90", "ring4.textseek.net:90"], 0]}'
    response = CustomResponse(
        status_code=200,
        text=t,
        headers={'Content-Type': 'application/json'},
        url=url
    )
    return response
    # return original_get(*args, **kwargs)


# marshal.dump = my_dump
# marshal.dumps = my_dumps
# marshal.load = my_load
# marshal.loads = my_loads

requests.post = my_post
requests.get = my_get


from _tsLib2 import *
from _tsLib1 import *


original_ae0tzqg=Arp6g.ae0tzqg
def hook_ae0tzqg(self,*args,**kwargs):
    print('hook_ae0tzqg params:',self,args,kwargs)
    print ('[####]ae0tzqg ra:',self.ra)
    return original_ae0tzqg(self,*args,**kwargs)
Arp6g.ae0tzqg = hook_ae0tzqg



# original_squ8g=Arp6g.squ8g
# def hook_squ8g(self,*args,**kwargs):
#     print('hook_squ8g params:',self,args,kwargs)
#     print ('[####]squ8g call before ra:', self.ra)
#     ret=original_squ8g(self,*args,**kwargs)
#     print ('[####]squ8g ra:',self.ra)
#     return ret
# Arp6g.squ8g = hook_squ8g
#
# # //_tsLib1.Akpf2.sovfn42
# original_sovfn42=Akpf2.sovfn42
# def hook_sovfn42(self,*args,**kwargs):
#     print('hook_sovfn42 params:',self,args,kwargs)
#     print ('[####]sovfn42 call before ra:', self.ra)
#     ret=original_sovfn42(self,*args,**kwargs)
#     print ('[####]sovfn42 ra:',self.ra)
#     return ret
# Akpf2.sovfn42 = hook_sovfn42
#
# # Aszdkp4.skqt4
# original_skqt4=Aszdkp4.skqt4
# def hook_skqt4(self,*args,**kwargs):
#     print('hook_skqt4 params:',self,args,kwargs)
#     print ('[####]skqt4 call before ra:', self.ra)
#     ret=original_skqt4(self,*args,**kwargs)
#     print ('[####]skqt4 ra:',self.ra)
#     return ret
# Aszdkp4.skqt4 = hook_skqt4

#获取机器码
xuuid=Ufs8dql.gcy8crd()
print ('xuuid:',xuuid)
if g.isMac:
    import multiprocessing as mp
else:
    import multiprocessing as mp

if __name__ == '__main__':

    isThru = 0
    try:
        if g.isMac:
            pass
        mp.freeze_support()
        g.nProc = mp.cpu_count()
        isThru = 1
    except:
        # le9ar(traceback.format_exc())
        print(traceback.format_exc())

    if isThru:
        # try:
        #     from _tsLib1 import *
        # except:
        #     print(traceback.format_exc())
        #     sys.exit()

        # import signal
        # signal.signal(signal.SIGINT, signal.SIG_DFL)

        try:
            g.app = MyApp(sys.argv)  # sys.argv
            if g.app.isBreak == 0:
                # g.isReg = 1
                # print ('mssuwqd:', g.mssuwqd)
                g.app.launch()

        except:
            # le9ar(traceback.format_exc())
            print(traceback.format_exc())

        # g.lm 可获取语言字典信息
        # print_obj_info(g.lm)

        # print_obj_info(g)
        # print_obj_info(Cl0sl)

        sys.exit()


xx.py (python3)

import base64
import hashlib


import random

def generate_serial():
    hex_chars = '0123456789ABCDEF'
    s='F9' + ''.join(random.choice(hex_chars) for _ in range(16))
    n=sum([ord(x) for x in s[:0x12]])%0x100
    s+='%02X'%n
    return s
# 'F96077D5B98EFC11B324'#
SERIAL=generate_serial()

import subprocess

def get_uuid():
    # 执行简单命令
    result = subprocess.run('wmic csproduct get uuid', capture_output=True, text=True)
    xuuid=result.stdout.split()[1].replace('-','')[:0xc]

    print('[-]TextSeek_off_get_uuid:',xuuid)
    return xuuid

def decimal_to_base36(decimal_str: str) -> str:
    """
    使用Python内置功能实现的36进制转换(更简洁)
    """
    if not decimal_str:
        return ""
    
    if not decimal_str.lstrip('-').isdigit():
        raise ValueError("输入必须是一个有效的十进制数字字符串")
    
    # 转换为整数
    num = int(decimal_str)
    
    # 使用Python内置转换函数
    # 注意:Python的int()函数支持2-36进制的字符串转换
    # 但这里我们需要自己实现转换逻辑
    def int_to_base36(n: int) -> str:
        if n == 0:
            return "0"
        
        chars = "0123456789abcdefghijklmnopqrstuvwxyz"
        result = []
        negative = n < 0
        n = abs(n)
        
        while n:
            n, remainder = divmod(n, 36)
            result.append(chars[remainder])
        
        if negative:
            result.append('-')
        
        return ''.join(reversed(result))
    
    return int_to_base36(num)
def left_pad_zero(s: str, width: int) -> str:
    """
    将字符串左填充0到指定宽度
    
    参数:
        s: 需要填充的字符串
        width: 目标宽度
        
    返回:
        左填充0后的字符串
        
    示例:
        >>> left_pad_zero("123", 5)
        '00123'
        >>> left_pad_zero("abc", 6)
        '000abc'
    """
    # 如果字符串长度已经等于或超过目标宽度,直接返回原字符串
    if len(s) >= width:
        return s
    
    # 计算需要填充的0的个数
    zeros_needed = width - len(s)
    
    # 返回填充后的字符串
    return '0' * zeros_needed + s

def calc_part_5_9(xuuid:str):
    md5hexstr=hashlib.md5(xuuid.encode()).hexdigest()
    x=md5hexstr[4:-4]
    a=hashlib.md5(x[4:].encode()).hexdigest()
    t=base64.b32encode(a[4:0x16].encode())
    ret=t[6:8]+t[0xb:0xd]
    print('[+]part_5_9:',ret)
    return ret

def calc_part_9_c(xuuid:str):
    md5hexstr=hashlib.md5(xuuid.encode()).hexdigest()
    x=md5hexstr[4:-4]
    a=hashlib.sha224(x[6:].encode()).hexdigest()
    t=base64.b32encode(a.encode())
    ret=t[8:0xa]+t[0xf:0xf+1]
    print('[+]part_9_c:',ret)
    return ret


def kg(mssuwqd:str,ra:str,xuuid):
    if isinstance(mssuwqd,str):
        mssuwqd=mssuwqd.encode()

    target=b''
    part_0_5=b''
    part_0xc_0x10=left_pad_zero(decimal_to_base36('9999'),4).encode()
    part_0x10=b'C'  # 范围:BC,未测试B,0.0
    part_0x11_end=b''

    temp=mssuwqd[:0xc][5:]
    x=hashlib.md5(temp).hexdigest()
    y=base64.b32encode(x[3:0x14].encode()).decode()
    part_0_5=(y[5:7]+y[9:0xb]+y[0xd]).encode()
    
    part_5_0xc=calc_part_5_9(xuuid)+calc_part_9_c(xuuid)

    part_data=b''.join([part_0_5,part_5_0xc,part_0xc_0x10,part_0x10])
    
 
    t=part_data[:0x11]+mssuwqd[:0xc]

    x=sum(list(t) )
    # print('[-]拼接数据:',t)
    # print('[-]拼接数据sum和:',x,hex(x))

    x+=int(ra)
    # print('[-]求和数:',x,hex(x))
    s_x=str(x)

    part_0x11_end=s_x[-3:]
    target=part_data+part_0x11_end.encode()

    print(target)

    # valCode=[:0x11]
    return target


if __name__=='__main__':
    # xuuid=get_uuid()
    print('[+]serial:',SERIAL)

    #先注册,弹出二维码后,验证码先输入20个数进行测试,这里一定注册失败
    #然后在main.py 应该已经输出验证码生成所需的mssuwqd、xuuid和ra
    #其实ra的获取应该通过二维码数据解析
    #条条大路通罗马,自行探索0.0
    
    #mssuwqd=b''
    #xuuid=''
    #ra=''
    #kg(mssuwqd,ra,xuuid)
    pass

main.py 输出示例:

C:\Program Files (x86)\TextSeek>python.exe main2.py
[1769963509, 'xxxxxxxxxx']
('[###]mssuwqd:', 'xxxxxxxxxx')
('xuuid:', '112233445566')
libpng warning: iCCP: known incorrect sRGB profile
('hook_ae0tzqg params:', <_tsLib1.Arp6g object at 0x00000000066C8888>, ([u'N3MZNBYTAGUD07prC537'], <_tsLib1.Qb8m0g object at 0x0000000009A8FFC8>), {})
('[####]ae0tzqg ra:', '8608')

总结

序列号:

# F 开头
# 9  商业版
# 最后2字节 sum([:-2])%0x100
# 举例: F9807EBCBF2B6F4A0439

验证码:

20字节,(24字节:xxxx-xxxx-xxxx-xxxx-xxxx)

所需数据:    
mssuwqd  			     (sink文件相关,未深入跟踪,??sink文件应该是启动后生成)
_tsLib1.Ufs8dql.gcy8crd()(机器id,)
ra                        (随机数,验证码验证相关)

ps

这是离线版,在线版不可以!

image-20260206220403022

验证码是绑定机器的,并且单次验证有效!

image-20260206220603036

image-20260206220617235

其实数据保存到了C:\Users\xxxx\AppData\Roaming\TextSeek_off\config.bin,

完全可以通过生成该文件实现注册,自行探索吧!

posted @ 2026-02-07 01:20  DirWangK  阅读(739)  评论(0)    收藏  举报