웹 어셈블리(Web Assembly)는 어떻게 보안 취약점 분석을 할까요?
천천히 정리하던 글이 하나 있었는데, 드디어 글로 올리게되었습니다. 오늘 이야기드릴 것은 웹 어셈블리와 보안분석 대한 내용입니다.
웹 어셈블리는 기술 자체가 나온지는 좀 되엇지만, 보안쪽에선 최근에 블랙햇 발표로 이슈가 됬습니다. 관련 앱들이 많이 생길수록 보안적인 사항도 고려해야하기 때문에 글로 좀 정리해둘까 합니다.
웹 어셈블리로 만들어진 탱크게임 한번 하고 가시죠. -> 22년도 기준, 페이지가 없어졌네요.. 😭
Web Assembly
웹 어셈블리는 웹에서 바이너리 포맷을 처리할 수 있는 로우 레벨의 어셈블리 언어입니다. 웹 위에서 동작하지만 네이티브단에서 동작하는 느낌으로 돌 수 있고, Javascript와 함꼐 동작할 수 있어 고성능 처리와 빠른 구현 모두 어느정도 잡을 수 있는 언어라고 보시면 됩니다.
공식 홈페이지에 있는 Hello world 예제인데, 딱 C랑 동일합니다.
main.c
#include <stdio.h>
#include <sys/uio.h>
#define WASM_EXPORT __attribute__((visibility("default")))
WASM_EXPORT
int main(void) {
printf("Hello World\n");
}
/* External function that is implemented in JavaScript. */
extern void putc_js(char c);
/* Basic implementation of the writev sys call. */
WASM_EXPORT
size_t writev_c(int fd, const struct iovec *iov, int iovcnt) {
size_t cnt = 0;
for (int i = 0; i < iovcnt; i++) {
for (int j = 0; j < iov[i].iov_len; j++) {
putc_js(((char *)iov[i].iov_base)[j]);
}
cnt += iov[i].iov_len;
}
return cnt;
}
main.js
let x = '../out/main.wasm';
let instance = null;
let memoryStates = new WeakMap();
function syscall(instance, n, args) {
switch (n) {
default:
// console.log("Syscall " + n + " NYI.");
break;
case /* brk */ 45: return 0;
case /* writev */ 146:
return instance.exports.writev_c(args[0], args[1], args[2]);
case /* mmap2 */ 192:
debugger;
const memory = instance.exports.memory;
let memoryState = memoryStates.get(instance);
const requested = args[1];
if (!memoryState) {
memoryState = {
object: memory,
currentPosition: memory.buffer.byteLength,
};
memoryStates.set(instance, memoryState);
}
let cur = memoryState.currentPosition;
if (cur + requested > memory.buffer.byteLength) {
const need = Math.ceil((cur + requested - memory.buffer.byteLength) / 65536);
memory.grow(need);
}
memoryState.currentPosition += requested;
return cur;
}
}
let s = "";
fetch(x).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, {
env: {
__syscall0: function __syscall0(n) { return syscall(instance, n, []); },
__syscall1: function __syscall1(n, a) { return syscall(instance, n, [a]); },
__syscall2: function __syscall2(n, a, b) { return syscall(instance, n, [a, b]); },
__syscall3: function __syscall3(n, a, b, c) { return syscall(instance, n, [a, b, c]); },
__syscall4: function __syscall4(n, a, b, c, d) { return syscall(instance, n, [a, b, c, d]); },
__syscall5: function __syscall5(n, a, b, c, d, e) { return syscall(instance, n, [a, b, c, d, e]); },
__syscall6: function __syscall6(n, a, b, c, d, e, f) { return syscall(instance, n, [a, b, c, d, e, f]); },
putc_js: function (c) {
c = String.fromCharCode(c);
if (c == "\n") {
console.log(s);
s = "";
} else {
s += c;
}
}
}
})
).then(results => {
instance = results.instance;
document.getElementById("container").innerText = instance.exports.main();
}).catch(console.error);
잘 보시면 Javascript 코드에서 C에서 정의한 함수를 끌어쓰는 것(writev_c)을 볼 수 있습니다. 빠른 처리를 위한 작업은 c 레벨에서 처리하고, 웹에선 js로 핸들링하여 사용할 수 있는 구조를 가지고 있습니다. 반대로 putc_js()
같이 c에서 js를 호출하는 함수도 있지요
모질라쪽에선 emcc(emsdk 의 sdk tool)로 컴파일 하는 내용으로 가이드하고 있네요. wasm과 embed된 html을 바로 뽑을 수 있어서 테스트하기에 좋습니다.
#include <stdio.h>
int main(int argc, char ** argv) {
printf("Hello World\n");
}
만약 위 코드를 wasm으로 사용하는 페이지를 emcc로 만들어보면 이렇습니다.
emcc hello.c -s WASM=1 -o hello.html
그리고 wasm은 Major 브라우저들은 지원하고 있기 때문에 개발자 콘솔에서 WebAssembly 오브젝트를 불러보면 내장된 function 정보를 얻어올 수 있습니다.
#> WebAssembly
WebAssembly
CompileError: function CompileError()
Global: function Global()
Instance: function Instance()
LinkError: function LinkError()
Memory: function Memory()
Module: function Module()
RuntimeError: function RuntimeError()
Table: function Table()
compile: function compile()
compileStreaming: function compileStreaming()
instantiate: function instantiate()
instantiateStreaming: function instantiateStreaming()
toSource: function toSource()
validate: function validate()
.....
Security testing point
사실 이번 포스팅의 목적이자 메인이 되는 부분입니다. 이 생각에 천천히 정리하고 있던겁니다.
웹 어셈블리로 만들어진 어플리케이션은 무엇을 어떻게 봐야할까?
우선 웹 어셈블리가 웹 기반에서 바이너리를 실행하고 자바스크립트와 연결해서 사용할 수 있기 때문에 전통적인 바이너리 공격 + 웹 해킹 기법이 모두 적용되는 케이스가 발생할 수 있습니다.
- 전통적인 바이너리 관련 공격들…
- BOF, FSB, OOB 등등 알려진 기법들이 많이 있긴한데, Web Assembly 자체적으로 보안로직이 적용되어 있어서 ROP등은 어렵다고 합니다. (실제론 해봐야알듯?)
- 그리고 실행 구조떄문에 DEP나 SSP도 필요없어 적용되지 않았다고 하네요. 이쪽에서 가장 걱정하는건 Indirect function call 인듯 합니다. 아마… XSS 등으로 JS 제어권을 가졌을 때 할 수 있는게 너무 많아져서 그런게 아닐까 싶습니다. (정상 콜인지 구별하는것도 어려울테구요..)
- 웹 공격들…
- 기존 웹 공격과 약간 차이가 있다면, 추가적으로 웹 페이로드나 공격코드가 C단을 거쳐서 넘어올 수 있다는 점입니다.
- 웹 기반 방어로직은 당연하게 SOP 정도이고 non-web 플랫폼에선 POSIX 모델 적용이라고 합니다.
SOP도 어느정돈 강제적으로 적용된게, wasm 가져올때도 fetch 등을 이용하기 때문에 강제로.. 브라우저 SOP를 적용받게 됩니다.
fetch('https://www.hahwul.com')
Promise { <state>: "pending" }
교차 출처 요청 차단: 동일 출처 정책으로 인해 https://www.hahwul.com/에 있는 원격 자원을 차단하였습니다. (원인: ‘Access-Control-Allow-Origin’ CORS 헤더가 없음).[더 알아보기]
왜 fetch 등을 이용해서 가져올까? 라는 의문이 발생할 수 있는데요, WebAssembly는 아직 <script type='module'>
또는 ES2015 import statements와 통합되어 있지 않아 import를 사용하여 브라우저에서 가져올 방법이 없습니다. 그래서 아래 2개의 함수로 불러오는 경우가 권장되고 있습니다. 이 과정에서 대부분 fetch 등을 사용하게 되구요.
- WebAssembly.compileStreaming
- WebAssembly.instantiateStreaming
보안 관련해선 블랙햇 문서 읽어보시면 도움 많이됩니다.
Step-by-Step
아직 웹 어셈블리로 개발된 앱을 분석해 본적은 없었습니다. 무엇부터 해야할까 고민이 좀 있었는데, 자료도 찾아보고 생각도 좀 해보니 외부 실행 파일을 JS에서 핸들링해서 쓴다는점에선 SWF 분석과 많이 유사한 부분이 있더군요. 그래서 저는 전반적인 분석 방식을 SWF 보는 방식과 비슷하게 보려합니다. (바이너리 코드단 분석과 이를 이어주는 JS단 처리까지 보면 깔끔할듯 합니다)
SWF 분석 관련은 아래 링크를 참고해주세요!
- SWF(Flash) Vulnerability Analysis Techniques
- SWF Debugging with ffdec(jpexs)
- SWF 디컴파일러 FFDEC (JPEX Free Flash Decompiler) 설치 및 사용
Find wasm file on web
가장 먼저 웹 어셈블리 파일을 찾아야합니다. (wasm!!)
좋은 방법은 Javascript에서 웹 어셈을 다루는 instance를 찾아보는 방법인 것 같습니다. SWF, ActiveX 처럼 어차피 JS에서 핸들링하기 때문에 결국 코드에는 관련 함수나 주소 정보가 남아있기 마련입니다. 아까 위에서도 말씀드렸지만 웹 어셈블리의 경우 wasm을 불러올 때 instantiateStreaming
등의 함수로 가져온다고 했습니다. 함수 추적해보면 초기 로딩 부분에 가져오는 코드가 존재합니다.
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject)
.then(results => {
// Do something with the results!
});
물론, 이렇게 XMLHttpRequest, Ajax call 등으로 가져오는 경우도 있습니다. 결국은 웹 어셈블리의 instance 관련 부분을 찾는게 핵심이 되겠네요.
request = new XMLHttpRequest();
request.open('GET', 'simple.wasm');
request.responseType = 'arraybuffer';
request.send();
request.onload = function() {
var bytes = request.response;
WebAssembly.instantiate(bytes, importObject).then(results => {
results.instance.exports.exported_func();
});
};
그리고 웹 브라우저의 개발자 도구 > Source 관련 부분에서 wasm파일에 대해 볼 수 있습니다. 해당 부분을 통해 wasm 파일을 다운로드할 수 있고, 브라우저상에서도 어느정도 내용을 볼 수 있어서 브라우저 디버거 만으로도 간단하게 분석 정도는 가능합니다.
wasm 파일의 헤더는 .asm
으로 시작됩니다.
hexdump -C fail.wasm
00000000 00 61 73 6d 01 00 00 00 01 85 80 80 80 00 01 60 |.asm...........`|
00000010 00 01 7f 03 82 80 80 80 00 01 00 06 81 80 80 80 |................|
00000020 00 00 07 8b 80 80 80 00 01 07 66 61 69 6c 5f 6d |..........fail_m|
00000030 65 00 00 0a 8d 80 80 80 00 01 87 80 80 80 00 00 |e...............|
00000040 41 01 41 00 6d 0b |A.A.m.|
00000046
Decompile wasm & find info
wasm을 확인했다면 디컴파일하여 분석을 시작합시다. git 좀 뒤져보면 디컴파일러 여러개 나오는데, 전 이게 가장 괜찮았던 것 같습니다. 공식에서 제공하는 웹 어셈블리 관련 바이너리 툴이고 디컴파일러 등도 내장하고 있어 편하게 쓸 수 있습니다.
내용을 보고 분석하는 것과 못보고 분석하는건 어마어마한 차이가 있죠. wasm도 브라우저 외부의 모듈과 통신하며 동작한다는 점을 봐선 swf, activex 분석과 굉장히 비슷합니다.
git clone --recursive https://github.com/WebAssembly/wabt
apt install clang // (clang이 없는 경우)
make
받고 빌드하면 wasm 관련 도구들이 여러개 확인됩니다.
#> cd gin
#> ls
CMakeCache.txt dummy.c wasm-interp wasm2wat
CMakeFiles hexfloat_test wasm-objdump wast2json
Makefile liblibgtest.a wasm-opcodecnt wat-desugar
cmake_install.cmake libwabt.a wasm-strip wat2wasm
config.h spectest-interp wasm-validate
dummy wabt-unittests wasm2c
Object dump
wasm-objdump
명령을 통해 objdump와 비슷하게 wasm 파일 구조를 살펴볼 수 있습니다.
./wasm-objdump -xd fail.wasm
fail.wasm: file format wasm 0x1
Section Details:
Type[1]:
- type[0] () -> i32
Function[1]:
- func[0] sig=0 <fail_me>
Global[0]:
Export[1]:
- func[0] <fail_me> -> "fail_me"
Code Disassembly:
00003a <fail_me>:
000040: 41 01 | i32.const 1
000042: 41 00 | i32.const 0
000044: 6d | i32.div_s
000045: 0b | end
Decompile
wasm2asm
, wasm2c
등 wasm을 c코드나 어셈, json 등으로 다시 디컴파일 할 수 있습니다.
./wasm2c fail.wasm
#ifndef WASM_H_GENERATED_
#define WASM_H_GENERATED_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include "wasm-rt.h"
#ifndef WASM_RT_MODULE_PREFIX
#define WASM_RT_MODULE_PREFIX
#endif
#define WASM_RT_PASTE(x, y) WASM_RT_PASTE_(x, y)
#define WASM_RT_ADD_PREFIX(x) WASM_RT_PASTE(WASM_RT_MODULE_PREFIX, x)
/* TODO(binji): only use stdint.h types in header */
typedef uint8_t u8;
typedef int8_t s8;
typedef uint16_t u16;
c코드로 디컴파일 되기 때문에 코드를 기반으로 테스트할 수 있는 부분이 많아지죠. 여기까지 보면 우리는 wasm에서 사용할 수 있는 function들과 어떤 기능을 하는지 알 수 있습니다. 이는 맨위에서 이야기드렸듯이 javascript와 wasm간 서로 function 호출이 가능하기 때문에 양쪽으로 테스틑 해볼 수 있습니다.
Call function
이제부턴 진짜 SWF나 ActiveX랑 동일합니다. 우리가 wasm으로 들어가는 입력 구간을 Javascript에서 제어할 수 있기 때문에 웹 어셈블리의 instance를 받은 객체부터 하위 function을 실행해가며 체크하시면됩니다.
역으로 wasm에서 javascript로도 데이터를 줄 수 있기 때문에 SOP를 우회할 수 있는 CORS 적용 범위 여부, 동일 도메인에 파일 업로드가 가능한지?(wasm 올라가면 공격자가 재 구성한 웹 어셈 파일을 로드하게 되고, 사용자는 의도하지 않은 행위를 수행할 수 있으니깐?) 등 여러가지가 있을듯합니다.
보편적인 웹 공격, 바이너리 공격 방식에 여러분들의 Offensive한 생각들을 더하면 재미있는 취약점들을 찾아낼 수 있을거라 생각합니다 😎
Conclusion
요즘들어 글쓰기가 뭔가 힘드네요(핑계), 요즘 자꾸 간단하고 메모성 글만 올려서 마음이 좀 그랬습니다..
원래 웹 어셈블리의 보안에 대한 이야기를 크게 쓸까 하다가.. 생각보다 고민해봐야할 여지가 많아서 분석 방법에 대한 글만 먼저 쓰게 되었습니다. (보안 관련은 그냥 블로그에서 다루긴 좀 버겁네요.. / 시간이 없음, 그냥 각자 회사에서 고민해보는걸로 ..ㅋㅋㅋㅋ)
아 물론 이런 방식이 분석 방법이다는 아닙니다. 그냥 생각난대로 써본거고, 관련 분석을 계속 해봐야 프레임이 잡히지 않을까 싶네요. 여러가지 방법들이 있을테니 공유주시면 정말 감사하겠습니다.
블로그 그냥 취미로 하는거라 작은 시간들 모아가며 글을 씁니다. 이상하거나 잘못된 부분이 많을 수 있으니 양해 부탁드리며 댓글로 폭풍 지적질 부탁드려요..!! 조만간 테스트용 웹 어셈 하나랑 벡터들 조금 정리해서 글 올려보도록 하겠습니다.
References
- https://webassembly.org/docs/security/
- https://i.blackhat.com/us-18/Thu-August-9/us-18-Lukasiewicz-WebAssembly-A-New-World-of-Native_Exploits-On-The-Web-wp.pdf
- https://developer.mozilla.org/ko/docs/WebAssembly