0%

微信公众号开发框架

微信公众号框架

类似的程序之前写过QQ的,当时玩菠菜的时候写的,但是发现QQ现在使用的越来越少了,公众号这个东西逐渐映入眼帘,之前也看到有人用这个东西写框架,之前没时间,现在时间有了,所以我也就开发一下这个框架,可以方便之后使用。

基本的用途:
根据用户的输入,来执行一些I/O操作较少的命令并将其结果返回给用户。

框架平台:

  1. 安卓
  2. Windows

其实我一开始写的是Windows平台下的,基本写完了,但是由于一部分小原因,我就没有继续往下开发,但是我把安卓平台下的已经写完了,并且实现了接口的对外开放。

实现效果

这里我用安卓平台下的APP进行演示。
首先我们先要拥有一个公众号(订阅号),很简单注册,有微信号就好。
先登录我们的公众号:

扫码登录

信息监视:

信息监视

就是类似这样子的效果,用户输入的内容是我们可以监控,并且根据用户输入来进行信息的查询,这里我只是简单的实现一个功能,但是其他的无非就是照葫芦画瓢,好了演示就到这里。

具体实现

Windows版本的我为什么开发完毕,是因为我的VS2010基本被一些大型框架丢弃了,但是我电脑C盘不够用更新不了,像jsoncpp这种的没法用,实在太麻烦了。
接下来,我会带着大家用java去完整的开发这样子的一个框架,文章可能略长。
我们需要实现的效果是这样的,为了后期开发的方便,在之前的登录和信息监视都是需要我们写成一个完整框架,并且对外提供一个类似于这样子的一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//对外实现的接口
private String getReturnMessage(String Name,String Content)
{
int indexOf = 0;
StringBuffer sb = new StringBuffer();
//子域名查询
if((indexOf = Content.indexOf("子域名="))!=-1)
{
try {
List<String> subDomin = GetSubDomin(Content.substring(indexOf));
for (String string : subDomin) {
sb.append(string).append("\r\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}

return Name+"你的内容没有对应的查询内容";
}

方法是这样的,在我们信息接收到的时候调用,参数有两个,一个是发信息的姓名(用来判断是否是我们想要提供服的用户,我没有找到一个合适的ID进行准确判断),另一个是用户发送的信息(判断我们需要返回的内容),返回值就是我们要发送给用户的信息。

我们需要用到的技术:

  1. 基本的抓包技术
  2. js加密
  3. cookie处理
  4. json解析
  5. 正则表达式

基本就这几个,还有一些杂碎的我会在后面说到,为了保障我账号的一个安全,在登录封包的时候我会用wkertest666代替我的密码。

首先我们先来捕获登录的封包:
关键封包:
登录封包

1
username=3311736869%40qq.com&pwd=45ebabf1cfa6670dbc5dac1ca48842b4&imgcode=&f=json&userlang=zh_CN&redirect_url=&token=&lang=zh_CN&ajax=1

进行简单的分析可以看到,密码的加密明显没有加盐,这是比较简单的一方面,并且有几个参数为空,比较扎眼的是imgcodetoken这两个字段,token我猜测可能是用来切换用户的,imgcode是验证码,这个我在编写Windows版本的时候解决了,在我们多次登录账号并且在短时间内的时候,确实会让我们输入验证码,也比较好解决,但是我在写Android的时候他又不出来了,所以我们这里暂时先不考虑验证码的这个特殊情况。
username是utf-8编码的一个字符串,而pwd是微信自己的一个加密算法,分析起来不难,我带着大家分析一下:

js加密

首先找到我们提交这个封包的ajax:
pwd加密
明显是一个嵌套,先截取0-16的字符串,然后传递给r函数,我们下断点跟进r这个函数:
r函数
可以看到有一个自执行,但是比较那个是,这里的自执行,明显跳进的是最后一个switch的情况,所以我们只要分析return e(o(n))这个返回值就好了,n就是我们输入的密码前17位,然后我们跟进o函数:
o函数
这又是一个自执行,调用的是u的返回值,u函数:

1
2
3
4
5
6
7
8
function u(n) {
return unescape(encodeURIComponent(n))
}
function o(n) {
return function (n) {
return a(i(h(n), 8 * n.length))
}(u(n))
}

可以看到u是一个硬编码,没啥太大的意思,而o里面又有三个嵌套函数,首先是h再是i,最后是a,将其返回回去,跟进h函数:
h函数
有是一个硬编码,没啥他太大的意思,继续回到o函数,再进入i函数,
i函数
这个就有点意思,因为h返回给的是一个数组,所以这里接收到了一个数组和密码长度的8倍,然后进行了一大顿的操作,这有一个坑我等下回过头来说,看完i之后我们继续往下看a函数:
a函数
还是和之前一样,没啥意思,就是这么转了一大顿而已,好了o函数出来了,我们跟进e函数:
e函数
这个也比较简单,反正跟着他来写就好了,但是这里还有一个坑,就是这个n参数,可以看到我们根本看不到是什么,是一段乱码,所以我们只能嵌套处理,好了分析完毕之后,跟出去发现确实是我们的pwd参数所需要的内容。

好了我们整体进行分析,首先我们截取前十七个字符,然后传递给r函数,由于微信这里这个加密可能是为后期为维护做一个所谓的:高聚合低耦合 所实现的铺垫,所以他在这个r函数中我们只可能到最后一个switch条件下面,然后调用o函数,o函数中又嵌套调用了:h,i,a函数,h的参数是u函数的返回值,最后返回给e函数再次嵌套调用,e函数的返回值就是我们的pwd参数,这里有个坑我给你们填一下,这个i函数其实在内部调用了好多函数,但是为了不给文章阅读者带来压力我就没截图,但是这都不重要,最后我们可以根据他的算法编写这样的一个自定义function:

1
2
3
4
5
function wechajiami(n)
{
mm=n.substr(0, 16)
return e(a(i(h(mm), 8 * mm.length)))
}

只要传进n给这个函数,最后返回的就是密码的加密,我们测试一下:
自定义函数执行
确实使我们的加密密码:45ebabf1cfa6670dbc5dac1ca48842b4
所以我们js加密这块就解决了,然后我们来看返回值:

1
{"base_resp":{"err_msg":"ok","ret":0},"redirect_url":"/cgi-bin/bizlogin?action=validate&lang=zh_CN&account=3311736869%40qq.com"}

这里我就不给大家看失败的了,我直接说了,这个base_resp对象下面的err_msg属性是我们的重要一点,这里如果是ok代表登录成功,还有很多,比如说需要输入验证码,验证码错误,密码错误的情况,这里我就不提了,好了简单分析完毕之后,我们需要先来写程序实现一下:
布局什么的我就不提了,安卓的权限不要忘记就好。
安卓进行js加密是很麻烦的,我们需要借助浏览的回调进行读取,所以我们首先需要编写html代码:

1
2
3
4
5
6
function wechatjiami(n)
{
mm=n.substr(0, 16);
android.toastMessage(e(a(i(h(mm), 8 * mm.length))));
return "yes";
}

可以看到我这里用了个android.toastMessage这个方法,这个是用来通讯的,名称使我们自己定义的,需要在安卓中进行编写。
我们先定义一个WeChatLogin类,专门用来管理微信登录的,全部函数都在这里面,在构造函数的时候我们初始化js加密,这里我没有用单例模式,是因为需要上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SuppressLint("SetJavaScriptEnabled")
public WeChatLogin(Context c) {
this.context=c;
webview = new WebView(context); //实例化浏览器
webview.loadUrl("file:///android_asset/wechat.html"); //加载资源文件
webview.getSettings().setJavaScriptEnabled(true);//设置JS可用
webview.setWebChromeClient(new WebChromeClient() {});
webview.addJavascriptInterface(new MyJs(), "android");//接口化类
webview.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {//当页面加载完成时就调用js函数
super.onPageFinished(view, url);
isJsLoadEnd = true;
}
});
}

代码比较固定,只是有个MyJs类是我们自己写,然后写一个toastMessage方法回调js解密结果的,在asset文件夹中放我们的加密HTML,然后加载他,大家可以注意有这个注解:@SuppressLint("SetJavaScriptEnabled"),这个注解是用来压制我们XSS漏洞的,因为我们会调用js代码,所以很容易导致XSS漏洞,其实确实是这样的,但是我们这是自己给自己用,所以压制就行了。然后我们编写登录方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void SendWeChatjiami(String pass)
{

new AsyncTask<String, Void, String>() {
@Override
protected String doInBackground(String... params) {
while(!isJsLoadEnd){}
return params[0];
}
protected void onPostExecute(String result)
{
webview.loadUrl("javascript:wechatjiami('"+result+"')");
};

}.execute(pass);

}

public void Login(String username,String password)
{
this.username = username;
SendWeChatjiami(password);
}

我们调用Login方法,然后传递给SendWeChatjiami这个方法,为什么我们要保存用户名呢?我们之后要多次用到,这里先不提及,加密方法,我用了异步执行,等待浏览器加载完毕之后,我们执行js:javascript:wechatjiami('"+result+"')"调用我们编写的js,js执行完毕之后我们会回调MyJs这个类中的方法,所以MyJs是我们的关键代码:

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
class MyJs {

@android.webkit.JavascriptInterface
//真正的登录操作
public void toastMessage(String password) {
//运行到这个地方代表我们就是获取到了信息
new AsyncTask<String, Void, String>()
{
@Override
protected String doInBackground(String... params) {
try {
URL url = new URL("https://mp.weixin.qq.com/cgi-bin/bizlogin?action=startlogin");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setConnectTimeout(5000);//连接超时
connection.setReadTimeout(5000);//read读取异常
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("X-Requested-With","XMLHttpRequest");
connection.setRequestProperty("Referer","https://mp.weixin.qq.com/");
connection.setRequestProperty("Accept-Encoding","gzip, deflate, br");
connection.setRequestProperty("Origin","https://mp.weixin.qq.com");
OutputStream os = connection.getOutputStream();
os.write(("username="+params[0]+"&pwd="+params[1]+"&imgcode=&f=json&userlang=zh_CN&redirect_url=&token=&lang=zh_CN&ajax=1").getBytes("UTF-8"));
os.close();

if(connection.getResponseCode() == 200)
{
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String str;
StringBuffer sb = new StringBuffer();
while((str=br.readLine())!=null)
{
sb.append(str);
}
br.close();
Cookie="";
Map<String, List<String>> heraders = connection.getHeaderFields();
for (String s : heraders.get("Set-Cookie")) {
Cookie+=(s.split(";")[0]+";");
}
return sb.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
@Override
protected void onPostExecute(String result)
{
try {
JSONObject jsonObject = new JSONObject(result).getJSONObject("base_resp");
isLoginEnd=true;
if(!jsonObject.getString("err_msg").equals("ok"))
{
Toast.makeText(context, "登錄失敗!", Toast.LENGTH_SHORT).show();
}

} catch (Exception e) {
Toast.makeText(context, "登錄失敗!", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
};

}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,username,password);
}
}

这里可以看到,很简单,就是一个IO的操作,只不过要注意的是,我们的账号需要UTF-8编码,这里需要注意的是AsyncTask这个类要执行多线程在线程池的操作需要使用线程池的executeOnExecutor,其他的我是用Toast进行提示密码失败的,登录完毕之后我们将isLoginEnd=true,然后设置我们的Cookie,这样我们的第一步登录就完毕了,下面是二维码。

二维码与心跳包

微信还是用的心跳包,不算太恶心,验证码的图片地址是:https://mp.weixin.qq.com/cgi-bin/loginqrcode?action=getqrcode&param=4300&rd=307,抓包很容易抓到,第一个param参数是固定的,在js中可以看到:
param参数
而后面的rd是一个随机数,这里随便填写一个就好,不重要。
然后微信的话呢心跳包还是比较好分析的:
https://mp.weixin.qq.com/cgi-bin/loginqrcode?action=ask&token=&lang=zh_CN&f=json&ajax=1
返回值是这个样子的:

1
{"acct_size":0,"base_resp":{"err_msg":"ok","ret":0},"status":0,"user_category":0}

最重要的是status这个属性,简单的分析你会发现,0是没扫描,4是扫描了但是没登录,1是登录成功,登录成功之后他就会访问这个样子的一个URL:https://mp.weixin.qq.com/cgi-bin/bizlogin?action=login使用POST的方式进行提交,参数的话呢userlang=zh_CN&redirect_url=&token=&lang=zh_CN&f=json&ajax=1没有改变,但是返回值比较重要:

1
{"base_resp":{"err_msg":"ok","ret":0},"redirect_url":"/cgi-bin/home?t=home/index&lang=zh_CN&token=XXXXXXXXX"}

这个token,我这里我用X代替了,这个东西很重要,是我们后续操作的一个基础。

这里有一点很重要:他基本在除了二维码没有重置Cookie之外,基本每一步都会重新设置我们的Cookie,所以为了我们后续代码能够很好的编写,我写了这么一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private String GetCookie(String oldCookie,Map<String, List<String>> headers)
{
String result="";
String[] oldCookies = oldCookie.split(";");
Map<String, String>tmpCookie = new HashMap<String, String>();
for(int i=0;i<oldCookies.length;i++)
{
tmpCookie.put(oldCookies[i].substring(0,oldCookies[i].indexOf("=")), oldCookies[i].substring(oldCookies[i].indexOf("=")+1));
}
for (String s : headers.get("Set-Cookie")) {
String value = s.split(";")[0];
tmpCookie.put(value.substring(0,value.indexOf("=")), value.substring(value.indexOf("=")+1));
}
for(String key:tmpCookie.keySet())
{
result+=(key+"="+tmpCookie.get(key)+";");
}
return result;
}

用来覆盖我们需要重新设置的Cookie,怎么使用等下说。
我们到这里基本上算登录成功,还有一个就是进入主页面,他的这个页面URL:
https://mp.weixin.qq.com/cgi-bin/home?t=home/index&lang=zh_CN&token=XXXXXXXXX
是根据我们token进行配置的,从这个页面中可以获取一些关键信息,我只获取了用户名:

1
2
3
4
5
6
7
8
Pattern pattern = Pattern.compile("wx.cgiData.nick_name = \"(.*?)\";");
Matcher matcher = pattern.matcher(sb.toString());
if(matcher.find())
{
WeChatName = matcher.group(1);
}
isRealLogin = true;
return WeChatName;

这样就可以获取到用户名,这些POST和GET操作的代码我就不贴出来了,因为太长了,而且基本一样,根据我上面写的,应该很简单就写出来了。

信息监控

既然我们已经登陆完毕了,那么我们就可以进行信息的监控了,信息管理的页面是这个:
https://mp.weixin.qq.com/cgi-bin/message?t=message/list&count=20&day=7&token=XXXXXXXXX&lang=zh_CN
关键的是我们的token和前面的count和day,很简单,token是我们之前的,而count是我们要查看的数目,day是我们要查看最近几天的,这里我觉得默认就好了。
但是你可能在分析的时候不是很好分析,因为这个文件太大了,我也是找了很久才找到,这里我贴一下关键的页面内容:
关键提取内容
可以看到他信息是在一个对象里面的,我们用正则提取:

1
2
3
4
5
6
7
8
9
private String GetMessageJson(String str)
{
Pattern pattern = Pattern.compile("list : \\((.*?)\\).msg_item");
Matcher matcher = pattern.matcher(str);
while(matcher.find()){
return matcher.group(1);
}
return "";
}

很简单提取出来之后,我们可以来解析这个json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private List<MessageStruct>getMessageJsonResult(String str) throws Exception
{
JSONObject AlJo = new JSONObject(str);
JSONArray ja = AlJo.getJSONArray("msg_item");
List<MessageStruct> tmplist = new ArrayList<MessageStruct>();
for(int i=0;i<ja.length();i++)
{

JSONObject jo = ja.getJSONObject(i);
MessageStruct tmpMessage = new MessageStruct();
tmpMessage.Name = jo.getString("nick_name");
tmpMessage.fakeid = jo.getString("fakeid");
tmpMessage.id = jo.getString("id");
tmpMessage.pictureUrl = jo.getString("small_headimg_url");
tmpMessage.message = "图片消息";
if(jo.getString("type").equals("1"))//正常消息
{
tmpMessage.message = jo.getString("content");
}
tmplist.add(tmpMessage);
}
return tmplist;
}

这里有几个关键的内容我需要讲一下fakeid和id这个是发信息的人的一个关键标识符,通过这两个信息可以给我他回复消息,我们需要保存,type是他发信息的类型,不是1代表是图片之类的,是1代表文字,我这里用了个内部类:

1
2
3
4
5
6
7
8
9
10
11
12
public class MessageStruct 
{
public String Name;
public String message;
public String fakeid;
public String id;
public String pictureUrl;
@Override
public String toString() {
return Name + "说:" + message;
}
}

信息获取完毕之后,这里其实还有一个心跳包,是用来监视有没有新的信息的:
https://mp.weixin.qq.com/cgi-bin/getnewmsgnum?f=json&t=ajax-getmsgnum&lastmsgid=500000170&filterivrmsg=&filterspammsg=&token=XXXXXXXXX&lang=zh_CN
和之前一样,但是多了一个lastmsgid这个参数,这个参数是一你要查询哪一条信息之后的最新消息,我们不是太需要,不填写代表是最新的,他的一个返回值:

1
{"base_resp":{"ret":0,"err_msg":"ok"},"newTotalMsgCount":0,"new_comment_count":0,"new_reward_count":0}

最重要的就是newTotalMsgCount这个属性,他代表最新的更新有几条,我们可以通过这个参数进行判断:

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
public String GetMessageStatus() throws Exception
{
URL urlG = new URL("https://mp.weixin.qq.com/cgi-bin/getnewmsgnum?f=json&t=ajax-getmsgnum&filterivrmsg=&filterspammsg=&token="+token+"&lang=zh_CN");
HttpURLConnection connection = (HttpURLConnection)urlG.openConnection();
connection.setConnectTimeout(5000);//连接超时
connection.setReadTimeout(5000);//read读取异常
connection.setDoInput(true);
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0");
connection.setRequestProperty("Upgrade-Insecure-Requests","1");
connection.setRequestProperty("Cookie",Cookie);
if(connection.getResponseCode() == 200)
{
String str;
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuffer sb = new StringBuffer();
while((str=br.readLine())!=null)
{
sb.append(str);
}
br.close();
Cookie = GetCookie(Cookie,connection.getHeaderFields());
JSONObject jo = new JSONObject(sb.toString());
return jo.getString("newTotalMsgCount");
}
return null;
}

我们返回回去只要不是零我们就刷新最新消息,然后将返回的个数进行一个遍历,然后通过我的内部类结构进行一个消息的回复。
上面有一个坑,就是在我们获取信息的时候有一个问题,就是他总是会出现HTTPS的异常,我们需要忽略证书问题:

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
URL urlG = new URL(messageURL);
HttpsURLConnection connection = (HttpsURLConnection)urlG.openConnection();

connection.setHostnameVerifier(new HostnameVerifier() {

@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}

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

};
try{
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}catch (Exception e) {
e.printStackTrace();
}

需要信任所有的域名,然后才会正常获取,并且有一个协议头不要忘记:

1
connection.setRequestProperty("Upgrade-Insecure-Requests","1");

这个是用来接收HTTPS完整信息的。
并且你会发现,有很多的协议头中必须用到Referer,而这个值一般的格式:

1
connection.setRequestProperty("Referer","https://mp.weixin.qq.com/cgi-bin/bizlogin?action=validate&lang=zh_CN&account="+URLEncoder.encode(username, "UTF-8")+"&token=");

我们是需要用到我们的用户名的,这就是为什么我们需要保存起来用户名的原因。

信息发送

就是一个POST包:
https://mp.weixin.qq.com/cgi-bin/singlesend?t=ajax-response&f=json&token=XXXXXXXXX&lang=zh_CN
然后发送的数据有点意思:
token=XXXXXXXXX&lang=zh_CN&f=json&ajax=1&random=0.08843816131755489&mask=false&tofakeid=oLkOPwgBRGPk7bcbkzvloWAr5J5w&imgcode=&type=1&content=test&appmsg=&quickreplyid=500000168
可以看到我们也是需要token的,并且我们需要的东西还有我们之前从json中获取的两个id,还有个验证码,但是在我测试过程中一直没有遇到,返回值不是很重要。

到此为止,我们基本的操作就完毕了,其他的一些不关键的东西我这里没给出来,因为只是一些语言的基本知识所以并不是很重要。

验证码

这个东西其实我在Windows版本编写的过程中遇到了,很简单,地址固定,正常获取之后重新发送一下登录包就好了,不要忘记Cookie就好了。

后续

这个小框架我写了接近两天,中途遇到好多东西异常,因为我一般是用Windows写这类程序的,所以有很多小披露,这次用安卓写主要是来巩固一下之前遗忘的东西,分析起来比较简单,和我之前分析反汇编的东西来说真的是小巫见大巫的感觉,本来想紧跟着把其他的东西再一些一下,但是想起D3D和驱动的东西我还没写完,这些娱乐性质的就要先放一下。

具体的源代码和之前的那个反汇编工具的源代码,我都会在我的GitHub中进行分享,可以关注一下我的博客。