让f1c100s开发板通过认证接入校园网

发布时间 2023-08-22 19:59:19作者: 可爱无辜猫猫头
title: 让f1c100s通过认证接入校园网
date: 2023-08-22 17:02:22
tags:
categories: embedded

书接上回,我们给一块小小的f1c100s开发板上配好了以太网的驱动,但是由于学校的校园网需要认证,未认证的话会使用防火墙屏蔽所有除了认证用的流量。所以我打算手写一个跨平台的认证程序。在通过认证,可以访问外网后,再移植一点好玩的进来。比如dpkg和apt。

上一回中移植linux的插曲

我的移植工作全部在windows下的wsl进行,使用usbip协议将tf卡直接连接到wsl上,在wsl下挂载两个分区进行读写操作。但是这个读写操作就特别坑人,有的时候命名执行了rm,cp,mv等指令,并且再ls看起来文件都已经改变了,再把卡一拔,插上板子,发现还是有问题。最后一看,是tf卡的更改根本就没有写入。后来一番折腾,发现覆盖文件不行,一定要先删,再疯狂sync几次,将更改从内存中写入文件系统。最后再多sync几次,确保更改成功写入。

贵校校园网认证过程

贵校的校园网用的是深澜的portal认证,一个非常**的协议。这个协议总共就两步,并且写进了网页的js里边,基本逻辑很容易摸清楚,但是它的签名过程极为麻烦,将javascript代码转换为c++也是一件极其麻烦的事情,涉及到各种符号、位数转换问题。

认证过程分为两步,第一步是challenge,真是奇怪的意图。反正照着F12抓包就好。

https://auth4.tsinghua.edu.cn/cgi-bin/get_challenge?callback=jQuery1322188223165048661_1692702135188&username=test&ip=&double_stack=1&_=1692702135189

jQuery111308321434325048661_1692702135188({
    "challenge": "72430d5fcaabc2152443242343210767508314ea103c21c940268995f28824b6",
    "client_ip": "101.5.*.84",
    "ecode": 0,
    "error": "ok",
    "error_msg": "",
    "expire": "60",
    "online_ip": "101.5.*.84",
    "res": "ok",
    "srun_ver": "SRunCGIAuthIntfSvr V1.18 B20190423",
    "st": 1692702141
})

通过调用这个接口我们得到了待认证客户端的ip地址和服务端传来的token令牌。接下来,需要使用这个令牌生成一堆签名。

首先需要根据特殊算法x_encode对客户端的信息进行签名。需要签名的信息有:

  • 认证所需用户名
  • 用户密码
  • 客户端ip地址
  • AC_ID
  • 未知参数enc_ver

这些信息的结构就是一个json,挖掉了所有的空格。用c++生成这些信息的代码如下:

std::stringstream ss;
    ss << R"({"username":")" << username << R"(","password":")" << password << R"(","ip":")" << ip_addr
       << R"(","acid":")" << ac_id << R"(","enc_ver":"srun_bx1"})";

然后使用它们的特殊x_encode算法进行签名。这个算法的源代码在网页的js里边,现在根据c++将其转写。

//
// Created by hanyuan on 2023/8/21.
//
#include <cmath>
#include <string>
#include <vector>
#include "../../include/SrunXEncode.h"

std::vector<size_t> s(const std::string &a, bool b) {
    size_t c = a.size();
    std::vector<size_t> v;
    for (auto i = 0; i < c; i += 4) {
        size_t temp = (a[i]) | (a[i + 1] << 8) | (a[i + 2] << 16) | (a[i + 3] << 24);
        v.push_back(temp);
    }
    if (b) {
        v.push_back(c);
    }
    return v;
}

std::string l(const std::vector<size_t> &a, bool b) {
    std::string res;
    auto d = a.size();
    auto c = (d - 1) << 2;
    if (b) {
        auto m = a[d - 1];
        if ((m < c - 3) || (m > c))
            return "";
        c = m;
    }
    for (auto i = 0; i < d; i++) {
        std::string temp;
        temp += (char) (a[i] & 0xff);
        temp += (char) (a[i] >> 8 & 0xff);
        temp += (char) (a[i] >> 16 & 0xff);
        temp += (char) (a[i] >> 24 & 0xff);
        res += temp;
    }
    if (b) {
        return res.substr(0, c);
    } else {
        return res;
    }
}

std::string x_encode(std::string str, std::string key) {
    if (str == "") {
        return "";
    }
    auto v = s(str, true);
    auto k = s(key, false);
    while (k.size() < 4) {
        k.push_back(0);
    }
    auto n = v.size() - 1;
    auto z = v[n];
    auto y = v[0];
    auto c = 0x86014019 | 0x183639A0;
    size_t m,
            e,
            p,
            q = std::floor(6.0 + 52.0 / ((double) n + 1.0)),
            d = 0;
    while (0 < q--) {
        d = d + c & (0x8CE0D9BF | 0x731F2640);
        e = d >> 2 & 3;
        for (p = 0; p < n; p++) {
            y = v[p + 1];
            m = z >> 5 ^ y << 2;
            m += ((y >> 3) ^ (z << 4)) ^ (d ^ y);
            m += k[(p & 3) ^ e] ^ z;
            size_t temp = v[p] + m & (0xEFB8D130 | 0x10472ECF);
            v[p] = temp;
            z = temp;
        }
        y = v[0];
        m = (z >> 5) ^ (y << 2);
        m += ((y >> 3) ^ (z << 4)) ^ (d ^ y);
        m += k[(p & 3) ^ e] ^ z;
        size_t temp = v[n] + m & (0xBB390742 | 0x44C6F8BD);
        v[n] = temp;
        z = temp;
    }

    return l(v, false);
}

这还没完,使用x_encode(ss.str(),token)加密完数据后,还需要再将其的返回值——8位二进制数组使用特供版base64编码出来。特供版代码使用c++转写如下:

//
// Created by hanyuan on 2023/8/21.
//

#include "../../include/SrunBase64.h"

static BYTE pad_char = '=';
static std::string alpha = "LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA";

std::string get_base64_string(std::string s) {
    int i;
    uint32_t b10;
    std::vector<BYTE> x;
    int imax = s.length() - s.length() % 3;
    if (s.length() == 0) {
        return s;
    }
    for (i = 0; i < imax; i += 3) {
        b10 = (((BYTE) s[i]) << 16) | (((BYTE) s[i + 1]) << 8) | ((BYTE) s[i + 2]);
        x.push_back(alpha[(b10 >> 18)]);
        x.push_back(alpha[((b10 >> 12) & 63)]);
        x.push_back(alpha[((b10 >> 6) & 63)]);
        x.push_back(alpha[(b10 & 63)]);
    }
    i = imax;
    switch (s.length() - imax) {
        case 1:
            b10 = ((BYTE) s[i]) << 16;
            x.push_back(alpha[(b10 >> 18)]);
            x.push_back(alpha[((b10 >> 12) & 63)]);
            x.push_back(pad_char);
            x.push_back(pad_char);
            break;
        case 2:
            b10 = (((BYTE) s[i]) << 16) | (((BYTE) s[i + 1]) << 8);
            x.push_back(alpha[(b10 >> 18)]);
            x.push_back(alpha[((b10 >> 12) & 63)]);
            x.push_back(alpha[((b10 >> 6) & 63)]);
            x.push_back(pad_char);
    }
    std::string result;
    for (i = 0; i < x.size(); i++) {
        result += x[i];
    }
    return result;
}

至此,总算完成了摘要的签名。接下来还要给密码签名。算法是hmac md5,msg是密码,key是token。接下来还要生成校验码。校验码是以下几项的字符串拼接:

  • token + username
  • token + hmac_md5
  • token + ac_id
  • token + ip_addr
  • token + "200"
  • token + "1"
  • token + base64编码的摘要

再用sha1把这个校验码再裹上一层。真是逆天的签名算法。

准备好以上数据后就可以准备调用srun_portal接口了,这个地址只需从challenge的url中把challenge替换成srun_portal就可以得到。
调用接口需要的参数有:

  • action=login
  • username,url编码过
  • password,开头是{MD5},剩下的是hmac md5摘要过的密码
  • ac_id
  • ip地址
  • 校验码
  • 摘要
  • n=200
  • type=1
  • os=windows+10
  • name=windows
  • double_stack=1
  • _=时间戳,单位为ms

调用之后会返回一些杂乱无章的信息,没有一个统一的方法来获取用户是否成功连接。如果连接失败,就会有一个errorerror_msg属性出现。如果连接成功,就只剩下suc_msg。所以这里就case by case分析。

int AuthWorker::fetch_error_state(const std::string &data) {
    fetch_from_json(data, "error", error_state);
    if (error_state == "ok") {
        fetch_from_json(data, "suc_msg", response_msg);
        if (response_msg == "login_ok") {
            return 0;
        }
    } else if (error_state == "login_error") {
        fetch_from_json(data, "error_msg", response_msg);
    } else {
        fetch_from_json(data, "res", response_msg);
    }
    return -1;
}

交叉编译移植程序到开发板

我的项目地址在thulogin,基于Clion与Cmake构建,因此可以轻松的移植到armlinux平台上。首先先在我的wsl下克隆这个仓库。

先执行

cmake . -D CMAKE_CXX_COMPILER=arm-linux-g++

将编译器设置为我们buildroot的编译器。然后再执行make,这样目标平台的二进制文件就已经生成了。让我们执行一下readelf -h thulogin

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x3400
  Start of program headers:          52 (bytes into file)
  Start of section headers:          250368 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         31
  Section header string table index: 30

如果我们使用编译内核的编译器,那么编译出来再readelf效果是这样的:

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x13da1
  Start of program headers:          52 (bytes into file)
  Start of section headers:          220844 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         39
  Section header string table index: 38

ABI和TYPE和入口点都不太一样。如果使用后者的程序,进去就会报段错误。

使用rz通过串口把二进制文件传到开发板,接下来让我们执行我们的程序:

# ls
thulogin
# chmod +x thulogin
# ./thulogin
****** thulogin ******
*** Initializing

*** Auth Server: http://auth4.tsinghua.edu.cn
*** Auth Ac_id: 173
*** Auth U/A: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Username:zhaohy22
Password:

*** Start authenticating...
*** Authenticate Username: zhaohy22
*** Logged in successfully!
# ping www.baidu.com
PING www.baidu.com (182.61.200.7): 56 data bytes
64 bytes from 182.61.200.7: seq=0 ttl=50 time=5.073 ms
64 bytes from 182.61.200.7: seq=1 ttl=50 time=4.876 ms
64 bytes from 182.61.200.7: seq=2 ttl=50 time=4.932 ms
^C
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 4.876/4.960/5.073 ms
#

成功联网!