요약
V8의 스택 트레이스 훅에 Function 생성자를 끼워 괄호 없이 임의의 자바스크립트를 실행하는 방법이 나왔습니다.
웹 보안 우회 기법을 연재해 온 블로그 blog.ikaes.de의 글입니다. 괄호 없이 함수를 부르는 방법은 이미 여럿 알려져 있지만, 작성자는 오류 처리 기능을 이용한 경로를 하나 더 찾아 공개했습니다.
왜 중요한가
- 스택 트레이스(오류가 난 지점까지의 호출 기록)를 꾸미라고 열어 둔 훅이 코드를 만들어 내는 통로가 될 수 있음을 보여 줍니다.
- 실행할 코드를 프로토타입 오염(모든 객체가 물려받는 원형에 속성을 심는 기법)으로 넣을 수 있다는 점도 드러났습니다.
핵심 내용
- V8의 Error.prepareStackTrace는 오류의 stack 속성을 읽을 때 불리고, 오류 객체와 호출 위치 배열을 인자로 받습니다.
- 작성자는 이 모양이 Function 생성자와 같다는 데 착안해 훅 자리에 Function을 넣었습니다.
- 호출 위치 배열은 코드가 아니라서 문법 오류가 나므로, 배열의 toString을 Error.prototype.toString으로 바꿔 원하는 문자열이 나오게 했습니다.
- 실행할 코드는 Object.prototype.message에 넣고, 괄호는 \x28·\x29 이스케이프로 적어 소스에 괄호가 나타나지 않게 했습니다.
- 마지막으로 만들어진 함수를 valueOf(객체를 원시값으로 바꿀 때 불리는 메서드)에 넣고 단항 +로 호출합니다.
HN 반응
- 여러 이용자가 난독화 C 코드 대회(IOCCC)나 "유닉스와 C는 장난이었다"는 농담 글을 떠올리며 이런 기교 자체를 즐겼습니다.
- LLM 이전처럼 코드에 관심을 두던 시절이 생각난다는 감상에, 이제 코딩은 주말 취미이고 월요일엔 에이전트를 돌본다는 자조가 이어졌습니다.
"유닉스와 C는 사기극이었다고 고백한 창시자들"(예: https://www.gnu.org/fun/jokes/unix-hoax.html )이 떠오르네요.
펄이 만들어진 과정은 이렇죠.
‘래리, 고양이를 키보드에 던져봐!’
야아아옹!
‘뭐라고 쳤어?’
‘&}Ak'>v=4%Qe}9;’
‘좋아, 이걸 hello world 문법으로 하자.’
유튜브 초창기에 누군가 비스타 음성 인식으로 펄 코드를 짜려던 영상이 있었는데 진짜 웃겼습니다.
본 지 수십 년은 됐지만, 그때는 볼 때마다 웃음 버튼이 눌렸습니다.
https://m.youtube.com/watch?v=MzJ0CytAsec
이건 이 예술 작품의 직계 후손 같네요.
https://www.youtube.com/watch?v=RyBEUyEtxQo
우와, 진짜 지옥을 찍은 영상이네요!
OCR로도 되더라고요. https://www.mcmillen.dev/sigbovik/
이 놀라운 논문도 항상 빠지지 않죠.
https://www.mcmillen.dev/sigbovik/2019.pdf
뭐, 이 장난은 벌써 수년째 다듬어지고 있습니다. 아래 사이트에서 즐겁게 놀고 있는 훌륭한 분들이 있죠.
https://www.ioccc.org
사실 그 줄은 IOCCC 수상작에서 나온 겁니다. https://github.com/ioccc-src/winner/blob/master/1985/shapiro/shapiro.c
90년대 후반에 토론토의 한 스타트업에 테크니컬 프로덕트 매니저로 면접을 본 적이 있습니다. CTO 겸 공동 창업자가 난독화 C 대회 수상자였습니다. https://www.ioccc.org/1985/lycklama/index.html
같이 일하기 좋은 분이었습니다. 지금은 수제 맥주 양조장을 운영하는 걸로 압니다.
유닉스는 죽었다! 한판 붙을래? (존 C. 드보락):
https://donhopkins.com/home/catalog/unix-haters/etc/unix-is-dead.html
정말 좋네요. 재미있는 해킹이기도 하지만, 사람들이 아직 코드 자체에 관심이 있던 LLM 이전 시절이 떠오르게 하는 글이기도 합니다.
90년대에 체스계가 실제로 체스를 두는 것보다 IBM 주가 얘기를 더 많이 하던 시기가 있었을까요?
"체스 솔루션을 납품하는 것"은 애초에 그 일이 아니었잖아요.
저는 만우절에 이걸 꼭 쓸 겁니다. Claude.md에 이 지시만 추가하면 되거든요.
그러면 동료들 중에 자기가 푸시하는 코드를 실제로 읽는 사람이 몇 명인지 알아보는 리트머스 시험지 노릇도 해줄 거고요.
프로그래머들은 여전히 코드에 관심 있어요.
주말 취미로요!
이제 다시 월요일이네요. 코딩은 끝, 에이전트 돌보미로 복귀입니다.
데이터를 여기서 저기로 옮기는 방법을 생각나는 대로 전부 보여주는, 아주 깐깐한 책을 쓰고 싶습니다. 같은 프로그램 안에서든 프로그램 사이에서든요.
전역 변수.
함수의 매개변수.
펑터/람다를 넘기고 값을 받아오기.
환경 변수.
레지스트리.
파일.
ping 응답에 담기.
썼다가 다시 읽는 VRAM 텍스처.
데이터베이스.
REST 서버.
천 가지 방법쯤은 충분히 떠올릴 수 있을 것 같습니다.
그런데... 왜일까요? 내가 왜 이런 책을 쓰고 싶은 걸까요? 왜 이게 계속 머릿속에 맴도는 걸까요?
Harder Drives가 떠오르네요. https://tom7.org/papers/murphy2022harder.pdf
제 친구가 몇 년 전에 만든 것도 있습니다. https://github.com/radian628/tram
데이터 저장과 전송 분야에는 혁신이 정말 많이 일어나고 있네요!
Khedker 외 저자의 "Data Flow Analysis"라는 책이 관심에 맞을 수도 있습니다.
컴파일러 중심이긴 하지만, 퍼즐의 한 조각은 됩니다.
왜 그런지 이 글을 보니 CLOS에서 제네릭 함수를 호출하고 실제로는 어떤 메서드(들)가 실행되는 과정에 얽힌 온갖 복잡함이 떠오르네요...
저는 호출할 함수를 인자 뒤에 두고 싶습니다.
그러면 일이 일어나는 순서를 왼쪽에서 오른쪽으로 그대로 볼 수 있으니까요. 반면에 이렇게 쓰면
거꾸로 읽어야 하잖아요.
ocaml이나 f# 같은 ML 계열 언어로 프로그래밍할 때 제가 좋아하는 것 중 하나가 순방향 파이프 연산자입니다.
연산자 왼쪽에 있는 것을 마지막 인자로 넣어 주니까 편합니다. 그래서 인자가 둘 이상인 함수에도 쓸 수 있습니다.
그러니까 이런 코드가 있다면
이렇게 쓸 수 있습니다.
데이터 가공 코드에 특히 잘 어울립니다.
게다가 객체지향 코드에서 메서드 호출로 하는 방식과 달리, 체인 중간에 새 함수를 끼워 넣을 때 클래스를 고치거나 확장 메서드, 트레이트 같은 더 복잡한 언어 기능을 만지작거릴 필요가 없습니다.
Lisp 매크로로도 비슷한 걸 할 수 있고, 언어 자체를 바꿀 필요도 없습니다.
ML 계열 언어에서도 이걸 하려고 언어를 바꿀 필요는 없습니다. 함수 적용이 원래 "인자 하나씩" 이루어지기 때문입니다(이를 "커리화"라고 합니다). 보통은 f x y z로 쓰지만 (f x y) z나 (((f)) (x)) y z도 똑같이 유효하고, f x y는 인자를 하나 받는 함수가 됩니다. 그래서 |>는 인자 하나짜리 함수를 받기만 하면 끝납니다.
요즘 Racket을 너무 많이 써서 제 시각이 영향을 받은 걸 수도 있지만, Lisp 매크로를 언어 자체를 바꾸는 수단으로 생각하지 않기가 정말 어렵습니다. 특히 리더 매크로 같은 거라면요.
Common Lisp은 상당 부분이 그렇습니다. 언어의 표준 부분을 구현하는 데 쓰던 것들을 두고 "이건 사용자들도 쓰고 싶어 하겠는걸" 하고 표준의 일부로 공개한 거죠. 제가 Common Lisp을 확장한다면 이런 것들을 더 표준화해서 공개하겠습니다.
그 말은 거의 모든 언어 기능에 똑같이 할 수 있습니다. 하지만 사람들이 합의한 표준 기능이 있는 편이, 어떤 매크로가 충분히 미적으로 마음에 드는지를 두고 사람마다 제각각인 의견을 따르는 것보다 낫습니다.