Show HN: RISC-V 어셈블리 탁상 보드게임으로 상대 해킹하기
(punkx.org)- PROJEKT: OVERFLOW는 RISC-V 어셈블리와 버퍼 오버플로우를 보드게임 규칙으로 바꿔, 메모리·스택·반환 주소 조작을 직접 따라가게 만든 학습용 게임임
- 플레이어들은 같은 메모리와 프로그램을 공유하고, 가상 메모리 없이 턴마다 10개 명령만 실행하는 선점형 스케줄링 방식으로 경쟁함
- 승부는 기존 명령을 복사해 쉘코드를 만들고, 상대의 return address를 덮어
game_over()로 보내는 흐름에서 갈림 - 잘못된 메모리 접근, 비정렬 읽기·쓰기, 불법 명령은 크래시와 예외 핸들러 실행으로 이어지며, trap 주소 변경과
nopmonkeypatch가 전략의 핵심 변수임 - 웹 플레이, 인쇄용 보드, ESP32·모바일 게임 헬퍼가 제공되지만 규칙 일부는 아직 조정 중이라 실험적인 해킹 퍼즐에 가까움
게임 목표와 실행 모델
- PROJEKT: OVERFLOW는 RISC-V 어셈블리와 버퍼 오버플로우를 탁상 보드게임으로 다루는 프로젝트임
- 핵심 목표는 기존 명령을 복사해 메모리 안에 작은 쉘코드를 만들고, 버퍼 오버플로우로 그 코드에 점프한 뒤 상대의 return address를 덮어
game_over()함수를 호출하게 만드는 것임 - 전략은 단순한 코드 실행을 넘어 예외 핸들러 설정과 monkeypatch까지 포함함
- 모든 플레이어는 같은 메모리와 같은 프로그램을 공유하고, 같은 프로세서를 시간 분할 방식으로 사용함
- 한 턴에는 10개 명령을 실행함
- 각 플레이어의 stack pointer는 서로 다른 위치에서 시작함
- 가상 메모리는 없음
빌드와 보드 생성 흐름
- 코드는
riscv64-unknown-elf-gcc로 RV32 대상에 맞춰 컴파일됨- 주요 옵션에는
-march=rv32g,-mabi=ilp32,-ffreestanding,-nostdlib,-nostartfiles,-O0등이 포함됨 -O0덕분에 머신 코드는 장황하지만 따라가기 쉬운 형태가 됨
- 주요 옵션에는
- 보드용 자료는
riscv64-unknown-elf-objdump -S -l -fd game출력물을 파싱해 만듦▲와✎명령을 수정함- 점프 오프셋을 16진수에서 10진수로 바꿈
- 어셈블리를 정리하고 소스 코드와 매칭함
- SVG를 만든 뒤 Inkscape로 PDF로 변환함
인쇄와 준비물
- 보드는 왼쪽과 오른쪽으로 나뉜 PDF를 인쇄해 사용함
- 인쇄는 A3가 선호되며, A4도 가능하지만 작음
- 준비물은
nop명령용 말 1개, trap 주소용 말 1개, 플레이어당 program counter와 stack pointer용 말 2개, 연필과 지우개임 - 웹 버전은 혼자 플레이와 친구와 함께 플레이를 지원하고, ESP32·모바일용 게임 헬퍼도 제공됨
기본 규칙과 턴 진행
- 시작 상태는 다음과 같음
- 모든 레지스터는 0으로 시작하지만 return address 레지스터
ra는 1000으로 시작함 - Player 1의
sp는 2244, Player 2의sp는 3844로 초기화됨 - 두 플레이어의
pc는main함수 시작 주소인 1000에서 시작함 - trap 말은 주소 1000에 놓음
- 미리 로드된 프로그램을 제외한 모든 메모리 주소는 0임
nop명령 말은 처음에는 보드에 놓지 않음
- 모든 레지스터는 0으로 시작하지만 return address 레지스터
- 한 턴에는 10개 명령을 실행해야 하며,
jal,beq같은 점프도 그대로 따라가야 함 - 플레이어는 적어도 1개 명령을 실행한 뒤 턴을 중단하고 남은 명령 수를 다음 턴으로 넘길 수 있음
- 누적 가능한 명령 수의 최대치는 20개임
Monkeypatch와 승리 조건
- 각 턴 시작 시 정확히 1개 명령을 실행한 뒤, 현재 플레이어들이 실행 중이 아닌 함수의 임의 주소로
nop명령 말을 옮길 수 있음 - 해당 주소에
pc가 도달하면 그 명령은 no-operation으로 동작함 nop말을 옮기면 현재 턴과 다음 턴을 잃고, 상대는 다음 턴에 최대 20개 명령을 실행할 수 있음- monkeypatch 규칙은 아직 균형이 맞지 않아 며칠마다 조금씩 바뀌고 있음
- 하드 모드에서는 상대를 해킹해
game_over()함수를 호출하게 만들면 게임이 끝남 - 어느 쪽도 상대를
game_over()로 보낼 수 없는 상태가 되면 무승부임 - 이지 모드에서는
main에서ret을 실행해 메인 루프를 빠져나가는 첫 플레이어가 승리함
특수 기호와 예외 처리
✎는li명령의 immediate 값으로 0부터 4095까지의 임의 12비트 숫자를 고를 수 있게 함▲는 load 명령에서 자신의 stack pointer 기준 ±128바이트 범위의 값을 고를 수 있게 함- 예를 들어
sp가 2180이면 2052부터 2308까지 선택 가능함
- 예를 들어
- 금지 행동은 프로그램 크래시로 이어짐
- 1192보다 낮은 메모리 주소 덮어쓰기
- 4의 배수가 아닌 주소에 대한 비정렬 읽기 또는 쓰기
- 불법 명령 실행
- 크래시가 나면 예외 핸들러가 실행되고 trap 주소로 점프함
- trap 주소는 처음에 1000이지만
set_trap()함수에서 덮어쓸 수 있음 - 예외가 발생하면 program counter가 특정 값으로 설정되고 실행을 계속함
- trap 주소는 처음에 1000이지만
- 부정행위나 실수가 적발되면 해당 플레이어의 프로그램 상태, 메모리, 레지스터가 재설정됨
3~4인 확장 규칙
- Player 3의
sp는 2116으로 설정됨 - Player 4의
sp는 3716으로 설정됨 - 3명 이상일 때
▲기호는 stack pointer에서 -128바이트 떨어진 범위에만 쓸 수 있음 - 2명보다 많은 플레이어로 진행하면 게임이 꽤 불안정해지고 빠르게 손상됨
- 승리 조건에 도달하기는 더 어려워지지만, 플레이는 더 재미있고 혼란스러워짐
해킹 전략 예시
- 크래시는 상대의 진행을 막는 공격 전략으로 사용할 수 있음
- trap handler를
game_over함수로 바꾸면 첫 번째로 크래시하는 플레이어가 패배함- 이 상태에서는
nop말이 매우 강력함 - 상대가 현재 실행 중인 함수의
ret위에nop을 올리면 상대가 패배할 수 있음
- 이 상태에서는
bug()함수에서 index를 400 또는 -400으로 overflow하면 상대의 stack에 접근해 return address를 덮을 수 있음- 예시로 주소 3784에서 2184로 이동하려면
(3784 - 2184) / 4 = 400이므로 index-400이 필요함
- 예시로 주소 3784에서 2184로 이동하려면
copy()함수로 특정 명령을 복사해 메모리에 짧은 쉘코드를 만들 수 있음- 예시 쉘코드는
li a4, ✎,li a5, ✎,sw a4, 0(a5),ret조합으로 임의 쓰기를 수행함 ret명령을 복사하면 return address가 쉘코드 시작 지점으로 설정되어 무한 루프가 됨
- 예시 쉘코드는
bug()함수에서 index를 6으로 설정하면value변수를 stack의 저장된 return address인28(sp)위에 쓸 수 있음bug()에서 반환할 때28(sp)값이 return address 레지스터로 복사됨- 이 값에 만든 쉘코드 주소를 넣으면 메모리로 점프할 수 있음
명령 해석과 변경 사항
- 모든 점프는 disassembler에서 절대 주소처럼 보이더라도 현재 program counter 기준 상대 점프임
- 예를 들어
jal a4, 0의 머신 코드 1903은 실행 시 무한 루프가 됨
- 예를 들어
- 유효한 게임 명령 목록은 머신 코드 0부터 4095까지의 RV32 JRI 명령 중
a0,a4,a5,sp,ra등을 사용하는 형태로 정리되어 있음 - 변경 로그 0.0.6에는
while(run)대신while(*prun)을 사용한 변경이 포함됨- 상대가 비정렬 역참조를 유도해 강제로 크래시를 일으킬 수 있게 됨
- NOP 규칙은 현재 실행 중이 아닌 함수에만 배치 가능하도록 바뀜
디자인과 학습 자료
- 보드 좌우의 사각형은 ASCII로 인코딩된 바이너리 메시지임
- 흰색 사각형은 1, 검은색 사각형은 0임
- 색상은 저렴한 인쇄와 흑백 프린터 가독성을 위해 빨강, 파랑, 검정, 흰색만 사용함
- 문법 강조는 사용하지 않음
- 테마에 따라 코드의 일부가 더 중요해 보이는 효과를 피하고, 직접 판단하며 집중하기 위한 선택임
- RISC-V 어셈블리 학습 자료로 riscv-programming.org, cs3410 risc-v interpreter, luplab의 rvcodecjs 등이 있음
- C 학습 자료로는 Beej's Guide to C Programming의 앞부분이 사용됨
- 변수, 함수 호출, 포인터, 문자열, 구조체, 배열, 재귀 등을 다루는 인쇄용 어셈블리 연습 PDF와 빈칸 채우기식 “assembly hangman” 버전이 제공됨
댓글과 토론
Hacker News 의견들
-
정말 인상적임. 특히 12살 딸이 이걸 같이 하게 만들었다는 점이 제일 대단해 보임
CHERI 버전은 언제쯤 기대하면 됨? :-D- “CHERI는 세밀한 메모리 보호와 확장 가능한 소프트웨어 격리를 위한 프로세서 지원을 통해 현대 C 언어 TCB의 보안을 크게 개선하려는 세 가지 핵심 설계 목표가 있으며, 서로 충돌할 때도 있는 요구사항 때문에 설계에서 신중한 조율이 필요했다”는 식이라서, CHERI 버전은 어려울 것 같음 :)
- 12살 때 6502 어셈블리를 짰었음. 요즘 컴퓨터 환경에서는 12살이 그렇게 하기 쉽지 않음
- 8비트 시절에는 컴퓨터에 입문하는 흔한 나이였음
-
Core War는 단순한 모의 어셈블리 언어를 지원하는 가상 머신의 메모리 경기장에서 하는 게임. 1984년 Scientific American에서 처음 봤고, 그때 이미 15년 정도 프로그래밍을 해왔기 때문에 Bell Labs의 더 오래된 게임 Darwin에서 영감을 받은 것임을 알아봤음
Darwin은 1961년에 만들어졌고 IBM 7090에서 실행됐음. 프로그램들이 자원을 두고 경쟁하며, 할당된 공간 전체를 복제해 장악한 프로그램이 승리하는 방식이었음. Robert Morris Sr.가 이길 수 없는 프로그램을 만든 뒤 오래가지는 못했음. [2] 참고
1970년대 중반에는 Software Practice and Experience가 가장 좋아하던 컴퓨터 과학 저널 중 하나였고, Aleph-Null이라는 필명으로 쓰인 Computer Recreations 칼럼이 자주 실렸음. 대학원 시절 그 칼럼에 나온 게임들을 여러 개 구현해 보며 즐겼음. 저널은 비싸지만, 대학생이라면 예전의 나처럼 대학 도서관에서 찾을 수 있을 가능성이 큼. 1970년대 호들은 Pascal 컴파일러, Algol 68, 동시성 프로그래밍 같은 주제가 실려 읽기 쉽고 재미있었고, N. Wirth의 글을 통해 Module[3,4]과 나중에 Oberon[5]을 알게 됐음
[1] https://en.wikipedia.org/wiki/Core_War
[2] https://en.wikipedia.org/wiki/Darwin_(programming_game)
[3] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43800701...
[4] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43800701...
[5] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43801909... -
게임은 좋아하지만 코딩 머리는 없다고 하던 친구가 있었는데, Human Resource Machine을 통해 사실상 코딩을 하게 됐고, 몇몇 해법은 수년 경력의 내 것보다 더 좋았음
- 때로는 새로운 관점이 생각보다 훨씬 큰 도움이 됨
내 12살 아이는 수학을 싫어하지만 Human Resource Machine과 SpaceChem은 놀랄 만큼 잘함. 고등학교 수학과 프로그래밍의 수학이 근본적으로 다른 건지 궁금해짐
- 때로는 새로운 관점이 생각보다 훨씬 큰 도움이 됨
-
매우 흥미로움. 오늘날 컴퓨터 메모리 크기를 생각하면 짧은 니모닉은 공학적으로 좋지 않은 선택이라고 늘 느껴왔음
여기서도 가장 먼저 해야 할 일이 명령어가 무엇을 하는지 배우고 기억하는 것임. 이름을 더 풀어쓴 형태로 바꾸면 익히기도, 기억하기도, 코드를 읽기도 훨씬 쉬워짐. 사람들이 자주 그렇게 하지 않는다는 점이 의심스러움
이런 유형의 취약점이 가능하다는 사실도 전체 시스템 설계 실패를 가리킨다고 봄. 재미있는 게임이 아니거나 배우기 좋은 방식이 아니라는 뜻은 아니지만, 공학에서 구조적 문제가 너무 쉽게 받아들여지고 있음. 대부분은 그 구조적 결함을 보지도 못할 정도임- 처음 버전에는 훨씬 읽기 쉬운 의사 어셈블리가 있었고, 그런 방향도 생각했음. 하지만 결국 딸이 objdump 출력을 편하게 읽게 하고 싶었고, 몇 개의 니모닉을 배우는 게 큰 문제라고 보지는 않음
아이들은 얕잡아 보지 않을 때 정말 잘 반응한다고 생각함. 적어도 내 아이는 그랬음
임의 읽기와 쓰기를 구조적 결함으로 보지 않는 사람이 있다고 생각하나? 수천 명이 그 문제를 다루며 꽤 진전도 만들고 있음. 동시에, 여전히 peek와 poke는 재미있다고 봄
- 처음 버전에는 훨씬 읽기 쉬운 의사 어셈블리가 있었고, 그런 방향도 생각했음. 하지만 결국 딸이 objdump 출력을 편하게 읽게 하고 싶었고, 몇 개의 니모닉을 배우는 게 큰 문제라고 보지는 않음
-
이거 정말 멋짐. 회사에서 해보고 싶음
-
꽤 재미있어 보임. 어느 연령대에 적합하다고 봄?
- 쉬운 승리 조건, 즉
bug()에서 빠른 버퍼 오버플로로 메인 루프를 빠져나가는 건 10~15살도 할 수 있다고 봄
내 딸은 12살이고 같이 재미있게 하고 있음. 어려운 승리 조건, 즉 상대를game_over()함수로 점프하게 만드는 건 더 어렵지만, 5~6개월 안에는 도달할 수 있을 것 같음
성인은 잘 모르겠음. 어떤 사람들은 어셈블리를 악마가 만든 것처럼 무서워해서, 아이들보다 플레이하게 만들기가 더 어려울 수도 있음
- 쉬운 승리 조건, 즉
-
흥미로운 건 우리가 세상을 자기 자신의 거울처럼 보는 경향이 있다는 점임
내가 버퍼 오버플로와 프로그래밍에 관심이 있으니 내 딸도 당연히 큰 관심이 있을 거라고 보는 건 얼마나 가능성이 있을까? 첫째 아이이고, 둘째 여자아이라는 점까지 있으면 확률은 더 낮아 보이는데, 그래도 많은 아빠들이 밀고 나가는 걸 봄
이런 프로젝트를 할 때, 적어도 어느 정도는 허영 프로젝트라는 걸 의식했는지 궁금함. 어쨌든 나는 이런 것에 관심이 있으니 공개해 줘서 기쁨- 자기 주장을 그럴듯하게 만들려고 사람들이 얼마나 큰 가정을 아무렇지 않게 하는지가 더 흥미로움
프로젝트 제작자가 자기 허영심 때문에 딸에게 이걸 강요한다고 암시하는데, 그 근거가 어디에 있음? 사이트 몇 페이지를 둘러봤지만 그런 걸 시사하는 내용은 전혀 못 봤고, 오히려 딸이 즐거워하고 아주 관심 있어 한다는 부드러운 표현이 여러 번 있었음
딸이 먼저 아빠가 컴퓨터에서 뭘 하는지 계속 궁금해하면서 시작했을 가능성은 왜 배제함? 작게 시작했다가, 관심을 공유하는 사람과 어린 공동 탐험가 사이의 양방향 과정으로 커졌을 수도 있음
실제로 어떤지는 나도 모르지만, 당신도 모를 것임. 교육에 몇 년 관여해 본 입장에서는 아이들이 일반적으로 믿어지는 것보다 훨씬 뛰어난 학습자임. 학교 구조도 한 이유겠지만, 핵심에는 이런 식의 제한적 믿음이 있을지도 모름. 딸과 세상에 자기 관심과 열정을 나누려 한 이 아버지에게 박수를 보내고 싶음 - 아버지로서 내가 할 수 있는 모든 걸 가르쳐 보려는 것뿐임. 때로는 프로그래밍이고, 때로는 격투이고, 때로는 명상임
그중 어떤 것은 가치가 있을 것이고, 어떤 것은 아닐 것임. 확률은 언제나 불리함. 삶이 원래 그렇다
- 자기 주장을 그럴듯하게 만들려고 사람들이 얼마나 큰 가정을 아무렇지 않게 하는지가 더 흥미로움
-
64비트 RISC-V 코드 경로가 안정화되고 충분히 잘 동작하며 “버퍼 오버플로”까지 사라지면, C/C++가 늘 문법을 바꿔 주지 않는 상황에서 계획적 노후화는 어떻게 하려는 걸까? 불쌍한 영혼들…
-
잠깐만.
어셈블리 코딩이 들어가는 테이블탑 보드게임이라고? 왜 이걸 전에 생각 못 했지? :D -
PL/I는 문자열/배열 경계 검사, 아래가 아니라 위로 자라는 스택 같은 부분을 제대로 했음
https://www.acsac.org/2002/papers/classic-multics.pdf