PT作弊与反作弊

Rhilip 2020-03-10 PM 1644℃ 7条

在去年年底(2019年12月),我曾经公开了一个Github仓库 Rhilip/awesome_ptcheater 收集了绝大多数用于PT作弊的软件,并谋划着这篇文章。但是由于原仓库使用git-lfs的方式占用了并不多的1G空间,所以于前段时间重新整理仓库,并重建仓库以及着手这篇文章

所以此文就主要介绍这些PT作弊的软件以及比较常用的反作弊思想。

特请注意:本文不提倡在任何PT站点作弊!毕竟只要怀疑,查起来非常容易2333

部分名词解释:

  • fake seeding: 由于某peer本地文件错误,导致从该peer从获得的区块文件hash错误,该peer称为fake seeding。

PT作弊方法及作弊软件

  • 同机/内网 多客户端/多端口

顾名思义,就是在同一台及其上通过多开BT软件,利用本地或者内部高速网络进行上传和下载。最低级的作弊方法,实际消耗了对应的带宽和存储空间,只是没有FAKE,且将流量传给了一个未被private tracker知道的peer。

  • 基于直接伪装announce思想实现

由于BT协议是完全信任的协议,所以在服务器端就无法通过字符串校验等形式确认用户是否存在伪造做种信息。如以下请求(使用python requests简单示例),如果对应站点存在该info_hash的种子,这以下代码执行的结果将会在1分钟内为你的账户增加1G的上传流量。

import time
import requests

# 基本信息 (在整个announce阶段都不会变化)
announce_url = 'http://nexusphp.local/tracker/announce'
info_hash = '%22%31%b1%de%32%2f%4d%0e%43%fe%14%79%d1%e6%68%f3%89%02%6b%23'
peer_id = '-UT2210-%16bh%b6K3%ca%cc%ce%c7T%96'
key = 'D22EF0E5'
user_agent = 'uTorrent/2040(22967)'

params = {
    # 确定种子,peer身份,开放端口
    'info_hash' : info_hash, 'peer_id' : peer_id, 'port' : 58140,
    # 客户端上传、下载、剩余、出错字节
    'uploaded' : 80, 'downloaded' : 0, 'left' : 10923445644, 'corrupt' : 0,
    'key' : key, 'event' : '', 'numwant': 200, 'compact' : 1, 'no_peer_id' : 1,
    # ... 其他可能还有一些字段
}

# 注意这里使用requests自带的params方法,实际多数btclient使用的都是字符串拼接方法
r = requests.get(announce_url, params=params, headers = {'User-Agent' : user_agent})  # 第一次请求

time.sleep(60)  # 休眠一段时间

params['uploaded'] = params['uploaded'] + 1 * pow(1024,3)  # 伪装增加1G上传,其他不变
r = requests.get(r'%s',params=params, headers = {'User-Agent' : user_agent})  # 第二次请求

基于直接伪装announce思想实现的PT作弊软件较多,如国内早些有点名气的ptliarptliar2,以及RatioMaster系列、mRatio等都是这类的代表。关于这些软件的简单介绍可以见更早大佬们的文章:

PT流量作弊工具之mRatio
PT流量作弊工具之PTLiarPT流量作弊工具之PTLiar2
PTMaster,新的PT流量作弊工具?

如果你稍对python有了解,那么就可以发现 PTLiarPTLiar2 此类软件的实现实质就是对上述示例代码的进一步包装,并增加额外的请求、相应处理。以RatioMaster Plus为例(这款软件是),我们可以看到打开该软件后,可以对UA头信息进行任意的伪装,之后只需要添加种子并启动,程序会自动进行作弊行为。

image-20200309193011514.png

此外可以对单种做高级设置,设定更为相似正常客户端的行为。

image-20200309193749336.png

然而这些软件基本都很稳定,也就是说基本都没有更新了,软件的一些特点也比较明显,容易被管理员直接发现。

而最近发现新出现的 Joal-Server ,相比上面提到的几款在一些参数FAKE上更具有优势,也比较接近,有兴趣可以自己在小站进行测试。

  • 基于中间人代理方式实现

上面说的直接announce还需要对进行客户端模拟,那么我直接使用中间人代理是否可行?毕竟announce请求的实质是一次web请求, 同样存在 “HTTP 请求 -> HTTP 响应”的过程,也就是说有 http_connect、request、response事件。以python的mitmproxy 做示例如下:

import re
from mitmproxy import ctx
import mitmproxy.http

class DoubleUpload:

    def request(self, flow: mitmproxy.http.HTTPFlow):
        """
        The full HTTP request has been read.
        """
        path = flow.request.path
        
        upload_search = re.search(r'uploaded=(\d+)',path)
        if upload_search:
            old_upload = int(upload_search.group(1)) 
            fix_upload = int(old_upload * 2)
            ctx.log.info("Old uploaded is %s, fix it to %s" % (old_upload, fix_upload))
            flow.request.path = path.replace(upload_search.group(0),'uploaded=%s' % (fix_upload,))


addons = [
    DoubleUpload()
]

该脚本将会充当中间人的角色,将每次汇报的上传量变成原先的两倍。使用mitmdump或mitmproxy命令启动后,将BT软件代理地址改为127.0.0.1:8080。通过对请求字段进行分析,我们可以看到通过mitmproxy 实际发送给tracker的上传数量确实如我们预期变成了原先的两倍了。

image-20200309203536890.png

基于这种中间人代理实现的有 UNI-LeechRatio FakerGiveMeTorrentGreedyTorrentTorrent Proxy等,有兴趣可以自己试试。下图展示了Ratio Faker的工作界面。从上面可以看出可以将汇报的uploaded值改为实际upload、download的相关倍数,开启软件后将bittorrent的代理设置改为对应端口127.0.0.1:8282,即可作弊,且主要有两个高级工作模式:

1. 伪装成seeder,且只汇报upload,不汇报download。
 2. 不汇报upload以及download数据。

image-20200309213859178.png

基于中间人代理模式的作弊软件,相对只进行announce FAKE的软件相对更难以识别。因为他本身就是正常的客户端行为,只是将announce请求字段进行修改。

  • 直接修改客户端源码,使其汇报上传下载不符合正常情况

此条和上一条基于中间人代理在一些表现上相同,但不如中间人代理灵活,且修改客户端基本都算严重作弊。故不叙述。

  • 基于云存储挂载方式实现

关于云挂载做种算不算作弊,不同站点的sysop或者admin态度不同。有些觉得算作弊,查到就ban号;有些觉得不算作弊,且在一定程度上能够有速度,使部分种子保持活种状态。因为本人认为这算作弊行为(骗取seed_bonus,恶意限速,且十分容易出现fake seeding的情况),所以在此列出。

一般都是找一个无限空间的Google云盘(部分使用OneDrive盘),然后使用rclone mount挂载到本地目录(RaiDrive也可以)。做种软件添加种子直接跳校验辅种(注意,一般不直接下载到云挂载目录),且通过限低速和mount cache的形式防止爆API请求。

PT反作弊思路

  • 核验单种子总上传以及总下载数据

PT网络(BT网络)其实是一个对等网络,在单一种子上所有peer的总上传和总下载数据一定能核验上。但实际情况下由于汇报间隔以及可能存在fake seeding,核算总上传和总下载实际并不可能均为正好的1。

将某站的snatched表导出,分析其单种总上传和总下载及比值,共计有效个案数48752。完整统计频率数据如下表:

统计项数值统计项数值
个案数(有效)48752峰度41886.118
平均值1.0646峰度标准误差.022
平均值标准误差.02133范围1001.58
中位数1.0080最小值.00
众数1.00最大值1001.58
标准 偏差4.70902百分位数(25)1.0005
方差22.175百分位数(50)1.0080
偏度199.025百分位数(75)1.0279
偏度标准误差.011

通过判读描述信息,我们可以看出数据分布存在右偏现象,且分布的峰态十分陡峭。然而很有意思的是,对样本以及自然对数转换后的样本进行Kolmogorov-Smirnov正态检验,结果均为拒绝原假设(笑,很不符合预期)。

原假设检验显著性决策
ratio 的分布为正态分布,平均值为 1.06,标准差为 4.70902。单样本柯尔莫戈洛夫-斯米诺夫检验.000a拒绝原假设。
lgratio 的分布为正态分布,平均值为 .01,标准差为 .23705。单样本柯尔莫戈洛夫-斯米诺夫检验.000a拒绝原假设。

然而从P-P图上看,在图左右侧出现明显的符合情况,但图主体部分满足正态分布的要求。故以95%置信区间进行单样本正态统计(后验),得到的置信区间为 1.0228-1.1064。

综合考虑后,比较建议选择1-1.1作为实际判断过程中选取的置信范围,对远超该范围的种子进行检查。(这个结论不用分析也能知道23333)

OUTPUT.png

  • 流量异常测试(上传速度限制)

很老套,目前看作用并不大,但是因为NPHP以及其他一些PT架构中使用,故做介绍。如下为NPHP默认的规则:

  • 上传量超过1GB,而且上传速度超过100 MB/s,无疑是作弊(会立刻禁用)
  • 上传量超过1GB,而且上传速度超过10 MB/s,可能是作弊(列入怀疑名单)
  • 上传量超过1GB,而且上传速度超过1 MB/s,而且此时下载人数小于2人,可能是作弊(列入怀疑名单)
  • 上传量超过10MB,而且上传速度超过100 KB/s,而且此时没有人在下载,可能是作弊(列入怀疑名单)

NPHP使用100MB/s限速ban有其历史背景,毕竟NPHP开发年代,网速并没有如今这么快,故设定100 MB/s作为超速线,并添加一些额外的判断,非常简陋。往往只需要低速长时间上传就可以绕过该流量异常测试。且往往需要管理者根据自身经验进行额外判断。(曾经某admin在线下聚会时,说他瞄一眼NPHP的作弊列表的信息,就能看出来那些人做没作弊)

  • 做种、作弊软件特征

一些比较明显的做种、作弊软件特征一般也会成为辅助管理员进行判断的标准。例如我在 Pt站点禁用Aria2客户端方法分析 一文中就是用Aria2其明显与正常BT软件不同两个特征进行判断。此外零零散散的一些特征有:

1.  uTorrent 2.x 版本不能添加1T以上种子。
 2.  作弊软件常用伪装版本号以及请求字段中的KEY信息等,例如 ptliar2 默认伪装的UA就是 uTorrent/2210(25130)。
 3. ......... (怎么可能全部告诉你)
  • 基于peer间协议

这个反作弊成本有点高,类似BT网络的蜜罐技术。通常我们知道,一般垃圾的作弊软件只管与Tracker之间annonce,而没有关注过peer之间的TCP请求。一般可以采用以下两种方式:

  1. 公网蜜罐创建socket server,tracker往peerlist中插入蜜罐信息,peer应与蜜罐进行连接测试。
  2. 蜜罐通过tracker主动获取peerlist,并对每一个peer(应具备公网可连接性)进行连接测试。

此处对第二种方式进行示例,

import errno
import socket
import logging
import hashlib
import binascii
import bencode

from struct import pack, unpack

# HandShake - String identifier of the protocol for BitTorrent V1
HANDSHAKE_PSTR_V1 = b"BitTorrent protocol"
HANDSHAKE_PSTR_LEN = len(HANDSHAKE_PSTR_V1)


class Message:
    def to_bytes(self):
        raise NotImplementedError()

    @classmethod
    def from_bytes(cls, payload):
        raise NotImplementedError()


class Handshake(Message):
    """
        Handshake = <pstrlen><pstr><reserved><info_hash><peer_id>
            - pstrlen = length of pstr (1 byte)
            - pstr = string identifier of the protocol: "BitTorrent protocol" (19 bytes)
            - reserved = 8 reserved bytes indicating extensions to the protocol (8 bytes)
            - info_hash = hash of the value of the 'info' key of the torrent file (20 bytes)
            - peer_id = unique identifier of the Peer (20 bytes)

        Total length = payload length = 49 + len(pstr) = 68 bytes (for BitTorrent v1)
    """
    payload_length = 68
    total_length = payload_length

    def __init__(self, info_hash, peer_id=b'-ZZ0007-000000000000'):
        super(Handshake, self).__init__()

        assert len(info_hash) == 20
        assert len(peer_id) < 255
        self.peer_id = peer_id
        self.info_hash = info_hash

    def to_bytes(self):
        reserved = b'\x00' * 8
        handshake = pack(">B{}s8s20s20s".format(HANDSHAKE_PSTR_LEN),
                         HANDSHAKE_PSTR_LEN,
                         HANDSHAKE_PSTR_V1,
                         reserved,
                         self.info_hash,
                         self.peer_id)

        return handshake

    @classmethod
    def from_bytes(cls, payload):
        pstrlen, = unpack(">B", payload[:1])
        pstr, reserved, info_hash, peer_id = unpack(">{}s8s20s20s".format(pstrlen), payload[1:cls.total_length])

        if pstr != HANDSHAKE_PSTR_V1:
            raise ValueError("Invalid string identifier of the protocol")

        return Handshake(info_hash, peer_id)


def read_from_socket(sock):
    data = b''

    while True:
        try:
            buff = sock.recv(4096)
            if len(buff) <= 0:
                break

            data += buff
        except socket.error as e:
            err = e.args[0]
            if err != errno.EAGAIN or err != errno.EWOULDBLOCK:
                logging.debug("Wrong errno {}".format(err))
            break
        except Exception:
            logging.exception("Recv failed")
            break

    return data


if __name__ == '__main__':
    # 打开一个info_hash为 b'91fb97619ef887d439a2142a2f9530b080cfbfd0' 的种子
    torrent_file = r'D:\Downloads\1.torrent'

    with open (torrent_file,'rb') as f:
        torrent_dict =  bencode.bread(f)

    raw_info_hash = bencode.bencode(torrent_dict['info'])
    info_hash = hashlib.sha1(raw_info_hash).digest()
    hex_info_hash = binascii.hexlify(info_hash)

    assert b'91fb97619ef887d439a2142a2f9530b080cfbfd0' == hex_info_hash

    # 通过请求tracker获得peer_list,这里直接假定获得的ip+port以及peer_id prefix
    peer_ip = '127.0.0.1'
    peer_port = 8299
    peer_id_prefix = b'-qB4210-'

    # 尝试连接peer并获取Handshake信息
    try:
        peer_socket = socket.create_connection((peer_ip,peer_port),timeout=2)  # 尝试连接
        #peer_socket.setblocking(False)
        peer_socket.send(Handshake(info_hash).to_bytes())  # 发送握手信息
        raw_data = read_from_socket(peer_socket)  # 从套接字中获取peer返回的握手信息
        if raw_data == b'':
            raise ValueError('Handshake Failed')

        parsed_handshake = Handshake.from_bytes(raw_data)  # 解析peer返回的握手信息
        if parsed_handshake.peer_id.find(peer_id_prefix) == -1:
            raise ValueError('The Peer Id (%s) from Handshake not what we wan\'t ' % (parsed_handshake.peer_id))

        print('This peer is exist and pass the handshake check')
    except Exception as e:
        print("Failed to Handshake with peer (ip: %s - port: %s - %s)" % (peer_ip, peer_port, e.__str__()))

通过类似脚本,我们可以主动探测一个peer是否运行的BT客户端。如果该用户使用BT软件做种,则通过BT握手检查;如果该用户未使用BT软件做种,或者通过握手检查返回的peer_id非我们需要检查的peer_id,则会报错。且如果你未关闭python运行终端,你还可以从被检测的peer处看到以下蜜罐信息。

image-20200310151954214.png

此外,你还可以在对BT协议有者更深入的理解上,构造request等其他信息来进一步探测peer情况。

非特殊说明,本博所有文章均为博主原创。

评论啦~



已有 7 条评论


  1. 醉渔
    醉渔

    大佬牛批,围观围观

    回复 2020-03-13 07:49
  2. BFDZ
    BFDZ

    围观大佬~

    回复 2020-03-22 13:45
  3. 醉清风
    醉清风

    byr.rhilip.info 大佬,我想知道这个链接不上了,是关了吗 毕业了就想偶尔回byr看一看

    回复 2020-03-22 16:39
    1. Rhilip
      Rhilip 博主

      关掉了,byr反代挺多的,找一下就行。

      回复 2020-03-22 17:29
      1. 醉清风
        醉清风

        不太懂这个,大佬能不能推荐一个?

        回复 2020-03-22 20:59
  4. qwe
    qwe

    R酱,想问下U2的tracker是怎么做到识别IPV6同时识别IPV4的。我发现tracker上报的时候有时候会走IPV4然后第二次又走IPV6,这样就导致报多个客户端下载。但是在U2的客户端显示的是同时有IPV4和IPV6识别为同一个客户端,这样的话站点是怎么计算流量上报的。会不会重复计算?

    回复 2020-05-18 21:52
    1. Rhilip
      Rhilip 博主

      根据peer_id,具体请看ridpt的实现

      回复 2020-05-19 16:21