# CyberConnect功能逆向与EasyCyberConnect的实现 **Published by:** [PictorHugo](https://paragraph.com/@pictorhugo/) **Published on:** 2022-05-29 **URL:** https://paragraph.com/@pictorhugo/cyberconnect-easycyberconnect ## Content 前言 在我学过的知识中,计算机知识是最开放最容易获取的。在计算机领域中,前端技术又是更开放的一个存在,因为浏览器的存在,你可以随意查看一些大项目的前端源代码而不需要谁给你任何权限,故在前端领域中逆向某个项目是最容易的了。逆向这个词,可以简单理解为根据结果推导过程,一般解密也就是对未开放源码的程序进行的,因为没有源码,我们只能对程序本身进行解密等操作,从而猜测其实现是什么。今天的主角CyberConnect作为一个社交图谱的web3应用,目前其对用户主要开放的就是cyberconnect.me这个站点。逆向部分就是cyberconnect的follow,查看follwers、followings,注册等功能。根据逆向的代码还实现了一个简易版本的cyberconnect前端,链接如下。 https://easycyberconnect.000webhostapp.com/ 目前支持搜索某个地址所有的followers、followings,不只是前50个哦,而且支持follow那些未关注的followers。假设你是一个大v,有很多人关注(followers可能达到了10000+),现在你想回关一些followers,现在无论是通过cyberconnect.me还是galaxy,还是 http://47.102.205.94:2000/ 目前都没办法很方便的去操作。我上面的easycyberconnect也就是解决了这个痛点。 除了解决自己的痛点外,自己做这个事情也是以学习为目的。自己作为一个年轻的程序员,对任何新技术都还抱有着热情,而且光有热情还是不够的,还要参与进来。那么通过一个个小的项目,从做中学,是一个很好的开始。希望自己可以坚持,我也会将自己的学习过程分享给大家。 EasyCyberConnect开源 https://github.com/PictorHugo/EasyCyberConnect 大部分朋友可能并不关注实现的细节,这里我把源代码开源出来,从而也让大家使用起来更放心。因为这个项目只是一个纯前端的实现,我将它架设在了000webhostapp上,这样也不需要掏任何服务器费用。想尝试丝滑般关注体验的朋友,可以尝试下下面的链接。 https://easycyberconnect.000webhostapp.com/ EasyCyberConnect源码逆向 这一部分记录了我对cyberconnect的研究,属于自己对学习过程的一个记录与整理。不感兴趣的朋友可以直接跳过。 在逆向前我要划定自己逆向的范围,因为cyberconnect确实是个很庞大的项目,像无头苍蝇一样到处看,必定是不行的。我逆向的目的如下: 查看超过top 50外的followers和followings 如何follow某个用户 确定了范围,接下来就要看具体方法了。从程序员的角度看,cyberconnect app也就是客户端就是一个前端程序,而且现在普遍的前端与后端交互的方式就是REST API。那么,我们只要获得,上面目的 中对应功能的REST API,并且了解REST API的各个参数是如何获得的,那么这个任务也就完成了。 Task 1: 查看超过top 50外的followers和followings 我们从galaxy的前端入手(原因是galaxy比较快一点),先了解下是如何获得top 50的followers的,那么也许可以猜测出如何查看超过top50的followers。这里就要利用chrome的一个强大的抓包功能(在galaxy.eco页面上单击鼠标右键 → 点击inspect或者检查 → 切换到Network或者网络),通过这个抓包功能我们能看到每个从前端发送出去的HTTP/HTTPs请求,那么只要耐心,我们一定可以找到一些线索。 下面就是我找到本次任务中API的过程 打开浏览器抓包功能 打开galaxy.eco/galaxyid这个页面 查看每一个请求的响应,看看是否有包含followers的信息 galaxy用于显示top 50 followers/follwings的API 通过以上方法,和一点耐心,终于找到了这个API,具体的API格式如下。这里可以看到比较重要的就是API BODY部分,如果理解了这一部分那么完成本task就不是问题了。API BODY又分为两个部分,一部分是query,这里使用的一种与graphql相关的语句,具体的作用就是查询后端服务器的哪些数据,因为我们没法看到后端的实现是什么,那么这一部分大概率先保持原状。初步理解这个graphql语句就当成一个函数,我们给它某些输入参数,这个函数就会返回一个JSON结构的数据,里面包含有followers和followings。另一部分是variables,目前看里面只有两个参数,分别是address和first,address就是当前的cyberconnect账户,first结合response结果也很好理解,也就是显示前50个数据。那么到这里我们也就有个想法,是不是修改first对应的值就可以了呢?比如改成100?我尝试后改成100还是不行,仍然只显示前50个数据。不过通过抓包发现cyberconnect的主站点上,在调用这个API的时候还多加了一个参数after,这个成为了本问题的突破口。 REST API请求: HTTP METHOD: POST API PATH: api.cybertino.io/connect API BODY: { "query": "query GetIdentity($address: String!, $first: Int, $after: String) {\n identity(address: $address) {\n address\n ens\n social {\n twitter\n __typename\n }\n followerCount(namespace: \"\")\n followingCount(namespace: \"\")\n followers(first: $first, after: $after, namespace: \"\") {\n pageInfo {\n ...PageInfo\n __typename\n }\n list {\n ...Connect\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n __typename\n}\n\nfragment Connect on Connect {\n address\n ens\n alias\n namespace\n __typename\n}\n", "variables": { "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa", "first": 50 } } REST API响应 (… 代表下面内容基本与上面一致,为了简洁考虑所以省略了): { "data": { "identity": { "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa", "ens": "pictorhugo.eth", "social": { "twitter": "PictorHugo", "__typename": "Social" }, "followerCount": 196, "followingCount": 238, "followings": { "pageInfo": { "startCursor": "0", "endCursor": "49", "hasNextPage": true, "hasPreviousPage": false, "__typename": "PageInfo" }, "list": [ { "address": "0xeefc6347cb77bbc878dd4ea0a616da69d7bc67f3", "ens": "", "alias": "", "namespace": "Galaxy", "__typename": "Connect" }, ... ], "__typename": "ConnectionIdentityPage" }, "followers": { "pageInfo": { "startCursor": "0", "endCursor": "49", "hasNextPage": true, "hasPreviousPage": false, "__typename": "PageInfo" }, "list": [ { "address": "0xee857065cbe599febfa772e96c17996deacd7830", "ens": "", "alias": "", "namespace": "CyberConnect", "__typename": "Connect" }, ... ], "__typename": "ConnectionIdentityPage" }, "__typename": "UserIdentity" } } } 这个就是从cyberconnect上查找到带after参数的接口,其实这个信息也可以通过graphql语句得到,但因为一开始不是很熟悉它的语法没有注意到。 带有after参数的请求 经过一些尝试,这个after参数里填写的也是一个地址string,而且通过这个after就可以实现翻页功能。具体意思就是,当after为空的时候,查找前first个数据,假设分别是,follower1,follower2,… folower50。在下一次执行查询语句的时候,将上一次的follower50的address填入after,first仍然是50,那么就可以查找到top50后面的数据。 Task 2: 如何follow某个用户 想获取follow某个用户的API很容易,不用像task1里那样,从整个抓包数据中一点点过滤。因为我们只需要触发一下follow这个操作,那么接下来抓去到的请求一定与follow操作相关。抓去到的API如下。 REST API请求: HTTP METHOD: POST API PATH: api.cybertino.io/connect API BODY: { "operationName": "connect", "query": "mutation connect($input: UpdateConnectionInput!) {connect(input: $input) {result}}", "variables": { "input": { "fromAddr": "0x037aa2a37b06699f9ea40c4830275620d367fefa", "toAddr": "0xf601d1f8e82fc71514e88bc2b0a26d8a305f9985", "alias": "", "namespace": "Galaxy", "signature": "...", "signingKey": "...", "operation": "{\"name\":\"follow\",\"from\":\"0x037aa2a37b06699f9ea40c4830275620d367fefa\",\"to\":\"0xf601d1f8e82fc71514e88bc2b0a26d8a305f9985\",\"namespace\":\"Galaxy\",\"network\":\"ETH\",\"timestamp\":1653799783858,\"alias\":\"\"}", "network": "ETH", "type": "FOLLOW" } } } REST API响应: { "data": { "connect": { "result": "SUCCESS" } } } 这个API的获取难度非常低,但难点在于如何知道API请求中各个参数都应该怎样填写。下面让我们来按个分析一下。 operation,的值为connect,这应该代表follow操作的意思,因为我也同时抓去到了unfollow的请求,其中operation的值为disconnect。 query,其值依然是一个graphql语句,我们暂且不去理解细节。 下面分析variables中input里的各个参数。 fromAddr,其值是连接cyberconnect的钱包地址 toAddr,将要关注的钱包地址 alias,暂时不清楚作用 signature,将某些数据经过浏览器生成的私钥(为了方便,后面称其为私钥a)签名后的字符串,具体计算逻辑查看下面`signature与signingKey计算逻辑`那一节。 signingKey,私钥a对应的公钥经过hash后的值,具体可以查看`signature与signingKey计算逻辑`。 operation,包括follow动作的发起人,将要follow的人等信息,具体和上面介绍过的都差不多,只是没有signature和signingKey等参数。 network,目前应该可以填写ETH和Solana两种。 type,是操作的名字。 浏览完上面每个参数的介绍后,我们可以发现,大部分参数都是很好获取的,比如network,type等,都是固定填写就好。但signingKey与signature就不太好理解了,下面让我们重点关注这两个参数。 signature与signingKey计算逻辑 为了获得这两个参数的计算逻辑,我使用了两种方法,一种是归纳法,一种是查看源码法。 归纳法: 我尝试了多次的follow操作,发现signingKey是一直不变的,从而可以断定这是一个存储在本地的值,那么也就很有可能是连接本网站的钱包地址经过hash后得到的值(后面被证明是错误的,请继续看)。但是signature却次次不一样,一种可能的猜测是,每次计算签名的时候加入了时间戳信息。 查看源码法: 光靠归纳的方法去猜测肯定是不行了,还是得看源码,这也是前端代码的一个好处,那么接下来的问题就是: 查看哪个具体文件的源码 如何看源码 第一个问题,chrome的抓包功能帮我们解决了,下面截图中有一列叫做initiator,这里就代表了这个请求发起的时候所对应的代码。 查看请求对应文件 源码文件 但当我们打开这个源码的时候却发现是这样的,基本上不具有任何可读性,这个时候我们可以通过点击左下角的按钮从而格`{}`式化源码文件,但也只能是让格式变的好看点,各种变量名仍然是不具有可读性的。下面就只能靠耐性了,说实在的,接下来是有点枯燥的,因为只能一点点去钻研这类minified(前端的代码压缩)过的代码,我下面抽取一段,分享一下基本的思路。下面就是当用户出发follow按键的时候会触发的函数,其核心逻辑就在switch case语句中,如果熟悉js的async/await代码实现原理的朋友看下面的代码应该并不陌生,或者大家可以查看下面这篇文章。其是就是一种状态机的转换。我下面实际的说一下,首先当a.lable为0的时候执行相应的语句,其返回值为this.getAddress(),当a.label为1的时候执行相应的语句,并且通过a.sent可以获得a.label为0时候的返回值,接下来就以此类推。 http://codereform.com/blog/post/es6-generators-and-asyncawait-in-typescript/ e.prototype.connect = function(e, t, n) { var r, o, s, c; return void 0 === t && (t = ""), void 0 === n && (n = x.ConnectionType.FOLLOW), i(this, void 0, void 0, (function() { var i, u, f, d, l, h, b; return a(this, (function(a) { switch (a.label) { case 0: return a.trys.push([0, 8, , 9]), i = this, [4, this.getAddress()]; case 1: return i.address = a.sent(), [4, this.authWithSigningKey()]; case 2: return a.sent(), u = { name: n.toLowerCase(), from: this.address, to: e, namespace: this.namespace, network: this.chain, timestamp: Date.now(), alias: t }, [4, (0, E.signWithSigningKey)(JSON.stringify(u), this.address)]; case 3: return f = a.sent(), [4, (0, E.getPublicKey)(this.address)]; case 4: return d = a.sent(), l = { fromAddr: this.address, toAddr: e, alias: t, namespace: this.namespace, signature: f, signingKey: d, operation: JSON.stringify(u), network: this.chain, type: n }, [4, (0, v.follow)(l, this.endpoint.cyberConnectApi)]; case 5: return h = a.sent(), "INVALID_SIGNATURE" !== (null === (r = null === h || void 0 === h ? void 0 : h.data) || void 0 === r ? void 0 : r.connect.result) ? [3, 7] : [4, (0, E.clearSigningKey)()]; case 6: throw a.sent(), new y.ConnectError(y.ErrorCode.GraphqlError,null === (o = null === h || void 0 === h ? void 0 : h.data) || void 0 === o ? void 0 : o.connect.result); case 7: if ("SUCCESS" !== (null === (s = null === h || void 0 === h ? void 0 : h.data) || void 0 === s ? void 0 : s.connect.result)) throw new y.ConnectError(y.ErrorCode.GraphqlError,null === (c = null === h || void 0 === h ? void 0 : h.data) || void 0 === c ? void 0 : c.connect.result); return [3, 9]; case 8: throw b = a.sent(), new y.ConnectError(y.ErrorCode.GraphqlError,b.message || b); case 9: return M.DFLAG && this.ceramicConnect(e, t), [2] } } )) } )) } 通过上面的方式,我通读了这一部分的代码,总结了一个follow过程的流程图给大家。具体的代码大家可以查我已经开源的代码。 获取signature signingKey的流程 研究这一部分后,主要获得一个信息,就是对数据进行签名的私钥并不是连接钱包的账户的私钥,而是前端自己生成的一个公钥和私钥对。这样确实也能让follow操作更丝滑一点,因为每次都要用户自己签名一次就去follow一个人还是挺费劲的。 key的注册 在我以为理解了signature的计算后就能自己脱离cyberconnect前端去follow用户的时候,我发现cyberconnect还是返回了一个error,不能让我成功follow,仔细推测后,发现有个流程被我漏掉了,就是当前端无法从indexed db超找到keypair的时候其实是会发送一个registerKey的请求,因为我漏掉了这个请求所以follow会失败。下面就是register key的REST API。 REST API请求: HTTP METHOD: POST API PATH: api.cybertino.io/connect API BODY: { "operationName": "registerKey", "query": "mutation registerKey($input: RegisterKeyInput!) {\n registerKey(input: $input) {\n result\n }\n }", "variables": { "input": { "address": "0x037aa2a37b06699f9ea40c4830275620d367fefa", "message": "I authorize CyberConnect from this device using signing key:....", "signature": "....", "network": "ETH" } } } 那么到此,我们也基本上理解了如何去通过REST API去follow一个用户。 ## Publication Information - [PictorHugo](https://paragraph.com/@pictorhugo/): Publication homepage - [All Posts](https://paragraph.com/@pictorhugo/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@pictorhugo): Subscribe to updates - [Twitter](https://twitter.com/PictorHugo): Follow on Twitter