XCTF Finals 2016
어찌어찌해서 XCTF 2016 본선에 초청을 받아서 벨루미나 때 가본 베이징을 다시 한번 가보게 되었어요. 가서 1등을 했습니다. 신기했고, 기쁘고, 팀원들에게 감사해요.
XCTF 소개
이 대회를 간략하게 소개하자면, XCTF는 중국 내에서 현재 진행하고 있는 리그 형식의 CTF 모음이에요. 각 대학교에서 CTF들을 진행하고 이를 CTFTime처럼 XCTFTime이라는 사이트가 있어서 점수를 합산해서 XCTF Finals라는 대회에 상위 몇 개 팀이 가서 붙는 대회랍니다. 저희는 외부에서 초청이 되었구요.
XCTF의 점수 제도는 DEF CON 23과 같아요.
- 각 팀마다 기본 점수가 부여되고
- 5분마다 각 팀, 각 문제의 플래그가 바뀌고,
- 그 플래그를 인증하면 점수가 올라간답니다.
- 각 팀마다 가지고 있는 자기 팀의 플래그는 참가 팀 수 - 1만큼의 점수를 갖고,
- 이 플래그를 여러 팀이 인증하면 1/n 해서 점수를 가집니다.
예를 들어 어떤 팀이 다른 팀들은 못 푼 문제를 모든 팀에 공격해서 점수를 얻으면 (팀 수 - 1) * (팀 수 - 1)
만큼의 점수를 그 라운드에 얻을 수 있어요. 단 모든 팀이 모든 팀의 플래그를 얻어서 전송하면 각각 (팀 수 - 1)
개의 점수만큼 얻을 수 있겠죠. 16팀이 대회를 하고 3팀이 어떤 다른 팀의 어떤 문제를 공격하면 그 3팀은 15 * 15 / 3 = 75
점을 얻습니다.
문제 환경에 대해서는 전부 우분투 14.04 버전이였고 x86-64 환경이였어요. 커널은 4.2정도 되더라구요. root 계정은 주어지지 않았고 자기 VM의 root 계정을 획득하는 것은 규칙 위반이라고 하네요.
여정
베이징에는 대회 바로 전날에 갔어요. 한 1시쯤 호텔 체크인을 한 뒤 간단한 요기를 하구요. 주최측 중 한 분이 어디서 식사를 할 수 있는지 알려주셨는데, 정말 감사했어요.
그리고 대회에 뭐가 필요하겠다 싶은 것들을 그 날에 준비를 했습니다. 저같은 경우에는 x86/x64 리눅스용으로 /etc/passwd를 읽는 쉘코드를 최대한 줄이려 했던 기억이 나네요. (대회중에는 쉘코드는 안썼습니다)
일찍 잤어요.
D+0
경고 여기서부터는 문제에 대한 스포일러가 있어요
대회 첫째 날, 아침을 먹고 대회장으로 갔습니다. 아침은 무난한 뷔페를 줬어요. 보통 대회장에는 신나는 음악을 틀어준답니다. 각 팀끼리 대화하는 소리를 가리기 위해서 틀거나, 음악 듣고 힘내라고 틀어주는 것이 아닐까 싶어요.
대회 직전에는 항상 작게던 크게던 바쁘답니다. 네트워크를 세팅하고, 분석 도구들을 켜요. 문제 목록들을 읽어보고(아직 안나왔었지만) 네트워크 구성도를 보면서 어떤 문제가 나올지 설렜습니다.
또, 문제를 보면 풀어야겠다는 다짐을 했죠.
대회는 10시 30분에 시작했습니다. 일단 web1이라는 웹문제와 richman이라는 포너블 문제가 1개씩 나왔는데요. web1은 CodeIgniter 기반 CMS 문제였고, richman은 일반적인 리모트 리눅스 포너블 문제였어요.
아무래도 저는 포너블쪽이 맞겠다 싶어서 먼저 포너블을 했어요. 풀고 있는데 처음 나온 웹 문제가 다른 팀에 의해 풀려서 16등까지 내려가더라구요. 초조하긴 했지만, 신경써봤자 좋은 건 없으니까요.
바이너리를 계속 분석하다가 12시쯤에 취약점을 발견해서 공격 방법을 구상한 뒤 로컬에서 익스플로잇을 짜봤어요. 한 2~3시간 걸렸나.. 그러다 1시 11분에 로컬에서 익스플로잇을 처음으로 성공했답니다.
어, 그러고보니 아직 아무도 richman을 안 풀었더라구요.. 안도하면서 익스플로잇을 수동으로 돌리고 토큰을 인증했습니다. 저희 팀만 푼 덕분에 일단 16위에서 1위까지 올라갔어요. 오예!
1-day 취약점이 수두룩했던 Web1
문제를 풀고 web1이 어떤 상황인가 궁금해서 봤더니, CodeIgniter 버전이 엄청 구버전인것처럼 보였고 취약점이 수두룩하더라구요! 1-day 취약점들을 보니까 대놓고 unserialize가 가능하더라구요. 심지어 파일을 써주는 클래스까지 있어서 백도어도 막 심기고.. (crontab 백도어까지 있었어요)
그러니까 패치하기가 상당히 어려워보였어요. 취약점도 많아서 하나 뚫리는거 막으면 하나 또 막아야되고 그랬습니다. 거기다가 기능을 또 잘못 수정하면 자동화된 SLA 체커(서비스가 제대로 동작하는지 체크하는 프로그램)에서 서비스가 의도한대로 동작하지 않는 것으로 간주하여 그 라운드의 플래그를 뺏어서 전 팀한테 1점씩 나눠준답니다.
SLA는 항상 이해가 안되요. 대체 그 사람들의 의도는 무엇일까요? SLA 체커는 대체 어떤 기능을 어떻게 체크할까요? 애매하답니다. 어떤 걸 어떻게 패치하고 SLA가 다운되면 다른 방법으로 처리를 해보고, 될 때까지 반복해보는거에요.
하지만 SLA도 털리고 플래그도 털리고 있었기에, 한 번만 털리는 것이 좋겠죠. SLA를 아예 다운시키고 서비스를 접근 불가능하게 하거나, 서비스를 초기 버전으로 돌려서 Web1을 푼 모든 팀한테 점수를 공평하게 주는거죠. 상황에 따라 제때제때 전략을 바꿨습니다. 위에서 언급했듯이 백도어가 심겼기에 서비스가 접근이 불가능함에도 계속 플래그를 잃고 있었고, 따라서 서비스를 초기 상태로 되돌리는 전략을 취했습니다. 그래도 거의 다 패치했던 것 같아요.
또 패치하는 부분에 대한 익스플로잇을 작성합니다. 잘 작성했어요. 패치를 아예 안하는 팀도 있었기에 유효했습니다. 이번 기회에 CodeIgniter를 쓰면 뭔가 보안쪽도 잘 되어있을거같고 그랬던 인식을 고쳐먹게 되었어요.
xhttpd의 등장
그러다가 새로운 포너블 문제가 나왔습니다. 웹 서버 컨셉의 문제였고, 서버에 접속하면 새로 포트를 열어서 해당 서버를 접근 가능하게 한 문제였어요. 저 문제가 나왔을 당시 저는 richman을 풀고 있었죠. 익스플로잇이 끝난 다음 보니까 바이너리가 복잡하길래 일단 web1을 봤어요.. 노답이길래 다시 xhttpd를 봤고 뭔가 익스플로잇이 가능할 것 같은 버그를 봤지만 확실하지 않아서 일단 생각만 하고 있었어요. 이런건 생각 날 때 바로 짜야 한답니다!
drawdraw.. 음..
Drawdraw라는 이름의, protobuf 데이터를 입력으로 받는 문제가 나왔어요.
근데 전 protobuf를 다뤄본적이 없었답니다. 입력 형식이 어떤지도 몰랐구요..
게다가 구글도 안된답니다.. 전 구글밖에 안썼는데.. 그래서 바이두를 썼어요.
열심히 검색을 해봤더니 protobuf는 타입이 있는 JSON 비스무리한거였고, proto라는 구조체 정의 파일을 각 언어로 컴파일해주는 역할을 하는 protoc 컴파일러가 있으면 각 언어에 자연스럽게 해당 파일을 첨부해서 같이 컴파일을 할 수 있더라구요.
네, 그러니까 그게 C++ 언어로 된 컴파일된 바이너리였는데, 그 proto 파일은 없었다는거죠. 야 신난다 약간 멘붕하면서 분석했었어요. 파싱하는 부분을 전부 분석해야 하나 하면서요.. 일단 그래서 그 후를 분석했습니다. 뭔가 입력인듯 한 것을 루프를 돌면서 파싱하더라구요. 근데 딱 보니까 공략하기 쉬운 취약점이 있어보이는거에요. 설레였지만 일단 입력을 넣어봐야 아니까요. 그래서 입력을 아무렇게나 구성해서 넣어봤는데 안됬고, 결국 입력 형식을 맞춰줘야되는구나.. 하는 결론에 도달했습니다. 그래도 대충 바이너리가 어떤 컨셉인지까지는 이해를 했습니다. 심플한 그림판을 구현했더라구요.
Web2가 나왔구나.. 어? 풀렸잖아!
대회 첫째 날 일정이 종료되기 한 30분 전이였나.. 그 때 Web2라는 문제가 나왔어요. http 주소를 받아서 요청한 뒤 그 내용을 base64로 얻어서 보여주는 문제였습니다. 저는 소스를 볼 생각도 안하고 그냥 이것저것 시도해봤었답니다. 그러다가 갑자기 forx라는 팀이 푼거에요. 로그를 남겨서 보니까 단순 커맨드 인젝션이였어요.
그러다가 소스가 있다는 사실을 알게 되었어요. 공방전이라서 당연히 소스가 제공되는데.. 그걸 못 보고.. 난독화된 소스와 아쉬운 마음을 안고 첫째 날 일정 종료 후 호텔로 갔습니다.
p.s. forx팀이 소스를 본 건지 안 본건지 궁금하네요. 게다가 난독화되어있다했는데!
D+0, 호텔에서
대회는 1박 2일로 진행되었는데, 첫 날 오후 6시 30분쯤에는 대회 서버를 모두 중단시키고 각자의 숙소에서 문제를 풀 수 있도록 해요. 당연히 분석에 필요한 문제 파일들을 미리 백업해서 가야했어요. 저희 팀은 가면서 몇 문제에 대해 풀릴 거 같은데.. 풀릴 거 같은데.. 하면서 갔습니다.
그리고 풀었죠! 저희 팀은 첫 날에 나온 문제는 전부 익스플로잇을 1개씩 짜서 갔답니다. 팀원 분이랑 같이 푼 xhttpd가 개인적으로 인상깊었던거같아요.
Web1 문제는.. 패치할 게 많아서 git으로 관리했습니다. 그리고 팀원분들이 익스플로잇도 짜셨어요. 다음 날에 잘 먹혔습니다.
D+1
그 전날 밤과 새벽에 걸쳐 작성한 익스플로잇을 이제 뿌려서 플래그를 얻을 시간이죠! 그런데 xhttpd 익스플로잇이 서버와의 미묘한 차이로 작동을 안하고 제 컴퓨터는 네트워크가 먹통이 되어서 drawdraw 문제를 0ops 팀이 먼저 풀었어요.
컴퓨터를 재부팅하고 drawdraw 익스플로잇을 수정한 뒤 돌리니 잘 되더라구요. 그리고 xhttpd의 익스플로잇을 팀원 한 분이 손봤습니다. 아마 xhttpd는 저희 팀 밖에 못풀었을거에요! 수고했어요 팀원님!
그리고나서 web2를 보는데.. 뭔가 이상한거에요.
일단 첫 날 본 바로는 PHP 소스코드에서 http 요청을 위해 원래 없는 함수를 쓰고 있었기 때문에(curl), 이 함수를 어딘가에서는 분명히 정의를 했을거라고 생각을 했어요. 그래서 서버를 잘 보니까 firedoor.so라는 root 소유의 확장 모듈이 있었는데 거기서 제공을 하더라구요. 게다가 그 확장 모듈에는.. 방화벽을 가장한 난독화된 백도어도 있었답니다! 아마 LLVM Obfuscator를 썼던것같긴 한데, 그리 어려운 건 아니고 DWARF 심볼도 있는데다가 간단한 수준이라서 그냥 그대로 분석해도 문제는 없었어요. 단 ELF의 심볼을 꼬아놓은건지 함수 이름이 가짜인 게 있었는데, 디버깅해서 봤습니다. PHP eval 함수(zend_eval_string
)를 숨겨놨더라구요.
그리고 방화벽처럼 그럴듯하게 구조체 이름을 붙인 구조체는 사실 RC4 복호화에 쓰였고 이는 eval 전에 사용자의 IP를 key로 해서 처리가 되었답니다. POST로 코드를 암호화해서 전송하면 서버에서 복호화해서 풀어주는 루틴이였어요. 익스플로잇을 바로 짰고, 잘 동작했어요. 짜릿했죠!
이걸로 모든 팀의 플래그를 대회가 끝날때까지 따왔답니다. 확장 모듈은 루트 권한으로 되어있어서 확장 프로그램을 제거한다던지 하는 행동은 안 되었기 때문에 PHP쪽만으로는 막기가 쉽진 않았어요. 사실 공격을 안 받았으니 고칠 필요도 없었지만요.
auction!
그리고 마지막 문제인 auction이라는 포너블 문제가 나왔습니다. .db 파일과 프로그램을 줬었는데, SQLite3 기반으로 유저 입력을 INSERT 쿼리에 넣어서 여러 스레드에 걸쳐 수행하는 프로그램이였습니다. 팀원분이 잘 보더니 레이스 컨디션으로 SQL 인젝션 및 BOF를 내더군요. 하지만 해당 BOF는 PIE환경에서 릴라이어블하게 공격하기가 쉽지 않았어요. 하다가 이거 어떻게 공격하지 하고 약간 회의감이 느껴지긴 하더라구요.
여러 문제의 익스플로잇을 돌리다보니 순위가 1등으로 올라가있었고, 10000점을 거쳐서 22000점까지 갔습니다. 오예!
후기
어찌저찌 해서 1등으로 대회가 끝났어요. 기분 좋았죠. 1등할 줄 예상 못했거든요. 팀원분들께 고마움을 전하고 싶습니다.
Richman 문제의 제 익스플로잇이 난독화가 되어있지 않아 분석하기에는 간단해서 분석이 용이했다고 하시던 분이 있더라구요.. 익스플로잇에는 약간의 난독화를 가미해보는 것도 괜찮은 것 같습니다. 그래서 xhttpd의 익스플로잇에는 약간 난독화랑 햇갈릴만한 것들을 많이 넣어놨었어요. 가짜 ROP 가젯이라던지..
Auction 문제 관련해서는 끝나고 출제자분께 들어보니까, 저희가 다른 포트로 열린 서비스를 하나 안 봤더라구요. SQL 인젝션으로 웹서비스에 웹쉘을 올리고 공격하는 문제였다는군요.. SQLite3 + C 조합을 쓰는 프로그램의 SQL 쿼리에서는 콜론(;) 기호로 여러 쿼리를 실행할 수 있고 ATTACH DATABASE로 새로운 파일을 열 수 있습니다. 이걸로 웹쉘을 쓰는 것을 의도한거죠. 참 아쉬웠어요. 서비스를 아예 못 보다니!
이번 XCTF의 문제들은 그래도 익스플로잇에 스트레스를 받는 문제는 없어서 재미있었던 것 같습니다. 그렇다고 아주 식상하지도 않았구요. 1등을 가능하게 해준 다른 팀원 3분 모두에게 감사드리고 싶습니다.
그리고 긴 글 읽어주셔서 감사합니다! 의견은 댓글로 남겨주세요! :)