npm 패키지 제작기 5
코드 난독화
웹서핑을 하다보면 웹사이트의 JS 소스코드를 살펴볼 때가 있다. 이 때, 뭔 코드가 이렇게 생겼나 싶게 알아보기 힘든 코드들이 있다.
처음봤을 땐 이게 뭔가 했지만 이게바로 코드 난독화가 적용된 것이라는 걸 알게됐다.
코드 난독화 과정을 찾다보니 대개 2개의 과정으로 이루어지는 것으로 확인했다.
Minify, Uglify
첫번째 과정은 코드의 공백이나 줄바꿈을 줄임으로서 코드의 양을 줄이고, 알아보기 힘들게 하는 과정이다.
여기에 추가적으로 변수명을 알아보기 힘들게 바꾸는 등의 추가작업이 가능하다.
Obfuscate
두번째 과정은 코드를 더욱 읽기 힘들도록 하는데, 내가 사용한 javascript-obfuscator는 아래와 같은 기능을 제공하고있었다.
- variables renaming
- strings extraction and encryption
- dead code injection
- control flow flattening
- various code transformations
- etc…
변수명을 바꾸거나 코드를 인코딩하고, 가짜 코드를 넣고 코드 흐름을 꼰다거나 등의 과정을 통한다.
예시
말로만 들으면 잘 이해가 안될 수 있으니, 실제로 어떻게 적용되는지 살펴보자.
아래 예시는 내가 개발한 모듈의 한 함수의 코드를 가져온 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {parse} from "marked";
import prettify from "@liquify/prettify";
/**
* parse Markdown Text to Grouped Html text.
* @param {string} markdownText text data written in markdown syntax
* @param {number} minLevel min level to start grouping
* @param {string} selector Selector that you want to use between id and class.(Possible values - All upper or lower cases of "class" and "id")
* @param {string} prefix prefix string of class or id.( "_" -> \<section class="_h1-1"> )
* @param {string} postfix postfix string of class or id.( "--" -> \<section class="_h1--1"> )
* @returns {string} html text data grouped by section tag
*/
export const parseToGroup = (markdownText, minLevel=1, selector="id", prefix="_", postfix="-") => {
selector = selector.toLowerCase();
if(!["id", "class"].includes(selector.toLowerCase())) {
throw new Error(`"${selector}" is invalid name for a selector.`);
}
if(!markdownDoc) {
markdownDoc = new MarkdownDocument();
}
markdownDoc.setDocument(false, markdownText, minLevel, 1, "", selector, prefix, postfix);
return markdownDoc.html;
}
여기에 일단 uglify 를 적용해보자. 결과는 다음과 같다.
1
import{parse}from"marked";import prettify from"@liquify/prettify";const parseToGroup=(o,r=1,e="id",t="_",a="-")=>{if(e=e.toLowerCase(),["id","class"].includes(e.toLowerCase()))return(markdownDoc=markdownDoc||new MarkdownDocument).setDocument(!1,o,r,1,"",e,t,a),markdownDoc.html;throw new Error(`"${e}" is invalid name for a selector.`)};export{parseToGroup};
공백이나 줄바꿈이 사라져서 한줄의 코드가 되었고, 함수의 매개변수, 지역변수들 등의 변수명이 변환되었다. 읽기 힘들어진것은 맞지만 사람이 확인할 수는 있는 정도이다.
이 과정까지는 코드의 압축 자체로도 사용이 가능하다.
여기서 조금 더 나아가서 위 결과에 obfuscate 까지 적용한 결과를 살펴보자.
1
(function(_0x360e50,_0x11d74e){const _0x3b03f8=_0x2bf0,_0x2a247b=_0x360e50();while(!![]){try{const _0x52477e=parseInt(_0x3b03f8(0x18f))/0x1*(-parseInt(_0x3b03f8(0x189))/0x2)+parseInt(_0x3b03f8(0x18c))/0x3+-parseInt(_0x3b03f8(0x191))/0x4+parseInt(_0x3b03f8(0x192))/0x5+parseInt(_0x3b03f8(0x197))/0x6*(-parseInt(_0x3b03f8(0x18a))/0x7)+-parseInt(_0x3b03f8(0x194))/0x8*(-parseInt(_0x3b03f8(0x18b))/0x9)+-parseInt(_0x3b03f8(0x18d))/0xa*(-parseInt(_0x3b03f8(0x18e))/0xb);if(_0x52477e===_0x11d74e)break;else _0x2a247b['push'](_0x2a247b['shift']());}catch(_0x294a39){_0x2a247b['push'](_0x2a247b['shift']());}}}(_0x137f,0x7700f));function _0x137f(){const _0x2eebd8=['194968FGkgmd','includes','toLowerCase','6VpemSW','315890jqWUCe','6385911WsmNob','180omOdmR','1416930hDCHOS','10bHPEaU','8642051ECnDxB','1ntaWqA','\x22\x20is\x20invalid\x20name\x20for\x20a\x20selector.','2464172YqZFQQ','2141645SHNnHc','class'];_0x137f=function(){return _0x2eebd8;};return _0x137f();}import{parse}from'marked';import _0x14ab5b from'@liquify/prettify';const parseToGroup=(_0x2ff51c,_0x30b819=0x1,_0x30ac68='id',_0x4f290c='_',_0x1feabc='-')=>{const _0x1bc1a4=_0x2bf0;if(_0x30ac68=_0x30ac68[_0x1bc1a4(0x196)](),['id',_0x1bc1a4(0x193)][_0x1bc1a4(0x195)](_0x30ac68[_0x1bc1a4(0x196)]()))return(markdownDoc=markdownDoc||new MarkdownDocument())['setDocument'](!0x1,_0x2ff51c,_0x30b819,0x1,'',_0x30ac68,_0x4f290c,_0x1feabc),markdownDoc['html'];throw new Error('\x22'+_0x30ac68+_0x1bc1a4(0x190));};function _0x2bf0(_0x375df5,_0xc65535){const _0x137f29=_0x137f();return _0x2bf0=function(_0x2bf02f,_0x868080){_0x2bf02f=_0x2bf02f-0x189;let _0x1d712d=_0x137f29[_0x2bf02f];return _0x1d712d;},_0x2bf0(_0x375df5,_0xc65535);}export{parseToGroup};
바로 이맛아닙니까….
사람이라면 이 코드를 건드리지 않고 눈으로만 확인하는건 불가능할 것이라고 확신한다.
코드의 양이 좀 늘어나서 압축의 목적으로는 적절하지 않다. 실제로 위 과정은 부득이하게 코드를 공개해야 할 때 소스코드의 로직을 감추고 싶을 때 사용한다고 한다.
사용방법
혹시나가 역시나 위 과정을 간단하게 사용할 수 있는 모듈들이 이미 존재한다.
바로 gulp 라는 모듈이다.
이 모듈이 뭔지는 소개글에서 간단하게 긁어왔으니 아래 내용을 읽어보자.
What is gulp?
- Automation - gulp is a toolkit that helps you automate painful or time-consuming tasks in your development workflow.
- Platform-agnostic - Integrations are built into all major IDEs and people are using gulp with PHP, .NET, Node.js, Java, and other platforms.
- Strong Ecosystem - Use npm modules to do anything you want + over 2000 curated plugins for streaming file transformations
- Simple - By providing only a minimal API surface, gulp is easy to learn and simple to use
간단하게 말하면 귀찮고 오래걸리는 개발 과정을 자동화해주는 모듈이다.
나의 JS 파일에 적용할 다양한 과정들을 간단한 코드로 자동화 할 수 있다.
기본적인 설치는 다음과 같다.
1
2
npm i gulp --save-dev
npm i gulp -g
모듈의 코드를 난독화하는데만 사용할 거니까 dev dependency로 로컬에 설치하고, global로도 설치해 cli로 사용할 것이다.
설치가 완료되면 자동화하고싶은 로직을 작성해야 하는데, 이를 프로젝트 경로에 gulpfile.js
로 만들면 된다.
로직 작성
사용방법이 아주 간단하므로, 바로 사용 예시를 살펴보자.
살펴볼 예시는 위에서 설명했던 난독화 과정을 자동화하는 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var gulp = require('gulp');
var uglify = require('gulp-uglify');
const javascriptObfuscator = require('gulp-javascript-obfuscator');
const rename = require('gulp-rename');
gulp.task("compressBIN", function() {
return gulp.src('./src/**/bin.mjs')
.pipe(uglify())
.pipe(javascriptObfuscator())
.pipe(rename(path=>({
...path,
basename: "index",
extname: ".mjs"
})))
.pipe(gulp.dest('bin/', {
overwrite: true
}));
})
위의 코드를 하나씩 살펴보자.
1. gulp.task(taskName, taskFunction)
gulp에서는 자동화할 개발 과정을 task
라는 단위로 묶어서 사용한다.
첫번째로 자동화할 로직의 이름을 지정하고, 실제로 실행될 로직을 return하는 함수를 지정해주면 된다.
2. gulp.src("소스파일의경로")
task
에서 사용할 소스 파일을 지정해주는 함수라고 생각하면 된다.
3. pipe(사용할 플러그인들)
소스 파일에 적용하고 싶은 개발 과정들을 하나씩 pipe 함수를 사용해서 엮어주면 된다.
원하는 기능들에 해당하는 플러그인 모듈들을 설치하고 사용하는데, 나는 난독화를 위한 gulp-uglify
, gulp-javascript-obfuscator
와 결과파일의 이름을 변경하기 위한 gulp-rename
을 사용했다.
4. gulp.dest("저장경로", 옵션)
마지막 pipe 에 개발 과정을 거친 결과를 저장할 경로를 지정하면 된다.
설명이 되게 간단해서 좀 그렇긴 한데, 실제로도 어려운 과정이 따로 필요없는 좋은 모듈이기 때문인 것 같다.
로직 실행
위에서 gulpfile.js
에 작성한 로직(task)를 실행해보자.
global 로 gulp 를 설치해놨기 때문에, gulpfile.js
와 같은 경로 내에서 명령어를 실행하면 된다.
1
2
# gulp task명
gulp compressBIN
위 명령어를 실행하고, 결과파일이 잘 저장됐는지 확인하면 된다.