一、起因
那是一个阴冷的夜晚,我的一个老乡怒气冲冲的给我拨了个微信视频,说他在微信上被人骗了。
他给我转发了一条骗子给他发的聊天记录,在点开之前是这样的:
图1 聊天记录
看上去是一个图片。点进去一看,果然是个图片。里面写了 4个数字。
从图上的“错一罚十”来看,貌似是某些“大神”给人推荐的一些“神秘数字”。
值得注意的一个细节是,当我点开菜单想用浏览器去打开这个图片的时候,发现很多菜单都被隐藏了。
图2 图片
老乡怒吼到:“奶奶的,老子前天找人花了一万块钱买的这个买马的推荐单,结果今天看到的数字,跟我昨天看到的数字完全不一样!”
这么一说我就明白了。原来骗子发了个图片来推荐受害人买马,里面手写了几个数字号称“预测绝对准确”。
然而,结果出来后,骗子又第一时间把图片改掉了,从而实现“绝对正确”。
我在心里说道“。。。卧槽,这你都能信”,嘴里说道:“。。这个在原理上当然可以实现啊,图片换掉了而已。”
老乡囔囔说:“现在这骗子说,这个是前天发出来的,聊天记录有时间作证,改不了的!他说除非我能发一个这样的记录给他,如果确实能够随便变图片,他就把钱还给我。”
我叹了口气,声色俱厉的教育了他,世界上没有未卜先知的大神。
老乡唯唯诺诺道:“好的我知道了!你才是大神,快给我做一个这样的记录出来!”
二、原理
首先可以确认的一点是,图1这种聊天记录,普通微信用户是做不出来的。
因为他既不是文字,又不是图片,他是微信公众号给用户发送的文章(Article)。
而骗子为了蒙骗不懂技术的老百姓,让人认为这就是个图片,在这个文章上做了几个细节:
1、聊天窗口上显示的文章卡片既没有标题,也没有链接,只有一个图片的缩略图,在心理上给人一种暗示,这就是一个图片。
2、文章点开后,是一张大图铺满屏幕,菜单里面没有“使用浏览器打开”、“复制链接”之类的菜单,因此很难让人想到这是一个网页。
因此,要复现这个骗局,需要:
1、有一个自己的通过认证的公众号。
2、使用该公众号给受害者推送一个没有标题、链接,只有缩略图的文章。
3、文章里的链接指向自己服务器的一个网页,网页域名要备案,网页内的图片要能变化。
4、这个网页隐藏了微信菜单,如“使用浏览器打开”等。
现在基本原理已经了解了,就可以尝试复现这个骗局了。
三、技术点
骗子敢信誓旦旦的说有本事你弄一个给我看,底气在于对普通百姓而言,要复现这一套骗术还是相当有技术门槛和成本的。
下面一个一个讲解。
1、已认证的公众号
公众号如果不通过认证,在调用接口来隐藏微信菜单的时候会有局限性(由于我已经认证了,这一点没有亲自证实。)
认证需要的最简单的材料也需要个体工商户营业执照、300元的认证费用。
这部分就是费时费力费钱,跟技术关系不大。
2、给用户推送特殊的文章。
公众号给用户推送的所有信息都是用xml来描述的。可查阅微信官方文档。
不过值得吐槽的是,这个官方文档恰恰漏了推送文章的xml描述方式:
<xml><ToUserName><![CDATA[{target}]]></ToUserName><FromUserName><![CDATA[{source}]]></FromUserName><CreateTime>{time}</CreateTime><MsgType><![CDATA[news]]></MsgType><Content><![CDATA[{content}]]></Content><ArticleCount>{count}</ArticleCount><Articles>{items}</Articles></xml>
可以发现<ArticleCount>定义了一个推送里有几篇文章,每一篇文章可以组装到<Articles>中,每个文章的描述如下:
<item><Title><![CDATA[{title}]]></Title><Description><![CDATA[{description}]]></Description><PicUrl><![CDATA[{img}]]></PicUrl><Url><![CDATA[{url}]]></Url></item>
因此,只需要把Title、Description都置空,把PicUrl设置成骗子要显示的缩略图,Url设置为骗人图片的网页链接,把文章发送给用户就完成了。
3、网页显示图片,且图片可变
这里要解决的一个问题是:如果用户已经打开过这个网页,那么手机里必然有缓存。那么即使你在服务器上换掉了图片,用户还是能看到之前的图片。
解决图片缓存的思路很多。如果不想改变图片文件名,一种思路是给图片加上随机的后缀,如 1.png 改成 1.png?123456
这样,骗子只需要在适当的时候后台把1.png换掉即可。
另外一种思路是把正确的结果做成另外一个名字的图片,使用代码,设计成某种条件下切换要展示的图片文件名。
为方便演示,我准备了5张png图片,随机生成文件名来展示。JS代码如下:
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.js">
</script>
<script type="text/javascript">// 随机选择5个图片$(function(){var randName = Math.floor(Math.random() * 65535) % 5$('.img_randPng').each(function(){this.src = this.src + randName + '.png';console.log(this.src)});});
</script>
然后在img标签里做修改即可:
<img src='https://********' class='img_randPng' />
4、隐藏微信菜单
最后一步是隐藏微信自带浏览器的部分菜单。这就要使用JS-SDK来实现了。
而麻烦的地方在于:所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用:
<script src='https://res.wx.qq.com/open/js/jweixin-1.6.0.js'>
</script>wx.config({debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId: '', // 必填,公众号的唯一标识timestamp: , // 必填,生成签名的时间戳nonceStr: '', // 必填,生成签名的随机串signature: '',// 必填,签名jsApiList: ['hideAllNonBaseMenuItem'] // 必填,需要使用的JS接口列表
});wx.ready(function(){// 必须要放在这里才会生效wx.hideAllNonBaseMenuItem();
});
其中你需要调用JS的哪些api,都必须写到jsApiList中进行注册。
例如这里调用了'hideAllNonBaseMenuItem'接口来隐藏所有非必须的菜单。这个api必须在wx.read中调用。
上述配置信息注入最麻烦的地方在于需要签名,而签名需要向微信服务器请求获得jsapi_ticket,而jsapi_ticket又需要请求获得access_token。
官方文档中《获取access_token》说的足够详细了。https请求如下,要带上自己的appid和secret:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
access_token拿到之后再请求一个jsapi_ticket。https请求如下,要带上上面获得的access_token:
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
终于拿到jsapi_ticket,接下来就是签名算法了。可参阅《JS-SDK使用权限签名算法》。
Python代码如下:
# 当前访问的页面的完整URLurl = current_urlparameters = {"noncestr" : noncestr,"jsapi_ticket" : self.jsapi_ticket,"timestamp" : timestamp,"url" : current_url }# 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串unsinged_str = '&'.join(['{}={}'.format(key.lower(), parameters[key]) for key in sorted(parameters)])# 进行sha1签名,得到signaturesignedstr = hashlib.sha1(unsinged_str.encode("utf-8")).hexdigest()
noncestr为数字字母构成的随机数。
timestamp为时间戳,必须和请求jsapi_ticket的时间戳保持一致。
self.jsapi_ticket为上文获取到的jsapi_ticket。
url为准备调用JS-SDK接口的网页的url。
timestamp和noncestr随机数生成代码如下:
timestamp = int(time.time())
noncestr = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15))
将上述noncestr、timestamp、signedstr、appid插入到你的html模板中即可。
四、效果
最后展示下效果。每次打开,图片中第一个推荐的数字都会随机变化。
我把这条消息转给了老乡,说:“拿好,去干骗子吧。”
对了,这里漏了一点,为什么这个页面要备案?
因为如果不备案的话,这个网页打开一般都是这种结果:
这种情况当然你可以点申请恢复访问,但是申请的时候官方也要看你是否备案。。。
五、后续
几天后。
“怎么样,骗子退钱了没。”我问老乡。
老乡眉飞色舞的说:“退了退了,对了,这骗子还问我要你的微信,说他有一些全新的业务可以和你开展合作!
我:“。。。。。。”