Security - Player/CTF

osu!gaming CTF 2024 Reversing Write-Up

PS리버싱마크해커박종휘TV 2024. 3. 4. 08:57

Figure I. 66th place

66등 하였다.

 

서론

 

원래 osu! 라는 게임을 즐겨하는 사람으로서 이 CTF는 굉장히 흥미로웠다. CTF 문제에 osu라는 새로운 항목이 추가되면서 CTF의 Fresh함을 챙겼다고 볼 수 있다. osu!와 CTF 모두 좋아하는 사람으로서 이 콤비네이션은 열심히 안 할 수 없다. 그 결과로 리버싱을 올킬했다. 이 글에서는 리버싱 문제에 관련해 Write-Up을 작성할 예정이다

 

SAT-before-osu

 

언제부터 이딴 게 리버싱이였는지 모르겠다. 주어진 dist.txt 안의 식을 풀면 된다. z3를 이용하거나 Sage를 이용하여 간단히 해결할 수 있다.

 

wysi-baby

 

Wysi 밈을 토대로 만든 웹사이트가 주어진다. 소스를 살펴보면 다음과 같은 Javascript 코드가 보인다.

 

Figure II. JS Code

 

combos에 들어가는 원소를 살펴보니 0~9 사이인 것을 확인할 수 있고 브루트 포스를 돌린다면 10^8, 그렇게 크지 않은 수치이다.

 

따라서 브루트 포스를 하면 풀린다.

 

ecs!catch

 

게임이 주어진다. Unity로 만든 게임이고 Mono를 사용하므로 Cheat Engine을 켜서 Enable Mono Features -> .NET Info를 본다면 NoteObject라는 클래스를 발견할 수 있는데 OnTriggerEnter2D에서 플레이어와 충돌했는지 검사하는 부분을 무조건 통과하게 만들면 된다.

 

그리고 Remote 서버에 간단한 검사가 있다고 하는데 이는 점수, Fruit/Drop/Droplet의 개수, 미스 개수, 콤보, 풀콤보 여부만을 검사하기 때문에 이 방식이 작동한다.

 

wysi-revenge

 

wysi-baby를 더 어렵게 만든 문제이다. 실제로 코드를 다시 확인하면 다음과 같은 코드가 나온다.

 

Figure III. ok

 

이는 Emscripten으로 만든 C 함수를 Javascript에서 호출하는 것으로, 내부적으로 checker.js는 checker.wasm 안의 checker 함수를 호출한다.

 

wasm에서 C 의사 코드로 변경해주는 wasm2c라는 도구가 존재한다. 이 도구로 wasm 파일을 c 파일로 만든다음 checker 함수를 검색하면 어렵지 않게 export되는 checker 함수가 있는데 이걸 분석하면 된다. 코드가 길지만 하다보면 대충 로직을 알 수 있다.

 

배울 수 있는 점은 w2c_checker->val_g0이 rsp 역할을 한다는 점이다.

 

scorev3

 

Go 언어로 만든 바이너리인 것을 확인할 수 있다.

 

scorev2와 작동 방식이 비슷한데, 현재 시각에 따라 점수가 바뀌는 제도를 채택한다.

 

Figure IV. checker logic

 

다음과 같은 조건을 거짓으로 만들어야 한다. 매우 복잡해 보이는데 복잡한 것 맞다.

 

주목할 점은 v10이 도출되는 과정인데, 실제로 디버깅을 해보게 되면 v10이 항상 0에서 1 사이로 나오는 것을 알 수 있다.

그렇다면 분자가 0~727 사이라는 말인데 저 복잡한 수식에서 마법같이 0에서 727 사이의 값이 나오는 것이 신기해서 좀 더 돌려보니 1초마다 분자의 값이 1씩 늘어나다가 값이 727이 되면 다시 0으로 바뀌는 것을 알 수 있다.

 

그리고 Go의 time.Now()는 로컬 타임을 반환한다고 기존에 알고 있어 다른 timezone으로 각각 디버깅 해보았는데, 바뀌는 것은 없었다. 왠진 모르겠지만 모든 지역에서 동시에 실행했을 때 같은 time을 반환한다는 소리이다.

 

그리고 저 수식을 통과해야 하는데 최종 결과 값이 1099727 이상이여야 한다. 이때 입력값을 살펴본다면 다음과 같은 포맷으로 작성해야 한다.

 

[DT/HD/HR] [0.0 < A < 95.0] [0 < B < 1000]/1200

 

 

중요한 점은 A와 B가 커질수록 시간에 구애받는 것이 작아진다. A가 90이라면 전체 1에서 90%은 최대 점수로 들어가고 10%은 현재 시간에 따라 들어간다는 것이다.

 

그리고 DT/HD/HR는 점수의 배수를 결정하는데 그것이 1.099727 이상이여야 하므로 1.1배수를 주는 DT를 적을 수 밖에 없다.

 

따라서 우리가 할 것은 적절한 시간대에 다음과 같은 페이로드를 보내 플래그를 얻는 것이다.

 

DT 94.9999999 1000.0/1200.0

 

이 '적절한 시간대'를 구하기 위해 게싱을 좀 했는데, 시각을 727로 나누는 것이 가능하게 하는 것이 Unix Timestamp라고 생각해 이와 비슷한 Python의 time.time() 함수를 사용해서 시도해보았는데 되었다.

 

사실 이 외에도 1초마다 시도하는 것이 가능해서 브루트 포싱도 가능하다.

 

참고로 '적절한 시간대'는 놓치면 727초 기다려야 해서 총 3번 실패했는데 그래서 48분 걸렸다.

 

 

'Security - Player > CTF' 카테고리의 다른 글

CCE Jr 1st  (1) 2024.09.14
Dreamhack Invitationals 후기  (1) 2024.05.28
LACTF 2024 - Write-Up  (0) 2024.02.19
Dreamhack CTF Season 5 Round #1 Write-Up  (0) 2024.01.06