웹 어셈블리 바이너리 보안에 논문 내용 중
C나 C++ 기반의 웹 어셈블리 바이너리에 대한 내용들 위주로 정리해봤다.
(javascript 쪽은 나중에 해보는 걸로)
상대적으로 이해가 쉬운 부분은 생략을 많이 했고 이해가 어려운 부분은 본문 내용을 거의 다 적었다.
(영어 문서를 번역기 돌리면서 봐서 오역이 있을 예정..)
1. Introduction
web assembly (이하 wasm)은 점점 더 대중화되는 바이트코드 언어이다. 이는 컴팩트하고 이식 가능한 표현, 빠른 실행 및 저수준 메모리 모델을 제공한다. 컴파일 타겟으로 설계되었으며 널리 사용되는 여러 컴파일러(예: C 및 C++용 Emscripten 또는 Rust 컴파일러)가 존재한다. 둘 다 LLVM을 기반으로 한다.
wasm은 보안을 굉장히 중요하게 생각한다. 이는 클라이언트 측 사용자가 웹 사이트에서 신뢰할 수 없는 코드들을 실행할 때 브라우저는 이를 실행한다. 스마트 컨트랙트 같이 돈이 오가는 작업을 수행할 수도 있다. 그래서 wasm 보안은 2가지 측면으로 나뉜다. 호스트 보안(host securiy)와 바이너리 보안(binary security)이다. 호스트 보안은 악성 wasm 코드로부터 호스트 시스템을 보호해야 함을 의미하고, 바이너리 보안은 무해한 wasm 코드를 악용할 수 없도록 내장된 결함 격리 메커니즘의 효율성이 중요하다. 호스트 보안에 대한 공격은 구현 버그에 의존하므로 일반적으로 특정 가상머신(VM)에만 국한된다. 바이너리 보안에 대한 공격은 각 wasm 프로그램과 해당 컴파일러 도구 체인에 따라 다르다.
wasm의 디자인에는 바이너리 보안을 보장하는 기능들이 포함되어 있다. 예를 들어, wasm 프로그램에서 유지 관리하는 메모리는 해당 코드, 실행 스택 및 기본 VM의 데이터 구조와 분리된다. 유형 관련 충돌 및 공격을 방지하기 위해 바이너리는 실행 전에 정적으로 유형을 쉽게 확인할 수 있도록 설계되었다. 또한 wasm 프로그램은 지정된 코드 위치로만 이동할 수 있다. 이는 많은 고전적인 제어 흐름 공격을 방지하는 결함 격리의 한 형태이다.
이러한 모든 기능에도 불구하고 wasm이 C 및 C++와 같이 수동 메모리 관리 기능이 있는 언어의 컴파일 대상으로 설계되었다는 사실은 다음과 같은 질문을 제기한다. "메모리 취약성이 WebAssembly 바이너리의 보안에 어느 정도 영향을 미칩니까?" wasm 문서에서는 "최악의 경우 버그가 있거나 악용된 WebAssembly 프로그램이 자체 메모리의 데이터를 엉망으로 만들 수 있습니다" 라고 말하면서 이 질문을 간략하게 설명한다. 보안에 관한 WebAssembly 설계 문서에서는 "DEP(데이터 실행방지) 및 SSP(스택 스매싱 방지)와 같은 일반적인 완화 기능은 WebAssembly 프로그램에 필요하지 않습니다."라고 결론을 내린다. 이렇게 누락된 보호 기법들은 익스플로잇을 더 쉽게 만들어주는 원인이 된다.
2. Background on WebAssembly
[Overview]
wasm은 바이너리 포맷이다. 그림 2는 간단한 wasm 프로그램을 보여준다. 모듈에는 함수, 전역 변수, 최대 하나의 선형 메모리 및 간접 호출 테이블이 포함된다. 함수나 지역 같은 프로그램 요소는 정수 인덱스로 식별된다. 편의상 텍스트 형식에서는 인덱스를 $name으로 작성할 수 있지만 해당 레이블은 바이너리에서 손실된다.
wasm 바이트 코드는 스택 기반 가상 머신에서 실행된다. 명령어는 암시적 평가 스택(evaluation stack, 그냥 스택이라고 쓰겠다)에서 입력을 팝하고 결과를 푸시합니다. 레지스터가 존재하지 않는다. 개별 값은 범위가 전체 모듈인 전역 변수와 현재 함수에만 표시되는 지역 변수에 무제한으로 저장될 수 있다. 함수는 caller나 callee가 아닌 지역 변수나 다른 함수의 스택에 액세스할 수 없다. 스택, 전역 및 지역 변수은 VM에서 관리된다.
[Types]
대부분의 기본 아키텍처와 달리 wasm는 전역, 지역 및 함수와 명령어의 인수와 결과가 입력된다. 바이너리는 실행되기 전에 정적으로 유형 검사를 받는다. 32비트 및 64비트 정수(i32, i64)와 단정밀도 및 배정밀도 부동 소수점(f32, f64)의 네 가지 기본 유형이 있다. 배열, 레코드 또는 지정 포인터와 같은 더 복잡한 유형은 존재하지 않다. 따라서 소스 수준 유형은 컴파일 중에 이러한 기본 유형으로 낮아진다.
[Control-Flow]
네이티브 코드나 Java 바이트코드와 달리 wasm에는 구조화된 제어 흐름만 있다. 함수의 명령은 잘 중첩된 블록으로 구성된다. Branch는 주변 블록의 끝으로만 점프할 수 있으며, 현재 함수 내에서만 점프할 수 있다. 다방향 분기는 분기 테이블에 정적으로 지정된 블록만 대상으로 할 수 있다. 무제한 goto 또는 임의 주소로 점프는 불가능하다. 특히 메모리의 데이터를 바이트코드 명령어로 실행할 수 없다. 따라서 wasm에서는 쉘코드 삽입이나 x86의 jmp *%reg와 같은 무제한 간접 점프 남용과 같은 공격들이 불가능하다.
[Indirect Calls]
함수 포인터와 가상 함수를 구현하기 위해 wasm에는 간접 호출이 있다. 그림 3은 작동 방식을 보여준다. 왼쪽의 call_indirect 명령어는 스택에서 값을 팝하여 테이블 섹션에 대한 인덱스로 사용된다. 테이블 항목은 이 인덱스를 나중에 호출되는 함수에 매핑한다. 따라서 함수는 테이블에 있는 경우에만 간접적으로 호출할 수 있다. 함수는 테이블에서 여러 번 참조될 수 있으며 테이블의 모든 항목을 채워야 하는 것은 아니다. 유형 정확성을 보장하기 위해 VM은 호출을 실행하기 전에 대상 함수가 간접 호출 명령에서 정적으로 선언된 유형과 유형 호환되는지 확인하고 그렇지 않으면 실행을 중단한다.
[Linear, Unmanaged Memory]
다른 바이트코드 언어와 달리 wasm은 관리되는 메모리(managed memory)나 가비지 수집을 제공하지 않는다. 대신, 소위 선형 메모리, 즉 단순히 단일 전역 바이트 배열이다. 로드 및 저장 명령어는 현재 할당된 메모리 내의 임의의 주소에 액세스할 수 있다. 메모리는 32비트 포인터로 주소가 지정되며 i32가 포인터 유형으로 사용된다. wasm 프로그램은 memory.grow 명령을 사용하여 선형 메모리를 늘리도록 VM에 요청할 수 있다. 효율적인 동적 메모리 할당을 위해 wasm 프로그램에는 일반적으로 프로그램에 malloc 및 free를 제공하여 선형 메모리를 관리하는 자체 할당자가 포함된다.
[Host Environment]
wasm 모듈은 호스트 환경에서 실행된다. 호스트 환경이 없으면 wasm 프로그램은 I/O를 수행하거나 네트워크에 액세스할 수 없다.
브라우저에서는 XmlHttpRequest, eval 또는 document.write와 같이 JavaScript 기반 클라이언트 측 웹 애플리케이션에 사용 가능한 모든 API를 가져올 수 있다. 서버 측 애플리케이션을 위한 Node.js, wasm 모듈에 자체 API를 제공하는 독립형 VM 등 다른 호스트 환경도 등장하고 있다. 예를 들어, Node.js에서 실행되는 모듈은 exec를 호출하여 셸 명령을 실행할 수 있고, 독립형 VM에서 실행되는 모듈은 WASI(WebAssembly 시스템 인터페이스)를 통해 로컬 파일 시스템과 상호 작용할 수 있다. 문자열이나 객체와 같은 비원시 데이터(Non-primitive data)는 호스트와 wasm 모듈 모두에서 액세스할 수 있는 선형 메모리를 통해 전달되어야 한다.
3. Security Analysis of Linear Memory
[Managed vs Unmanaged Data]
wasm에서는 관리되는 데이터와 관리되지 않는 데이터를 구별한다. 관리되는 데이터(예: 로컬 변수, 전역 변수, 평가 스택의 값, 반환 주소)는 VM에서 직접 처리하는 전용 스토리지에 상주한다. wasm 코드는 지침을 통해 암시적으로 관리 데이터와만 상호 작용할 수 있으며 기본 저장소를 직접 수정할 수는 없다. 예를 들어, local.get 0은 로컬 0을 읽지만 프로그램에 표시되는 로컬의 실제 기본 주소는 어디에도 없다. 관리되지 않는 데이터는 선형 메모리에 상주하는 모든 데이터이다. 완전히 프로그램의 제어를 받으며 일반적으로 컴파일러에서 생성된 코드로 구성된다.
관리되지 않는 데이터를 선형 메모리에 넣는 데는 여러 가지 이유가 있다. wasm에는 4가지 타입만 있고 관리되는 데이터는 해당 기본 유형의 인스턴스만 보유할 수 있으므로 문자열, 배열 또는 목록과 같은 모든 비스칼라 데이터는 선형 메모리에 저장되어야 한다. 관리되는 데이터에는 주소가 없기 때문에 소스 프로그램에서 주소를 가져오는 모든 변수도 선형 메모리에 저장되어야 한다. 스칼라가 아닌 유형은 소스 프로그램에서 함수 범위, 전역 또는 동적 수명이 있는 데이터로 발생하므로 컴파일러는 선형 메모리에 호출 스택, 힙 및 정적 데이터에 대한 영역을 만든다. 선형 메모리에서 컴파일러가 생성한 호출 스택을 여기선 관리되지 않는 스택이라고 부르겠다. 이는 명령어의 중간 값을 보유하는 관리되는 스택 및 로컬과 반환 주소를 보유하는 관리되는 호출 스택과 구별하기 위한 것이다. 중요한 것은 많은 데이터가 VM의 보호를 받는 것이 아니라 프로그램의 메모리 쓰기 명령에 의해 제어되는 관리되지 않는 선형 메모리에 있다는 것이다.
[Memory Layout]
네이티브 ELF 바이너리3에는 0으로 초기화된 데이터 (.bss), 읽기 및 쓰기 가능한 데이터(.data), 읽기 전용 데이터 (.rodata), 코드 (.text), 스택 및 힙에 대한 섹션이 포함되어 있다. mscripten, Clang 및 Rustc는 모두 wasm 바이너리에서 선형 메모리의 유사한 세분화를 수행한다. (그림 4 참고)
힙은 항상 선형 메모리의 끝에 배치되어야 한다. 그래야 더 높은 주소를 향해 성장할 수 있고 호스트 환경에서 요청할 때 추가 메모리를 사용할 수 있다. 힙 아래에는 스택과 정적 데이터가 있다. wasm에는 읽기 전용 메모리가 없으므로 .data와 .rodata 사이에 구분이 없으며, 메모리는 항상 0으로 초기화되므로 전용 메모리가 필요하지 않다. bss 섹션. 즉, .data, .rodata 및 .bss는 wasm에서 명시적으로 구별되지 않으며, 선형 메모리의 데이터 섹션을 참조할 때 프로그램의 전체 수명 동안 유효한 모든 데이터(예: 정적으로 초기화된 문자열 상수, 전역 배열 또는 0바이트 범위)를 의미한다.
[Memory Protections]
기본 프로그램의 가장 기본적인 보호 메커니즘 중 하나는 매핑되지 않은 페이지가 있는 가상 메모리이다. 매핑되지 않은 페이지에 대한 읽기 또는 쓰기는 페이지 오류를 유발하고 프로그램을 종료하므로 공격자는 그러한 주소에 쓰기를 피해야한다. 하지만, wasm의 선형 메모리는 구멍이 없는 단일 연속 메모리 공간이므로 모든 포인터 ∈ [0,max_mem]가 유효하다. 공격자의 모든 읽기/쓰기는가 성공한다. wasm은 설계상 선형 메모리는 점프할 수 없으므로 설계상 페이지를 읽기 가능, 쓰기 가능 또는 실행 가능으로만 표시할 수 없다. 선형 메모리의 모든 데이터는 항상 쓸 수 있다. (버퍼 오버플로우의 피해를 극대화시키는 셈이다)
기본 실행의 추가 확률적 방어로서 ASLR(주소 공간 레이아웃 무작위화)은 런타임 시 주소 공간의 스택, 힙 및 코드를 무작위로 배열한다. 하지만, wasm에는 ASLR이 없으며 선형 메모리는 결정론적으로 배열한다. 즉, 스택 및 힙 위치는 컴파일러와 프로그램에서 예측 가능하다. 가령 wasm에 어떤 형태의 ASLR을 추가하더라도 선형 메모리는 32비트 포인터로 처리되므로 강력한 보호를 위한 충분한 엔트로피를 제공하지 못할 가능성이 높다.
4. Obtaining a Write Primitive
4-1. Obtaining a Write Primitive
[Stack-based Buffer Overflow]
"스택 기반 버퍼 오버플로가 WebAssembly에 영향을 줍니까?" wasm VM은 관리되는 데이터, 특히 반환 주소를 격리하기 때문에 영향이 없다고 생각할 수도 있다. 하지만 C의 함수 범위 데이터(function-scoped data) 일부가 선형 메모리의 관리되지 않는 스택에 저장되기 때문에 버퍼 오버플로로 인해 wasm의 데이터가 손상될 수 있다. 그림 5(c)는 선형 메모리의 관리되지 않는 스택(상단)과 호출의 반환 주소를 저장하는 wasm VM의 내부 상태(하단)를 보여줌으로써 문제를 보여준다. VM 내부 상태는 VM에 의한 덮어쓰기로부터 보호되지만 관리되지 않는 스택은 그렇지 않다. 실제로 관리되지 않는 스택(예: 버퍼)의 로컬 변수에 쓰는 동안 오버플로가 발생하면 동일한 로컬 변수는 물론 스택 위쪽의 다른 스택 프레임(예: parent_frame)에 있는 다른 로컬 변수를 덮어쓸 수도 있다.오버플로는 상위 함수의 데이터와 심지어 다른 메모리 섹션에도 쓸 수 있다.
[Stack Overflow]
또 다른 쓰기 프리미티브는 과도한 또는 무한 재귀로 인해 또는 가변 크기의 로컬 버퍼가 스택에 할당되는 경우(예: alloca를 사용하여) 발생하는 스택 오버플로이다. 공격자가 스택 할당 크기를 제어하거나 재귀 함수의 내부 가정을 위반하는 손상된 입력 데이터를 제공하는 경우 스택 오버플로가 발생할 수 있다. 예를 들어, tree나 list에서 작동하는 함수의 재귀적 구현은 종종 비순환성을 가정한다. 그러한 함수에 전달된 순환 데이터 구조는 무한 재귀로 이어질 수 있다. 대부분의 기본 플랫폼에서 스택 오버플로로 인해 스택이 다른 메모리 영역과 분리되는 특수 가드 페이지로 커지면서 프로그램이 충돌하게 된다. 웹 어셈블리에서는 관리되지 않는 스택에 대해 이러한 보호 기능이 존재하지 않으므로 공격자가 제어하는 스택 오버플로를 사용하여 스택 다음에 잠재적으로 민감한 데이터를 덮어쓸 수 있다.
[Heap Metadata Corruption]
공격자가 wasm 프로그램에 메모리를 쓰기 위해 사용할 수 있는 또 다른 기본 요소는 wasm 바이너리와 함께 제공되는 메모리 할당자의 힙 메타데이터를 손상시키는 것이다. wasm에서는 호스트 환경에서 기본 할당자가 제공되지 않기 때문에 컴파일러는 컴파일된 프로그램의 일부로 메모리 할당자를 포함합니다. wasm 모듈은 일반적으로 실행 직전에 인터넷에서 다운로드되어야 하므로 할당자의 코드 크기는 중요한 고려 사항이다. 따라서 Emscripten 컴파일러를 사용하면 개발자는 dlmalloc을 기반으로 하는 기본 할당자와 최종 코드 크기를 줄이는 단순화된 할당자 emmalloc 중에서 선택할 수 있다. 마찬가지로, Rust 프로그램은 wasm로 컴파일할 때 wee_alloc이라는 보다 가벼운 할당자를 선택할 수 있다.
dlmalloc과 같은 표준 할당자는 다양한 메타데이터 손상 공격에 대해 강화된 반면, 단순하고 가벼운 할당자는 전형적인 공격에 취약한 경우가 많다. emmalloc과 wee_alloc 모두 메타데이터 손상 공격에 취약하다. free를 호출하여 메모리 chunk를 할당 해제할 때 할당자는 조각화를 방지하기 위해 가능한 한 많은 인접 free chunk를 하나의 큰 chunk로 병합하려고 한다. 이로 인해 그림 6에 표시된 전형적인 unlink 공격이 발생한다. emmalloc은 first-fit(최초 적합) 할당자이므로 할당 요청을 충족할 수 있을 만큼 큰 여유 목록의 첫 번째 청크를 반환한다. 따라서 두 번의 연속 할당 요청은 그림 6(a)의 alloc1 및 alloc2와 같이 메모리에서 서로 인접한 두 개의 청크를 생성한다. 그림 6(c)의 emmalloc 소스 코드의 1~9행은 각 청크의 메타데이터가 시작되는 것을 보여준다. 현재 청크가 사용 가능한지 여부를 나타내는 비트, 청크의 크기, 이전 청크에 대한 포인터, 마지막으로 페이로드(원시 바이트) 또는 FreeInfo 구조체 중 하나를 포함한다. FreeInfo 구조체는 청크를 doubly linked list of free chunks의 일부로 만든다.
alloc1에 데이터 오버플로가 있는 경우(예: 길이가 잘못된 memcpy로 인해) 공격자는 alloc2의 바로 인접한 메타데이터에 쓰기를 사용하여 사용된 비트를 지우고 fake FreeInfo 구조체를 설정할 수 있다(그림 6(b)). 마지막으로 alloc1이 해제되면 할당자는 새로 해제된 청크를 인접한 해제된 청크와 병합할 기회가 있는지 확인한다. 조작된 메타데이터는 다음 청크를 사용 가능한 청크로 식별하므로 할당자는 두 청크를 병합할 준비를 하기 위해 RemoveFromFreeList를 호출하여 연결을 해제한다. 그림 6(c)의 라인 13에서 emmalloc의 unlinking 코드는 공격자가 제어하는 next 필드의 값을 다른 FreeInfo 구조체의 next 필드(즉, 4바이트 오프셋)에 쓴다. 이를 통해 공격자는 임의의 주소에 임의의 값을 쓸 수 있다. 라인 14에서 next가 가리키는 위치에 미러링된 쓰기가 추가로 있다. 따라서 실행을 종료하는 런타임 오류를 방지하려면 prev와 next가 모두 유효한 포인터여야 한다. Emscripten은 5MiB의 기본 스택 크기를 할당하므로 5 × 2^20 미만의 값은 거의 안전하게 기록될 수 있다. 이는 최대 수천 범위에 있는 함수 테이블 인덱스를 덮어쓰는데 충분하다. 쓰기 프리미티브를 얻기 위한 완전 방법은 아니지만 현재 wasm에 완화 기능이 없는 기존 익스플로잇 무기고에서 가장 직접적인 방법이다. 다른 가능한 공격은 format string 취약점, use-after free 및 double-free 취약점, 단일 바이트 버퍼 오버플로를 악용하거나 메모리 관리에 대한 보다 정교한 공격을 수행할 수 있다.
4.2 Overwriting Data
[Overwriting Stack Data]
선형 메모리에서 관리되지 않는 스택은 배열, 구조체 또는 해당 값을 갖는 모든 값과 같은 함수 범위 데이터가 포함된다. native 코드와 달리 리턴 주소를 저장하지 않아 실행 흐름을 핸들링하긴 어렵지만 모든 active call frames에 접근할 수 있으며 로컬 변수의 값들을 보호 기법에 의해 종료될 위헙 없이 오버라이트 할 수 있다.
[Overwriting Heap Data]
힙은 일반적으로 수명이 더 긴 데이터를 포함하며 다양한 기능에 걸쳐 복잡한 데이터 구조를 저장한다. wasm에서는 완전히 결정적인 메모리 할당으로 인해 힙 데이터에 대한 대상 쓰기가 간단하다. 설상가상으로 선형 스택 기반이라도 충분한 길이의 버퍼 오버플로로 인해 힙 데이터가 손상될 수 있습니다. 그 이유는 모든 컴파일러에서 힙이 스택 뒤에 오기 때문이다. 오버플로는 힙 데이터를 자동으로 손상시킬 수 있다.
[Overwriting “Constant” Data]
이건 modern native platforms에선 절대 발생할 수 없는 일이다. wasm은 선형 메모리에서 데이터를 불변으로 만들 수 있는 방법이 없으므로 프로그램에서 스칼라가 아닌 상수의 값을 변경할 수 있다. 그림 4(b)의 메모리 레이아웃이 포함된 스택 오버플로는 상수 데이터에 쓸 수 있다. 마찬가지로 스택 기반 버퍼 오버플로는 그림 4(c)의 메모리 레이아웃에서 일정한 데이터에 도달 할 수 있다. 결과적으로 이러한 기능 중 하나를 가진 공격자는 프로그래밍 언어가 의도한 보장을 손상시켜 상수 데이터로 추정되는 것을 덮어쓸 수 있다.
4.3 Triggering Unexpected Behavior
[Redirecting Indirect Calls]
위에서 wasm의 간접 함수 호출을 설명했다. 공격자는 결국 테이블 섹션에 대한 인덱스 역할을 하는 선형 메모리의 정수를 덮어써서 간접 호출을 리디렉션할 수 있다. 이 정수 값은 관리되지 않는 스택의 지역 변수, 힙 개체의 일부, vtable 또는 심지어 상수 값일 수도 있다.
wasm에는 공격자가 간접 호출을 리디렉션하는 능력을 제한하는 두 가지 메커니즘이 있다. 첫째, wasm 바이너리에 정의되거나 wasm 바이너리로 내보낸 모든 함수가 간접 호출에 대한 테이블에 표시되지 않도록 하고 간접 호출의 대상이 될 수 있는 함수만 표시한다. 둘째, 직접 호출과 간접 호출 모두 유형을 확인한다. 결과적으로 공격자는 유형 기반 제어 흐름 무결성과 유사하게 동일한 유형의 기능의 동등 클래스 내에서만 호출을 리디렉션 할 수 있다.
[Code Injection into Host Environment]
wasm 모듈은 다양한 방식으로 호스트 환경과 상호 작용하여 외부에 가시적인 효과를 일으킬 수 있다. 그러한 방법 중 하나는 주어진 문자열을 코드로 해석하는 JavaScript 호스트 환경의 악명 높은 eval 함수를 호출하는 것이다. eval에 액세스하기 위해 Emscripten을 통해 컴파일된 wasm 모듈은 예를 들어 emscripten_run_script를 사용할 수 있다
[Application-specific Data Overwrite]
애플리케이션에 따라 데이터 덮어쓰기에 대한 다른 민감한 대상이 있을 수 있다. 예를 들어, 가져온 함수를 통해 웹 요청을 발행하는 wasm 모듈은 쿠키 도용을 시작하기 위해 대상 문자열을 덮어써서 다른 호스트에 연결하도록 만들 수 있다. 추가 예로서, 브라우저 에서 직접 CIL/.NET 코드를 실행하기 위해 여러 인터프리터와 런타임이 wasm으로 컴파일되었을 때, 이러한 종류의 환경에는 프로그램 동작을 크게 변경할 수 있는 많은 기회가 포함되어 있다(예: 바이트코드를 덮어쓴 다음 런타임에서 해석함).
이어지는 내용은 브라우저, node.js 서버와의 상호작용을 통한 공격, stand-alone vm에 파일 쓰기 등에 관한 내용이 나온다.
지금까지의 내용으로도 충분히 버거웠기 때문에 나중에 또 읽어보도록 하자.
읽어보면서 웹 어셈블리가 생각보다 취약할 수 있다는 점을 알았다.
https://www.usenix.org/conference/usenixsecurity20/presentation/lehmann
Everything Old is New Again: Binary Security of WebAssembly | USENIX
Open Access Media USENIX is committed to Open Access to the research presented at our events. Papers and proceedings are freely available to everyone once the event begins. Any video, audio, and/or slides that are posted after the event are also free and o
www.usenix.org