欢迎光临
我们一直在努力

【绷住】GhostTrack 技术拆解:公开信息查询工具的原理、精度边界与 MATLAB 实现

GhostTrack 技术拆解:公开信息查询工具的原理、精度边界与 MATLAB 实现

最近在研究开源 OSINT 工具的实现方式,GhostTrack 是一个比较典型的入门项目。它把几个公开数据查询功能整合到一个命令行菜单里,代码量不大,但涉及号码解析、IP 地理定位、HTTP 状态检测几个有意思的技术点。这篇文章从实现原理出发,分析每个模块的工作机制和能力边界,最后用 MATLAB 复现一遍。

说明:本文仅用于技术学习,所有查询仅限授权测试或查询自己的公开信息。

想到多年以前看到的文章

浅谈信息泄露 个人信息保护

一、GhostTrack 是什么

在这里插入图片描述 GhostTrack 是一个 Python 命令行工具,菜单包含四个功能:

  • IP 信息查询
  • 显示本机公网 IP
  • 手机号元数据解析
  • 用户名公开账号存在性检测

依赖只有两个:

requests
phonenumbers

安装:

git clone https://github.com/HunxByts/GhostTrack.git
cd GhostTrack
pip3 install -r requirements.txt
python3 GhostTR.py

它的数据来源都是公开接口或本地规则库,不涉及任何非公开数据。代码结构清晰,适合用来理解 OSINT 工具的基本实现思路。

模块依赖数据源核心原理
IP 信息查询 requests ipwho.is 公开 IP 地理定位 API
本机公网 IP requests api.ipify.org 获取出口 IP
手机号解析 phonenumbers Google libphonenumber 本地号码元数据规则引擎
用户名检测 requests 各平台公开页面 HTTP 状态码检测

二、架构速读:菜单与选项分发

GhostTR.py 的核心是一个 options 列表:

options = [
{'num': 1, 'text': 'IP Tracker', 'func': IP_Track},
{'num': 2, 'text': 'Show Your IP', 'func': showIP},
{'num': 3, 'text': 'Phone Number Tracker', 'func': phoneGW},
{'num': 4, 'text': 'Username Tracker', 'func': TrackLu},
{'num': 0, 'text': 'Exit', 'func': exit}
]

call_option 根据输入的数字查找对应函数并调用。execute_option 负责异常处理。

有个装饰器 is_option:

def is_option(func):
def wrapper(*args, **kwargs):
run_banner()
func(*args, **kwargs)
return wrapper

它让每个功能函数执行前先打印 banner。这个装饰器没有处理 functools.wraps,属于教学级写法。

整个工具没有超时设置、没有重试机制、没有并发。网络请求一旦卡住,程序就卡住。这是它和成熟工具的第一个差距。

三、手机号解析:libphonenumber 的规则引擎

这是 GhostTrack 中技术含量最高的模块。它用的 phonenumbers 是 Google libphonenumber 的 Python 移植版。

3.1 号码解析不是正则匹配

很多人以为手机号解析就是正则匹配,其实不是。libphonenumber 的核心是一套基于元数据的规则引擎。

号码解析流程大致如下:

  • 规范化:去掉空格、连字符、括号。
  • 确定国家代码:如果号码以 + 开头,直接按国家代码匹配;否则用 default_region 补全。
  • 剥离国内前缀:比如很多国家的国内长途前缀 0,需要去掉。
  • 匹配国内目的地代码:根据国家元数据中的规则,尝试匹配前缀。
  • 验证号码:用元数据中的正则模式验证号码是否合法。
  • 元数据编译成 protobuf 二进制格式存储。PhoneNumberMetadata.xml 定义了各国号码规则,构建时转成 .proto 二进制文件,运行时直接加载到内存。每个国家的元数据包含:

    • generalDesc:一般号码模式
    • fixedLine、mobile、tollFree、premiumRate、voip 等:各号码类型的正则模式
    • possibleLengths:可能的号码长度集合
    • nationalPrefix:国内前缀规则
    • carrierData:运营商前缀映射(部分国家有)

    这套设计不依赖在线查询,完全基于本地元数据做规则判断。元数据文件体积很小,但覆盖了全球 200 多个国家和地区的号码规则。

    3.2 isValidNumber 和 isPossibleNumber 的区别

    GhostTrack 调用了两个验证函数:

    is_valid_number = phonenumbers.is_valid_number(parsed_number)
    is_possible_number = phonenumbers.is_possible_number(parsed_number)

    • isPossibleNumber:只检查号码长度是否在 possibleLengths 集合中。它很快,但可能误判。号码长度对但前缀不对,也会返回 True。
    • isValidNumber:遍历所有号码类型,检查号码是否匹配某一类型的 nationalNumberPattern。更严格,也更慢。

    所以 isPossibleNumber 为 True 不代表号码真实存在,只代表“长度看起来像”。isValidNumber 为 True 才说明号码符合某个国家的号码规则。

    3.3 运营商查询:前缀映射的局限

    carrier.name_for_number() 的工作原理是号码前缀映射。libphonenumber 维护了一份号段与运营商的映射表,通过号码前缀匹配运营商。

    但这里有个重要限制,源码注释写得很清楚:

    “返回的是号码最初分配时的运营商名称,但如果该国支持携号转网,号码可能已经不属于返回的运营商了。”

    也就是说,携号转网后运营商会查错。此外,getNameForValidNumber 方法有个前提:此方法假定传入号码的有效性已经过检查,且号码适合进行运营商查询。固定电话没有运营商信息,查询结果为空。

    3.4 地区与时区:行政维度而非实时位置

    geocoder.description_for_number() 返回的是号码注册地对应的行政区划名称,不是号码持有者的当前位置。

    timezone.time_zones_for_number() 返回该号码可能所在的时区,对于跨时区的大国,可能返回多个时区。

    所以手机号模块给的是号码的元数据画像——归属地、运营商、时区、号码类型。它不知道机主是谁,也不知道机主在哪。

    3.5 默认区域问题

    GhostTrack 把 default_region 硬编码为 "ID"(印度尼西亚):

    default_region = "ID"

    这意味着不带 + 国家代码的号码会被当作印尼号码解析。查中国号码时,输入 +86138… 可以正确解析;输入 138… 就会按印尼规则解析,大概率失败。实际使用时建议改成 "CN"。

    四、IP 信息查询:公开 API 的字段逻辑与精度边界

    4.1 数据来源:ipwho.is

    GhostTrack 调用:

    req_api = requests.get(f"http://ipwho.is/{ip}")
    ip_data = json.loads(req_api.text)

    ipwho.is 是一个免费的 IP 地理定位 API,无需 API Key,免费端点限制每天约 1000 次请求,超限返回 HTTP 429。

    返回的 JSON 包含多个数据类别:

    类别关键字段
    Geolocation 国家、地区、城市、经纬度、accuracy_radius
    Connection ASN 编号、组织名、ISP、域名、连接类型
    Timezone IANA 时区名、UTC 偏移、当前时间
    Security isVpn、isTor、isThreat

    accuracy_radius 是 API 给出的定位精度估计(单位:公里)。但 GhostTrack 的代码里完全没有使用这个字段,直接输出了经纬度。这意味着用户看到一个坐标时,不知道它可能偏离几十甚至几百公里。

    security 类别中的 isVpn、isTor 在免费端点中不可用,Pro 端点才提供。所以 GhostTrack 输出的 IP 信息缺少一个关键判断维度:查询对象是否在使用代理。

    4.2 IP 定位的精度边界

    IP 地理定位本质上是推断,不是精确测量。它的原理是:

  • RIR 注册信息:五大区域互联网注册机构维护 IP 块分配记录。
  • BGP 路由公告:确定某个 IP 前缀属于哪个 ASN。
  • 地理定位数据库:MaxMind、DB-IP 等将 ASN/前缀映射到地理位置。
  • 用户提交数据:部分数据库依赖用户提交的 Wi-Fi 热点位置来细化城市级定位。
  • 但“注册地”和“实际使用地”经常不一致。实际精度数据:

    • 国家级别:固定宽带网络中可以超过 99%
    • 省级/州级:大约 55%-80%
    • 城市级:可能低至 20%-75%,移动网络中更差
    • 移动网络的定位误差比固定宽带差 10 倍以上

    IP 地理定位不可能 100% 准确。使用代理、加密隧道或共享出口都会让定位失效。

    所以 IP 查询最可靠的输出是国家级别的归属判断,城市和经纬度只能作为粗略参考。

    五、用户名检测:HTTP 200 的盲区

    5.1 工作原理

    GhostTrack 的用户名检测方式很简单:把用户名拼接到各平台的 URL 模板中,发 GET 请求,如果返回 HTTP 200 就认为账号存在。

    response = requests.get(url)
    if response.status_code == 200:
    results[site['name']] = url
    else:
    results[site['name']] = "Username not found"

    这种方式的误报率很高。很多平台对不存在的用户也返回 200,只是带一个“该账号不存在”的提示页面。

    5.2 误报来源

    具体来说,误报来源包括:

  • soft-404:平台对不存在的用户返回 200 而不是 404,页面上显示提示。
  • WAF 拦截:部分平台部署了 Web 应用防火墙,将拦截响应也返回为 200。
  • 设计差异:有些平台不做 RESTful 设计,不存在的用户也返回 200。
  • 封禁/删除账号:账号已封禁或删除,但页面仍返回 200。
  • 重定向:有些平台把不存在的用户重定向到首页,首页返回 200。
  • 5.3 更好的做法:多策略检测

    对比 Sherlock,它的检测策略更精细。Sherlock 用一个 JSON 数据库定义每个平台的检测规则,支持三种策略:

    • status_code:简单的状态码判断
    • message:检查响应体中是否包含错误提示文本
    • redirect:分析重定向行为

    配置文件长这样:

    {
    "ExampleSocial": {
    "errorType": "message",
    "errorMsg": "The page you're looking for isn't here",
    "url": "https://examplesocial.com/{}",
    "username_claimed": "blue",
    "username_unclaimed": "noonewouldeverusethisname7"
    }
    }

    其中 username_claimed 和 username_unclaimed 是测试用例——用已知存在和不存在的用户名验证检测逻辑是否有效。当平台改版错误页面时,这个验证机制能及时发现问题。

    GhostTrack 只用了最简单的 status_code 策略,没有错误消息检测,没有重定向分析,没有测试用例验证。所以它的检测结果只能作为线索,必须人工验证。

    5.4 并发与效率

    Sherlock 用 concurrent.futures.ThreadPoolExecutor 实现并行请求,400+ 平台可以在 60 秒内完成检测。GhostTrack 是纯同步循环,速度慢,而且没有超时设置。

    如果要做批量检测,至少需要:设置 timeout、使用 Session 复用连接、引入线程池或异步请求、对 429 做退避重试。

    六、MATLAB 复现

    6.1 复现策略

    Python 中的 requests 在 MATLAB 中对应 webread 或 matlab.net.http 包。phonenumbers 在 MATLAB 中没有对应库,但 MATLAB 内置 JVM,可以直接调用 Google libphonenumber 的 Java 库。

    准备 libphonenumber jar:

  • 下载 libphonenumber-8.x.x.jar
  • 放到 lib/ 目录
  • 在 MATLAB 命令行先运行:
  • javaaddpath('lib/libphonenumber-8.13.0.jar');

    注意:javaaddpath 必须在函数外部先执行,否则函数内部的 import 会报“无法找到或导入”的错误。更稳妥的做法是不用 import,直接使用完全限定类名。

    6.2 完整代码

    保存以下代码为 GhostTR_MATLAB.m。代码在 Python 版基础上增加了 accuracy_radius 字段的显示,用户名检测改为 200 + 内容双重确认,并改用完全限定类名避免导入问题。

    function GhostTR_MATLAB()
    % GhostTrack MATLAB 复现版
    % 仅用于学习公开信息查询原理
    clc;
    while true
    fprintf('\\n===== GhostTrack MATLAB =====\\n');
    fprintf('[1] IP Info\\n');
    fprintf('[2] Show My IP\\n');
    fprintf('[3] Phone Number Info\\n');
    fprintf('[4] Username Availability\\n');
    fprintf('[0] Exit\\n');
    opt = input('Select Option: ');
    if isempty(opt), continue; end
    switch opt
    case 1, queryIPInfo();
    case 2, showMyIP();
    case 3, parsePhoneNumber();
    case 4, checkUsernameAvailability();
    case 0, fprintf('Exit\\n'); break;
    otherwise, fprintf('Option not found\\n');
    end
    input('Press enter to continue', 's');
    clc;
    end
    end

    function s = toStr(x)
    if ischar(x) || isstring(x)
    s = char(x);
    elseif iscell(x)
    s = strjoin(cellfun(@toStr, x, 'UniformOutput', false), ', ');
    elseif isnumeric(x) || islogical(x)
    s = num2str(x);
    else
    try
    s = char(x.toString());
    catch
    s = '<unprintable>';
    end
    end
    end

    function showMyIP()
    try
    ip = webread('https://api.ipify.org/');
    fprintf('\\n===== My Public IP =====\\n');
    fprintf('IP Address : %s\\n', toStr(ip));
    catch e
    fprintf('Error: %s\\n', e.message);
    end
    end

    function queryIPInfo()
    ip = input('Enter IP: ', 's');
    try
    opts = weboptions('Timeout', 15, 'ContentType', 'json');
    data = webread(sprintf('http://ipwho.is/%s', ip), opts);

    fprintf('\\n===== IP Information =====\\n');
    fprintf('IP : %s\\n', ip);
    if isfield(data,'type'), fprintf('Type : %s\\n', toStr(data.type)); end
    if isfield(data,'country'), fprintf('Country : %s\\n', toStr(data.country)); end
    if isfield(data,'country_code'), fprintf('Country Code : %s\\n', toStr(data.country_code)); end
    if isfield(data,'city'), fprintf('City : %s\\n', toStr(data.city)); end
    if isfield(data,'continent'), fprintf('Continent : %s\\n', toStr(data.continent)); end
    if isfield(data,'region'), fprintf('Region : %s\\n', toStr(data.region)); end
    if isfield(data,'region_code'), fprintf('Region Code : %s\\n', toStr(data.region_code)); end
    if isfield(data,'latitude'), fprintf('Latitude : %s\\n', toStr(data.latitude)); end
    if isfield(data,'longitude'), fprintf('Longitude : %s\\n', toStr(data.longitude)); end
    if isfield(data,'accuracy_radius')
    fprintf('Accuracy Radius : %s km\\n', toStr(data.accuracy_radius));
    end
    if isfield(data,'latitude') && isfield(data,'longitude')
    lat = data.latitude; lon = data.longitude;
    fprintf('Maps : https://www.google.com/maps/@%f,%f,8z\\n', lat, lon);
    end
    if isfield(data,'is_eu'), fprintf('EU : %s\\n', toStr(data.is_eu)); end
    if isfield(data,'postal'), fprintf('Postal : %s\\n', toStr(data.postal)); end
    if isfield(data,'calling_code'), fprintf('Calling Code : %s\\n', toStr(data.calling_code)); end
    if isfield(data,'capital'), fprintf('Capital : %s\\n', toStr(data.capital)); end
    if isfield(data,'borders'), fprintf('Borders : %s\\n', toStr(data.borders)); end
    if isfield(data,'flag') && isfield(data.flag,'emoji')
    fprintf('Country Flag : %s\\n', toStr(data.flag.emoji));
    end
    if isfield(data,'connection')
    c = data.connection;
    if isfield(c,'asn'), fprintf('ASN : %s\\n', toStr(c.asn)); end
    if isfield(c,'org'), fprintf('ORG : %s\\n', toStr(c.org)); end
    if isfield(c,'isp'), fprintf('ISP : %s\\n', toStr(c.isp)); end
    if isfield(c,'domain'), fprintf('Domain : %s\\n', toStr(c.domain)); end
    end
    if isfield(data,'timezone')
    tz = data.timezone;
    if isfield(tz,'id'), fprintf('Timezone ID : %s\\n', toStr(tz.id)); end
    if isfield(tz,'abbr'), fprintf('ABBR : %s\\n', toStr(tz.abbr)); end
    if isfield(tz,'is_dst'), fprintf('DST : %s\\n', toStr(tz.is_dst)); end
    if isfield(tz,'offset'), fprintf('Offset : %s\\n', toStr(tz.offset)); end
    if isfield(tz,'utc'), fprintf('UTC : %s\\n', toStr(tz.utc)); end
    if isfield(tz,'current_time'), fprintf('Current Time : %s\\n', toStr(tz.current_time)); end
    end
    catch e
    fprintf('Error: %s\\n', e.message);
    end
    end

    function parsePhoneNumber()
    userPhone = input('Enter phone number (e.g. +8613800138000): ', 's');
    defaultRegion = 'CN';
    try
    phoneUtil = com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance();
    num = phoneUtil.parse(userPhone, defaultRegion);

    regionCode = char(phoneUtil.getRegionCodeForNumber(num));
    try
    carrierName = char(phoneUtil.getCarrierNameForNumber(num));
    catch
    carrierName = 'N/A (可能为固话或元数据缺失)';
    end

    geocoder = com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder.getInstance();
    location = char(geocoder.getDescriptionForNumber(num, java.util.Locale('zh','CN')));

    tzSet = phoneUtil.getTimeZonesForNumber(num);
    tzCell = cell(tzSet.toArray());
    timezoneF = strjoin(cellfun(@char, tzCell, 'UniformOutput', false), ', ');

    isValid = phoneUtil.isValidNumber(num);
    intl = char(phoneUtil.format(num, …
    com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
    e164 = char(phoneUtil.format(num, …
    com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164));
    mobile = char(phoneUtil.formatNumberForMobileDialing(num, defaultRegion, true));

    numberType = phoneUtil.getNumberType(num);
    typeStr = char(numberType.toString());

    fprintf('\\n===== Phone Number Information =====\\n');
    fprintf('Location : %s\\n', location);
    fprintf('Region Code : %s\\n', regionCode);
    fprintf('Timezone : %s\\n', timezoneF);
    fprintf('Operator : %s\\n', carrierName);
    fprintf('Valid number : %d\\n', isValid);
    fprintf('International format : %s\\n', intl);
    fprintf('Mobile format : %s\\n', mobile);
    fprintf('E.164 format : %s\\n', e164);
    fprintf('Country code : %d\\n', num.getCountryCode());
    fprintf('National number : %d\\n', num.getNationalNumber());

    switch typeStr
    case 'MOBILE'
    fprintf('Type : Mobile number\\n');
    case 'FIXED_LINE'
    fprintf('Type : Fixed-line number\\n');
    case 'FIXED_LINE_OR_MOBILE'
    fprintf('Type : Fixed-line or mobile\\n');
    case 'VOIP'
    fprintf('Type : VOIP number\\n');
    otherwise
    fprintf('Type : %s\\n', typeStr);
    end
    catch e
    fprintf('Error: %s\\n', e.message);
    fprintf('请确认已运行 javaaddpath(''lib/libphonenumber-8.x.x.jar''),且号码格式正确。\\n');
    end
    end

    function checkUsernameAvailability()
    username = input('Enter Username: ', 's');
    sites = {
    'GitHub', 'https://www.github.com/%s';
    'Telegram', 'https://www.telegram.me/%s';
    'TikTok', 'https://www.tiktok.com/@%s';
    'Instagram', 'https://www.instagram.com/%s';
    'LinkedIn', 'https://www.linkedin.com/in/%s';
    'Medium', 'https://www.medium.com/@%s';
    };
    fprintf('\\n===== Username Availability =====\\n');
    for i = 1:size(sites,1)
    name = sites{i,1};
    url = sprintf(sites{i,2}, username);
    [found, status] = checkAccountURL(url);
    if found
    fprintf('[+] %-10s : EXISTS (HTTP %d) %s\\n', name, status, url);
    else
    fprintf('[-] %-10s : NOT FOUND (HTTP %d)\\n', name, status);
    end
    end
    end

    function [found, statusCode] = checkAccountURL(url)
    % 200 + 内容双重检测,减少 soft-404 误报
    try
    import matlab.net.http.*;
    import matlab.net.URI;
    req = RequestMessage('GET');
    req.Header = HeaderField('User-Agent', 'Mozilla/5.0');
    opts = HTTPOptions('ConnectTimeout', 10, 'Timeout', 10);
    resp = req.send(URI(url), opts);
    statusCode = double(resp.StatusCode);
    found = false;
    if statusCode == 200
    body = char(resp.Body.Data);
    errorPatterns = {'not found', 'doesn''t exist', …
    'user not found', 'page not found', …
    'couldn''t find', 'no user'};
    found = true;
    for p = 1:length(errorPatterns)
    if contains(lower(body), lower(errorPatterns{p}))
    found = false;
    break;
    end
    end
    end
    catch
    found = false;
    statusCode = –1;
    end
    end

    运行方式:

    javaaddpath('lib/libphonenumber-8.13.0.jar');
    GhostTR_MATLAB

    七、效率与局限

    GhostTrack 的 IP 查询走的是同步 requests.get,没有超时设置和重试机制。ipwho.is 免费端点限制约每秒 1 次请求,频繁调用会触发 429。webread 和 Python 的 requests 都不支持连接复用,如果要做批量查询需要引入连接池或第三方工具。

    Sherlock 在架构上做了优化:用 concurrent.futures.ThreadPoolExecutor 实现并行请求,400+ 平台可以在 60 秒内完成检测。GhostTrack 如果能加一个并发层,实用性会提升不少。

    但 GhostTrack 的价值不在“能不能实战”。它的代码结构清晰,适合作为理解 OSINT 基本原理的起点。不过正因为简单,它的局限性也很明显:

    • IP 查询不显示 accuracy_radius,用户不知道精度。
    • 用户名检测不做 soft-404 验证,误报率高。
    • 手机号模块默认区域是 ID,查中国号码容易失败。
    • 没有数据来源标注和置信度评估。
    • 没有超时、重试、并发。

    八、合规提示

    最后强调几点:

    • 本文讨论的 GhostTrack 只查询公开信息,不涉及任何非公开数据。
    • 所有查询仅限授权测试或查询自己的公开信息。
    • 未经授权对他人进行信息收集可能违反法律法规和平台服务条款。
    • 工具本身是中性的,使用方式决定其性质。

    MATLAB 复现也只复现公开查询,请勿越界。

    赞(0)
    未经允许不得转载:171主机测评 » 【绷住】GhostTrack 技术拆解:公开信息查询工具的原理、精度边界与 MATLAB 实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址