让f1c100s开发板通过认证接入校园网
书接上回,我们给一块小小的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
调用之后会返回一些杂乱无章的信息,没有一个统一的方法来获取用户是否成功连接。如果连接失败,就会有一个error
和error_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
#
成功联网!