instabtbu-详解上网登录

最开始的时候我们的上网登录是通过HTTP的POST实现的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 public String POST(String url,String postdata)
{
String result = "";
HttpPost hPost=new HttpPost(url);
List params=new ArrayList();
String posts[]=postdata.split("&");
String posts2[];
int i;
for(i=0;i<posts.length;i++)
{
posts2=posts[i].split("=");
if(posts2.length==2)
params.add(new BasicNameValuePair (posts2[0],posts2[1]));
else params.add(new BasicNameValuePair (posts2[0],""));
}
try{
HttpEntity hen=new UrlEncodedFormEntity(params,"gb2312");
hPost.setEntity(hen);
myClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 30000);
//请求超时
myClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 30000);
//读取超时
HttpResponse hResponse;
hResponse = myClient.execute(hPost);
if(hResponse.getStatusLine().getStatusCode()==200)
{
result = EntityUtils.toString(hResponse.getEntity());
//result = new String(result.getBytes("ISO_8859_1"),"gbk");
//转码
}

}catch(Exception e){
if(dialog2.isShowing())dialog2.dismiss();
show("连接BTBU失败。n请确认信号良好再操作。");
}
return(result);
}

之后学校封了HTTP(80端口),只留下了HTTPS(443端口),于是我们将协议修改为HTTPS.不过这里有一个证书问题,我们选择信任所有证书来解决这个问题.引用了一个库里面的类:SSLSocketFactoryEx.下面是这个类的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package hk.ypw.instabtbu;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpVersion;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

public class SSLSocketFactoryEx extends SSLSocketFactory {

public static DefaultHttpClient getNewHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);

SSLSocketFactoryEx sf = new SSLSocketFactoryEx(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));

ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

return new DefaultHttpClient(ccm, params);
} catch (Exception e) {
return new DefaultHttpClient();
}
}

SSLContext sslContext = SSLContext.getInstance("TLS");

public SSLSocketFactoryEx(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);

TrustManager tm = new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {return null;}

@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] chain, String authType)
throws java.security.cert.CertificateException {}

@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] chain, String authType)
throws java.security.cert.CertificateException {}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}

@Override
public Socket createSocket(Socket socket, String host, int port,boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port,autoClose);
}

@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}

利用这个东西,我们实现了https:
1
2
3
4
HttpPost hPost = /*跟之前一样*/;
HttpClient httpclient = SSLSocketFactoryEx.getNewHttpClient();
HttpResponse hResponse = httpclient.execute(hPost);

不过,神奇的网络中心干脆取消了https,全面取消网页上网方式,仅留下了电脑端登录的方式. 而神奇的我们则再一次研究了他们的电脑端.

instabtbu2上网登录原理:

电脑端的原理就和HTTP完全不沾边了. 电脑端需要的前置技能有:十六进制数据处理,转义,TCP通信协议,UDP通信协议,CRC16校验,RSA加密.

电脑端登录的过程比较复杂.

建立TCP连接

首先我们要与192.168.8.8的21098端口建立TCP连接.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean connect() {
boolean f = false;
try{
client = new Socket("192.168.8.8",21098);
outputStream=client.getOutputStream();
inputStream = client.getInputStream();
client.setSoTimeout(30000);
f=true;
}catch (Exception e){
e.printStackTrace();
print("错误:"+e.getMessage());
}
return f;
}

这里的outputStream和inputStream都是全局变量,用来输入输出的. 连上之后我们要发送一个空包给服务器.

读取返回数据:

读取方式很简单,我们通过刚才client得到的inputStream即可读出数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public char[] read(){
char[] read2=null;
try{
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
read2 = new char[len];
int j;
for(j=0;j<len;j++)read2[j]=(char)buffer[j];
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
print("错误:"+e.getMessage());
}
print("读到数据:"+charsToHexString(read2)+",长度:"+read2.length);
return read2;
}

数据包结构

BTBU网络服务器的数据包结构是这样的:
7E 命令 长度 低位 长度 高位 数据 CRC 低位 CRC 高位 7E
根据这个结构,我们可以计算出空包是这样的
7E 11 00 00 54 01 7E

好,现在我们将这个数据(7E 11 00 00 54 01 7E)发送给服务器,服务器返回给我们的就是这样的数据(这是例子) 7E 11 10 00 E7 55 98 4A 70 7D 3E 74 EE 44 95 E4 88 FA F5 58 37 E1 D1 7E 7E 11 10 00 B7 12 5F E6 60 5B A7 69 3E 18 06 16 9D 57 CC 81 9C 93 7E 这时候你发现了一个问题,长度10 00明明是16位,第一行的数据却出现了17位,这是怎么回事呢? 其实这只是数据的转义.由于我们的数据中可能包含7E,而它的意义是数据的结束,如果数据中出现了7E,我们必须对它进行转义. E7 55 98 4A 70 7D 3E 74 EE 44 95 E4 88 FA F5 58 37中的7D 3E实际上就是7E转义以后的数据.

反转义

反转义规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public char[] fanzhuan(char[] buf){
char[] rec = new char[1024];
int i,j=0;
for(i=0;i<buf.length;i++)
{
if(buf[i]==0x7D){
rec[j++]=(char) (buf[++i]^0x40);
}else rec[j++]=buf[i];
}

char[] rec2 = new char[j];
for(i=0;i<j;i++)rec2[i]=rec[i];
return rec2;
}

构建数据包

OK我们理解转义之后,就能解析出服务器给我们发的固定长度的16字节数据.然后,我们需要构建出登录数据包来. 登录数据包(82字节)格式如下:

  • 23字节用户名
  • 23字节密码
  • 20字节IP
  • 16字节验证数据 验证数据就是服务器发过来的那一串. 那么这一部分用java生成登录数据包就是这样的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public char[] user(String number ,String password){
    char[] msg = new char[82];
    String ip = getIp();
    try{
    int i;
    for(i=0;i<number.length();i++)msg[i]=number.charAt(i);
    for(i=0;i<password.length();i++)msg[i+23]=password.charAt(i);
    for(i=0;i<ip.length();i++)msg[i+23+23]=ip.charAt(i);
    for(i=0;i<verify.length;i++)msg[i+23+23+20]=verify[i];
    }catch(Exception e)
    {
    e.printStackTrace();
    print("错误:"+e.getMessage());
    }
    return msg;
    }

    通过这样的方式,我们可以获得登录数据包:
    1
    2
    char[] msg = user(editText_num.getText().toString(), editText_psw.getText().toString());

RSA加密

数据包生成之后,我们需要对它进行RSA加密,RSA加密是一种非对称加密方式,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public char[] jiami(char[] data){
PublicKey key = getPublicKey();
Cipher cipher;
byte[] buf = null;
try {
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
buf=cipher.doFinal(charToByte(data));
}catch(Exception e)
{
e.printStackTrace();
print("错误:"+e.getMessage());
}
return byteToChar(buf);
}

public static PublicKey getPublicKey() {
PublicKey publicKey = null;
String modulus = "EA32BA96FCC395CC766EAFFEBC8EFE1F0886E99504CB7C3877548698793446BA7BA07CF915DBB5BE69337A3697B4DC354DA78ABAE17ED33EDAD87674D0D0D2B54D549E566AF0C016C276F327ADC3D4EE06E64EBC608E4AC9E3CE63416C246FD57DBEA8ADA036AA683F9A812CD8ECA705E019D6A943121CDDB2CF9BF1BCD0F5F9";
String publicExponent = "65537";
BigInteger m = new BigInteger(modulus,16);
BigInteger e = new BigInteger(publicExponent);

RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e);
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(keySpec);
} catch (Exception e1) {
e1.printStackTrace();
}
return publicKey;
}

封装数据包

加密数据包之后,就可以封装成服务器需要的那个格式了:
7E 命令 长度 低位 长度 高位 数据 CRC 低位 CRC 高位 7E

命令是1(msg=feng(msg, 0x01);),具体封装方式可以看到下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public char[] feng(char[] buf,int cmd){
char[] rec = new char[1024];
char[] jiami = jiami(buf);
char[] crc = new char[jiami.length+3];
char[] rec2 = new char[crc.length+4];
rec[0]=0x7E;
rec[1]=(char)cmd;
int len = jiami.length;
rec[2]=(char) (len&0xFF);
rec[3]=(char) (len>>8);

int i;
for(i=0;i<jiami.length;i++)rec[i+4]=jiami[i];

for(i=1;i<jiami.length+4;i++)crc[i-1]=rec[i];

int c = getCRC16(charToByte(crc));

rec[crc.length+1]=(char) (c&0xFF);
rec[crc.length+2]=(char) (c>>8);
rec[crc.length+3]=0x7E;
for(i=0;i<crc.length+4;i++)rec2[i]=rec[i];

return rec2;
}

CRC-16校验

其中有用到getCRC16()这个命令,多项式它是这样的X16+X15+X2+1,下面是查表法获得CRC-16的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public int getCRC16(byte[] bytes) {
int[] table = {0x0000,0x8005,0x800F,0x000A,0x801B,0x001E,0x0014,0x8011,0x8033,0x0036,0x003C,0x8039,0x0028,0x802D,0x8027,
0x0022,0x8063,0x0066,0x006C,0x8069,0x0078,0x807D,0x8077,0x0072,0x0050,0x8055,0x805F,0x005A,0x804B,0x004E,0x0044,
0x8041,0x80C3,0x00C6,0x00CC,0x80C9,0x00D8,0x80DD,0x80D7,0x00D2,0x00F0,0x80F5,0x80FF,0x00FA,0x80EB,0x00EE,0x00E4,
0x80E1, 0x00A0,0x80A5,0x80AF,0x00AA,0x80BB,0x00BE,0x00B4,0x80B1,0x8093,0x0096,0x009C,0x8099,0x0088,0x808D,0x8087,
0x0082,0x8183,0x0186,0x018C,0x8189,0x0198,0x819D,0x8197,0x0192,0x01B0,0x81B5,0x81BF,0x01BA,0x81AB,0x01AE,0x01A4,
0x81A1,0x01E0,0x81E5,0x81EF,0x01EA,0x81FB,0x01FE,0x01F4,0x81F1,0x81D3,0x01D6,0x01DC,0x81D9,0x01C8,0x81CD,0x81C7,
0x01C2,0x0140,0x8145,0x814F,0x014A,0x815B,0x015E,0x0154,0x8151,0x8173,0x0176,0x017C,0x8179,0x0168,0x816D,0x8167,
0x0162,0x8123,0x0126,0x012C,0x8129,0x0138,0x813D,0x8137,0x0132,0x0110,0x8115,0x811F,0x011A,0x810B,0x010E,0x0104,
0x8101,0x8303,0x0306,0x030C,0x8309,0x0318,0x831D,0x8317,0x0312,0x0330,0x8335,0x833F,0x033A,0x832B,0x032E,0x0324,
0x8321,0x0360,0x8365,0x836F,0x036A,0x837B,0x037E,0x0374,0x8371,0x8353,0x0356,0x035C,0x8359,0x0348,0x834D,0x8347,
0x0342,0x03C0,0x83C5,0x83CF,0x03CA,0x83DB,0x03DE,0x03D4,0x83D1,0x83F3,0x03F6,0x03FC,0x83F9,0x03E8,0x83ED,0x83E7,
0x03E2,0x83A3,0x03A6,0x03AC,0x83A9,0x03B8,0x83BD,0x83B7,0x03B2,0x0390,0x8395,0x839F,0x039A,0x838B,0x038E,0x0384,
0x8381,0x0280,0x8285,0x828F,0x028A,0x829B,0x029E,0x0294,0x8291,0x82B3,0x02B6,0x02BC,0x82B9,0x02A8,0x82AD,0x82A7,
0x02A2,0x82E3,0x02E6,0x02EC,0x82E9,0x02F8,0x82FD,0x82F7,0x02F2,0x02D0,0x82D5,0x82DF,0x02DA,0x82CB,0x02CE,0x02C4,
0x82C1,0x8243,0x0246,0x024C,0x8249,0x0258,0x825D,0x8257,0x0252,0x0270,0x8275,0x827F,0x027A,0x826B,0x026E,0x0264,
0x8261,0x0220,0x8225,0x822F,0x022A,0x823B,0x023E,0x0234,0x8231,0x8213,0x0216,0x021C,0x8219,0x0208,0x820D,0x8207,
0x0202};
int i = 0;
int len = bytes.length;
int crc = 0;
while(i<len){
int index = (crc>>8)^bytes[i++];
if(index<0)index+=256;
crc = ((crc&0xFF)<<8) ^ table[index];
}
return crc;
}

转义

封装好了之后,我们还需要转义一次,以免出现7E:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public char[] zhuan(char[] buf){
char[] rec = new char[1024];
int i=0,j=0;
for(i=0;i<buf.length;i++)
{
if((buf[i]==0x7D||buf[i]==0x7E)&&i!=0&&i!=buf.length-1){
rec[j++]=0x7D;
rec[j++]=(char) (buf[i]^0x40);

}else{
rec[j++]=buf[i];
}
}
char[] rec2 = new char[j];
for(i=0;i<j;i++)rec2[i]=rec[i];
return rec2;
}

发送

转义好了之后,就可以发送了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean send(char[] buf){
boolean f = false;
try {
byte[] send = charToByte(buf);
outputStream.write(send);
outputStream.flush();
f=true;
}catch(Exception e)
{
e.printStackTrace();
print("错误:"+e.getMessage());
}
print("发送数据:"+charsToHexString(buf)+",长度:"+buf.length);
return f;
}

利用之前的读取函数,我们在发送完毕之后马上读取数据:char[] rec = read();

解封装

那么发送过去登录数据之后,就要接收数据,不过接收到的数据依然是封装好的,我们需要对其解封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public char[] jiefeng(char[] buf){
char[] rec = null;
try{
int len=0;
int buf2=buf[2],buf3=buf[3];
if(buf2>256)buf2=0x100-0x10000+buf2;
if(buf3>256)buf3=0x100-0x10000+buf3;
len=buf2+buf3*0x100;
buf=fanzhuan(buf);
rec = new char[len];
for(int i=0;i<len;i++)rec[i]=buf[i+4];
if(buf[1]==1){
rec=jiemi(rec);
print("解密数据:"+charsToHexString(rec)+",长度:"+rec.length);
remain=rec;
}else{
recString = new String(charToByte(rec), "GBK");
rec=null;
}
}catch(Exception e){
e.printStackTrace();
}

return rec;
}

这里解封装有两种可能,一种是解出来纯文本数据,一种是解出来登录成功之后的保持在线数据,这里我们通过命令码判断,如果是1就代表登录成功,那么就进行RSA解密,并赋值给全局变量remain,否则就是string,我们对其进行GBK解码.

RSA解密

RSA解密如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public char[] jiemi(char[] data){
PublicKey key = getPublicKey();
Cipher cipher;
byte[] buf = null;
try {
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
buf=cipher.doFinal(charToByte(data));
}catch(Exception e)
{
e.printStackTrace();
print("错误:"+e.getMessage());
}
return byteToChar(buf);
}

解密是DECRYPT_MODE,其余和加密没有差别.

获取到remiain(通常是20字节)这个量之后就可以进行各项操作了,比如保持在线,断开等.

断开连接

下面是断开的操作(断开需要发送三次断开连接的数据):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Runnable duankaiRunnable = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket();
print("建立断开socket");
}catch (Exception e) {
e.printStackTrace();
}
InetAddress ip = null;
try {
ip = InetAddress.getByName("192.168.8.8");
char[] data = getcmd(1);
print("断开发送数据:"+charsToHexString(data));
DatagramPacket datagramPacket = new DatagramPacket(charToByte(data), remain.length+7, ip, 21099);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
remain=null;
show("断开成功");
}catch(NullPointerException e){
show("你还没有登录");
}catch (Exception e) {
e.printStackTrace();
print("断开出错:"+e.getMessage());
}
}
};

7E 命令 长度 低位 长度 高位 数据 CRC 低位 CRC 高位 7E

断开连接数据包例子: 7E 01 14 00 61 A1 06 AD 35 04 4B 23 0C DC 98 9C B6 6B D0 4B B3 16 01 00 03 D4 7E 断开中的getcmd函数相当于封装那20字节的remain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public char[] getcmd(int cmd){
char[] data = new char[remain.length+7];
data[0]=0x7E;
data[1]=(char)cmd;
data[2]=0x14;
data[3]=0x00;
int i;
for(i=0;i<remain.length;i++)data[i+4]=(char) remain[i];
char[] crc = new char[remain.length+3];
for(i=0;i<remain.length+3;i++)crc[i]=data[i+1];
int c = getCRC16(charToByte(crc));
data[crc.length+1]=(char) (c&0xFF);
data[crc.length+2]=(char) (c>>8);
data[crc.length+3]=0x7E;
return data;
}

保持在线

保持在线的话就这样获取数据包:getcmd(0);,其余的数据不变.

查流量

查流量的话与登录不同,查流量发送的数据第一发送的命令码是3,第二发送的数据不含IP 也就是说查流量的格式是这样的:

  • 23字节用户名
  • 23字节密码
  • 16字节验证数据 没错,长度只有62字节. 其他的RSA,CRC都是一样的操作. 返回的数据是纯文本的,储存在recString全局变量中.

取出流量数据

得到文本之后,我们通过正则表达式得到剩余流量:

1
2
3
4
5
6
7
8
9
Pattern p=Pattern.compile("([0-9]+)(兆)");
Matcher m=p.matcher(result);
int liuliang=0;
while(m.find()){
String tempString = m.group(1);
liuliang+=Integer.valueOf(tempString).intValue();
}
liuliangString=String.valueOf(liuliang);

这里凡是兆前面的数字都会被找到,然后加到流量中去. 同样我们也可以通过这个正则表达式得到网费数据:
1
Pattern.compile("剩余网费.*?(\\w+.\\w*元)");
在线数据:
1
Pattern.compile("在线:(\\d+)");