微信公众号框架 类似的程序之前写过QQ的,当时玩菠菜的时候写的,但是发现QQ现在使用的越来越少了,公众号这个东西逐渐映入眼帘,之前也看到有人用这个东西写框架,之前没时间,现在时间有了,所以我也就开发一下这个框架,可以方便之后使用。
基本的用途: 根据用户的输入,来执行一些I/O操作较少的命令并将其结果返回给用户。
框架平台:
安卓
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进行准确判断),另一个是用户发送的信息(判断我们需要返回的内容),返回值就是我们要发送给用户的信息。
我们需要用到的技术:
基本的抓包技术
js加密
cookie处理
json解析
正则表达式
基本就这几个,还有一些杂碎的我会在后面说到,为了保障我账号的一个安全,在登录封包的时候我会用wkertest666
代替我的密码。
首先我们先来捕获登录的封包: 关键封包:
1 username=3311736869%40qq.com&pwd=45ebabf1cfa6670dbc5dac1ca48842b4&imgcode=&f=json&userlang=zh_CN&redirect_url=&token=&lang=zh_CN&ajax=1
进行简单的分析可以看到,密码的加密明显没有加盐,这是比较简单的一方面,并且有几个参数为空,比较扎眼的是imgcode
和token
这两个字段,token我猜测可能是用来切换用户的,imgcode是验证码,这个我在编写Windows版本的时候解决了,在我们多次登录账号并且在短时间内的时候,确实会让我们输入验证码,也比较好解决,但是我在写Android的时候他又不出来了,所以我们这里暂时先不考虑验证码的这个特殊情况。 username是utf-8编码的一个字符串,而pwd是微信自己的一个加密算法,分析起来不难,我带着大家分析一下:
js加密 首先找到我们提交这个封包的ajax: 明显是一个嵌套,先截取0-16的字符串,然后传递给r函数,我们下断点跟进r这个函数: 可以看到有一个自执行,但是比较那个是,这里的自执行,明显跳进的是最后一个switch的情况,所以我们只要分析return e(o(n))
这个返回值就好了,n就是我们输入的密码前17位,然后我们跟进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函数: 有是一个硬编码,没啥他太大的意思,继续回到o函数,再进入i函数, 这个就有点意思,因为h返回给的是一个数组,所以这里接收到了一个数组和密码长度的8倍,然后进行了一大顿的操作,这有一个坑我等下回过头来说,看完i之后我们继续往下看a函数: 还是和之前一样,没啥意思,就是这么转了一大顿而已,好了o函数出来了,我们跟进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 ); webview.setWebChromeClient(new WebChromeClient() {}); webview.addJavascriptInterface(new MyJs(), "android" ); webview.setWebViewClient(new WebViewClient() { @Override public void onPageFinished (WebView view, String url) { 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 ); 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¶m=4300&rd=307
,抓包很容易抓到,第一个param参数是固定的,在js中可以看到: 而后面的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 ); 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中进行分享,可以关注一下我的博客。