📅⏱️7 分钟阅读
本篇文章介绍了如何在校园网环境下突破「单账号仅限两台设备」的限制,并实现多账号带宽叠加。作者通过分析锐捷 Web Portal 认证流程,移植登录算法至 OpenWrt,利用

背景

入学新学校,校园网 49元/月 100M ,只能够同时登录两台设备,于是着手破解。

破解后当普通 Wi-Fi 连接即可,无设备数量限制,同时还可以做到多账号网速叠加。

Image

思路

  1. 逆向校园网 Web 登录算法
  2. 将登录算法移植到 OpenWrt 中
  3. 使用 Macvlan 和 MultiWAN 插件实现多账号登录

登录算法逆向

连接校园网,随便输入一个网址就可以跳转到登录界面(这是通过 DNS劫持 实现的)。

通过观察代码发现,这是锐捷网络的 Web 认证,便在网络上找到了相同认证系统的登录算法实现,所以这一步非常的轻松,基本没有自己进行逆向分析。


校园网认证流程分析及自动认证脚本 - Mars-Luke - 博客园
校园网认证流程分析及自动认证脚本 很久之前在处理Linux终端环境下校园网认证登录时所做的工作,理论上适用于所有基于锐捷网络的认证系统 分析过程 未进行校园网认证时curl -v任意网站得到 可见会重定向到认证网址并且附带一些参数 浏览器访问该网址并打开网络日志,输入自己的账号密码测试登录 找到登录
cnblogs.com

OpenWrt

硬路由的选择

因为要使用 OpenWRT 系统,所以路由器需要选择能够刷机的,且自带 Wi-Fi 的硬路由。

这里选择的是 H3C NX30 Pro,二手售价约100元,配置如下:

型号H3C NX30 Pro
CPUMT7981B
闪存128M
内存DDR3 256M

刷机教程

网络上有很多刷机的教程,这里不多赘述。

使用的 uboot 仓库如下:

https://github.com/hanwckf/bl-mt798x

固件的选择

固件选择了自编译 ImmortalWrt 23.05.6,且使用最精简固件的思路,仅保留了 mwan、turboacc、fileassistant、upnp 插件。


编译产物的 config.buildinfo 如下
Plain text
CONFIG_TARGET_mediatek=y
CONFIG_TARGET_mediatek_filogic=y
CONFIG_TARGET_mediatek_filogic_DEVICE_h3c_magic-nx30-pro-nmbm=y
CONFIG_LIBCURL_COOKIES=y
CONFIG_LIBCURL_CRYPTO_AUTH=y
CONFIG_LIBCURL_FILE=y
CONFIG_LIBCURL_FTP=y
CONFIG_LIBCURL_HTTP=y
CONFIG_LIBCURL_NGHTTP2=y
CONFIG_LIBCURL_OPENSSL=y
CONFIG_LIBCURL_PROXY=y
CONFIG_LIBCURL_TFTP=y
CONFIG_LIBCURL_THREADED_RESOLVER=y
CONFIG_LIBCURL_TLS_SRP=y
CONFIG_LIBCURL_UNIX_SOCKETS=y
CONFIG_PACKAGE_bash=y
CONFIG_PACKAGE_curl=y
CONFIG_PACKAGE_htop=y
CONFIG_PACKAGE_ip-tiny=y
CONFIG_PACKAGE_ip6tables-nft=y
CONFIG_PACKAGE_ipset=y
CONFIG_PACKAGE_iptables-mod-conntrack-extra=y
CONFIG_PACKAGE_iptables-mod-ipopt=y
CONFIG_PACKAGE_iptables-nft=y
CONFIG_PACKAGE_iputils-arping=y
CONFIG_PACKAGE_jq=y
CONFIG_PACKAGE_kmod-ip6tables=y
CONFIG_PACKAGE_kmod-ipt-conntrack=y
CONFIG_PACKAGE_kmod-ipt-conntrack-extra=y
CONFIG_PACKAGE_kmod-ipt-core=y
CONFIG_PACKAGE_kmod-ipt-ipopt=y
CONFIG_PACKAGE_kmod-ipt-ipset=y
CONFIG_PACKAGE_kmod-nf-conncount=y
CONFIG_PACKAGE_kmod-nf-ipt=y
CONFIG_PACKAGE_kmod-nf-ipt6=y
CONFIG_PACKAGE_kmod-nft-compat=y
CONFIG_PACKAGE_kmod-nft-fullcone=y
CONFIG_PACKAGE_kmod-tcp-bbr=y
CONFIG_PACKAGE_libblkid=y
CONFIG_PACKAGE_libcap=y
CONFIG_PACKAGE_libcap-ng=y
CONFIG_PACKAGE_libcurl=y
CONFIG_PACKAGE_libipset=y
CONFIG_PACKAGE_libiptext=y
CONFIG_PACKAGE_libiptext-nft=y
CONFIG_PACKAGE_libiptext6=y
CONFIG_PACKAGE_libmount=y
CONFIG_PACKAGE_libncurses=y
CONFIG_PACKAGE_libnghttp2=y
CONFIG_PACKAGE_libreadline=y
CONFIG_PACKAGE_librt=y
CONFIG_PACKAGE_libsmartcols=y
CONFIG_PACKAGE_libstdcpp=y
CONFIG_PACKAGE_libuuid=y
CONFIG_PACKAGE_libuv=y
CONFIG_PACKAGE_libwebsockets-full=y
CONFIG_PACKAGE_libxtables=y
CONFIG_PACKAGE_lsblk=y
CONFIG_PACKAGE_luci-app-fileassistant=y
CONFIG_PACKAGE_luci-app-mwan3=y
CONFIG_PACKAGE_luci-app-turboacc=y
CONFIG_PACKAGE_luci-app-upnp=y
CONFIG_PACKAGE_luci-i18n-mwan3-zh-cn=y
CONFIG_PACKAGE_luci-i18n-turboacc-zh-cn=y
CONFIG_PACKAGE_luci-i18n-upnp-zh-cn=y
CONFIG_PACKAGE_miniupnpd-nftables=y
CONFIG_PACKAGE_mwan3=y
CONFIG_PACKAGE_openssh-client=y
CONFIG_PACKAGE_openssh-keygen=y
CONFIG_PACKAGE_pdnsd-alt=y
CONFIG_PACKAGE_smartmontools=y
CONFIG_PACKAGE_smartmontools-drivedb=y
CONFIG_PACKAGE_terminfo=y
CONFIG_PACKAGE_xtables-nft=y
CONFIG_PACKAGE_zlib=y


通过上面的 buildinfo 编译出来的固件仅仅 15.4MB。

登录算法移植

这里使用 OpenWrt 的 init.d 与 procd 守护校园网登录状态,如果检测到离线则自动尝试登录。

代码如下:

Shell
#!/bin/sh /etc/rc.common
# campus portal login as an init.d service for OpenWrt (with procd monitor)

START=95
STOP=10
USE_PROCD=1

#  配置区 
USER_ID="" # 用户名
PASSWORD="" # 密码
SERVICE="DianXin" # Service 字段,没有可以留空

INTERFACE="eth1" # 网络端口,用于指定 Macvlan 多账号登录

PING_URL="http://baidu.com"
PORTAL_HOST="10.30.252.71" # 记得修改这个为自己学校的登录网址 IP
INDEX_URL="http://$PORTAL_HOST/eportal/index.jsp"
LOGIN_API="http://$PORTAL_HOST/eportal/InterFace.do?method=login"

CURL_UA="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36"
CURL_OPTS="-fsS --connect-timeout 5 --max-time 10 --interface $INTERFACE"

# 监控间隔(秒)
MONITOR_INTERVAL=60
# 断线判定失败后重试次数/间隔
RETRY_MAX=3
RETRY_SLEEP=2

EXTRA_COMMANDS="login sstatus"
EXTRA_HELP="        login   Run a one-shot portal login now
        sstatus  Print current connectivity/portal status"

url_encode() {
    local s="${1}" i c out=""
    for i in $(seq 1 ${#s}); do
        c="$(printf '%s' "$s" | cut -c $i)"
        case "$c" in
            [a-zA-Z0-9.~_-]) out="${out}${c}" ;;
            *) out="${out}$(printf '%%%02X' "'$c")" ;;
        esac
    done
    printf '%s' "$out"
}

is_online() {
    local resp
    resp="$(curl $CURL_OPTS "$PING_URL" -A "$CURL_UA" || true)"
    echo "$resp" | grep -q "http://www\.baidu\.com/" && return 0 || return 1
}

extract_query_string() {
    local resp="$1"
    echo "$resp" \
    | grep -o "top\.self\.location\.href='http://$PORTAL_HOST/eportal/index\.jsp?[^']\+'" \
    | sed "s#.*index.jsp?##; s/'\$//"
}

do_login_once() {
    echo "[campus_portal_eth1] Probing connectivity..."
    if is_online; then
        echo "[campus_portal_eth1] Already online."
        return 0
    fi

    echo "[campus_portal_eth1] Fetching portal redirect page..."
    local resp qs enc1 enc2 post_data login_json
    resp="$(curl $CURL_OPTS "$PING_URL" -A "$CURL_UA" || true)"
    qs="$(extract_query_string "$resp")"
    if [ -z "$qs" ]; then
        echo "[campus_portal_eth1] ERROR: failed to get queryString."
        return 1
    fi
    echo "[campus_portal_eth1] Got queryString (len=${#qs})."

    enc1="$(url_encode "$qs")"
    enc2="$(url_encode "$enc1")"
    post_data="userId=$(url_encode "$USER_ID")&password=$(url_encode "$PASSWORD")&service=$(url_encode "$SERVICE")&queryString=$enc2&operatorPwd=&operatorUserId=&validcode=&passwordEncrypt=false"

    echo "[campus_portal_eth1] Logging in..."
    login_json="$(curl $CURL_OPTS -X POST "$LOGIN_API" \
        -H "User-Agent: $CURL_UA" \
        -H "Referer: $INDEX_URL" \
        -H "Accept: */*" \
        -H "Accept-Encoding: gzip, deflate" \
        -H "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8" \
        -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
        --data "$post_data" || true)"

    echo "$login_json" | grep -q '"result":"success"' && {
        echo "[campus_portal_eth1] Login success."
        return 0
    }
    echo "[campus_portal_eth1] Login failed."
    echo "$login_json" | sed -n 's/.*"message":"\([^"]*\)".*/[campus_portal_eth1] reason: \1/p'
    return 2
}

# procd 管理的常驻监控
_monitor_loop() {
    # 注意:这里不要用 'EOF',让变量在这里就展开
    cat <<EOF
MONITOR_INTERVAL=${MONITOR_INTERVAL}
RETRY_MAX=${RETRY_MAX}
RETRY_SLEEP=${RETRY_SLEEP}
PING_URL="${PING_URL}"
CURL_UA="${CURL_UA}"

is_online() {
    resp="\$(curl -fsS --connect-timeout 5 --max-time 10 --interface $INTERFACE "\$PING_URL" -A "$CURL_UA" || true)"
    echo "\$resp" | grep -q "http://www\\.baidu\\.com/"
}

while true; do
    if is_online; then
        sleep "\$MONITOR_INTERVAL"
        continue
    fi

    echo "[campus_portal_eth1] offline detected, trying to login..."
    tries=1
    rc=1
    while [ "\$tries" -le "\$RETRY_MAX" ]; do
        # 请注意,一定要保证这里的文件名和你的文件名一致。
        /etc/init.d/campus_portal_eth1 login && { rc=0; break; }
        sleep "\$RETRY_SLEEP"
        tries=\$((tries+1))
    done
    if [ "\$rc" -ne 0 ]; then
        echo "[campus_portal_eth1] auto-login failed after \$RETRY_MAX tries."
    fi
    sleep "\$MONITOR_INTERVAL"
done
EOF
}

start_service() {
    procd_open_instance
    # 用 /bin/sh -c 启一个 loop,当作守护进程
    procd_set_param command /bin/sh -c "$(_monitor_loop)"
    # 异常退出自动重启(首次延迟 5s,之后指数回退 60s 封顶)
    procd_set_param respawn 5 1 60
    procd_set_param stdout 1
    procd_set_param stderr 1
    # 可按需添加:当网络配置变更时重启该实例
    procd_close_instance
}

stop_service() {
    # procd 会杀掉守护进程
    return 0
}

reload_service() { stop; start; }

service_triggers() {
    # 当 network / wireless 配置被 commit 时触发
    procd_add_reload_trigger "network" "wireless"
}

# rc.common 默认 boot() 会调用 start()
# 我们保留一次性手动命令
login() { do_login_once; }
sstatus() { if is_online; then echo "[campus_portal_eth1] Online"; else echo "[campus_portal_eth1] Offline"; fi; }


修改完配置后将代码写到 /etc/init.d/campus_portal_eth1(注意,文件名要在代码里面一并修改),启动后可以在系统日志中看到日志信息。

Shell
# 开机启动
/etc/init.d/campus_portal_eth1 enable
# 启动
/etc/init.d/campus_portal_eth1 start

多账号

配置详细教程如下:基于openwrt的MWAN3实现多运营商负载均衡的一种方法


配置完成端口后将上面的 init.d 代码全局替换 eth1 为你创建的接口后,复制一份到 /etc/init.d/ 中即可,每个账号一个服务这样可以保证对每个校园网账号的登录控制。

Macvlan

在 网络→接口→设备 中可以添加 Macvlan,选择正确的 基础设备 后点击保存即可。

添加 Macvlan 完成后在 网络→接口 中添加对应的 DHCP客户端,同时设置防火墙区域为 wan 即可。

Image
Image

MultiWAN 负载均衡

Image
Image
Image
Image

mwan 的配置教程这里就不过多赘述了,我使用个两个接口(也就是两个账号)配置如上图。mwan 是通过 iptables + fwmark 路由规则来实现的负载均衡,所以在单线程情况下,无法速度叠加。

附图

OpenWrt 后台界面
OpenWrt 后台界面
100M + 50M 账号 mwan 测速
100M + 50M 账号 mwan 测速