LakeCTF 2022 - porcosort (Dockerfile, libc, oob)
분석 환경: Windows 11, Ubuntu 22.04
사용자 제공 파일: 바이너리, 도커파일
tmi
libc 때문에.. 고생을 좀 했지만 결론적으로는 잘 해결했다.
맨 처음에 Ubuntu 20.04에서 분석 및 익스 진행하려고 했는데 실행이 잘 되지 않아 그냥 Ubuntu 22.04에서 진행했다. 버전을 올린 이유는 20.04에서 실행할 때 GLibc? 그 버전이 낮아서 실행이 안된다고 에러 문구가 떴기 때문에 당연하게도(?) 22.04로 버전을 올릴 생각을 하였기 때문이다. 하지만 결론적으로 Ubuntu 22.04의 libc도 아니었기 때문에(arch의 libc였음) 굳이 22.04로 버전을 안올리고 문제를 풀 수도 있었다.
ctf 챌린지에서 도커파일을 제공하는 이유는 libc 버전 때문이다. libc 파일을 던져준 것은 아니지만 libc를 거져준 것이나 다름없다. 알고있었지만 활용하는 방법을 제대로 알지 못했다. libc 때문에 삽질을 많이 해서 결국 롸업을 찾아보고 해당 부분을 바로 해결할 수 있었다.
https://blog.csotiriou.com/post/lakectf-2022-qualifier-porcosort-pwn/#1-setup
이번 기회로 도커파일을 눈여겨봐야겠다는 생각과 libc를 가져오고 patch하는 방법을 배웠다. 이제라도 알게 되어서 다행이다.
Analysis
로컬에서 문제 환경 세팅하기 (feat. 도커파일이 주어졌을 때 libc 가져오기)
Ubuntu 22.04에서 ldd
명령을 통해 바이너리에 매핑된 라이브러리를 알 수 있다. Ubuntu 20.04에서 진행했을 때는 버전에 맞는 libc가 없어서 실행조차 되지 않았는데, 여기선 그래도 실행은 된다. 하지만 아무리 실행이 되더라도 remote와 환경이 다를 수도 있기 때문에 제공된 도커파일을 꼭 확인해야 한다.
1 | jir4vvit@22:~/ctf/lake/porcosort2$ ldd porcosort |
도커파일을 확인해보자.
1 | FROM docker.io/library/archlinux@sha256:2bfe247c46221b0770325d69ec195b50455b2865588665e6926b2d1168982e67 AS builder |
문제 환경이 Ubuntu가 아니라 arch linux임을 알 수 있다. 저기로 접속해서 libc를 가져와 바이너리를 patch하는 작업을 진행하자.
1 | docker run -it --rm -v `pwd`:/data archlinux@sha256:2bfe247c46221b0770325d69ec195b50455b2865588665e6926b2d1168982e67 bash |
도커를 실행하면 컨테이너에 접속할 수 있다.
다른 터미널 창을 열어서 컨테이너로 문제 바이너리를 넣어주고 ldd
명령어로 매핑된 라이브러리를 확인해보자.
1 | docker cp /home/jir4vvit/ctf/lake/porcosort2/porcorsort ce9c17249f0e:/home/ |
1 | [root@ce9c17249f0e home]# ldd porcosort |
다른 터미널에서 아래 두 명령어로 컨테이너 안의 ld와 libc 파일을 가져오자.
1 | docker cp ce9c17249f0e:/usr/lib/libc.so.6 /home/jir4vvit/ctf/lake/porcosort2/ |
그럼 이제 로컬에 ld와 libc가 저장이 되고, patchelf
를 이용해서 바이너리를 패치할 수 있다.
1 | jir4vvit@22:~/ctf/lake/porcosort2$ patchelf --set-rpath . --replace-needed libc.so.6 ./libc.so.6 porcosort |
--set-interpreter <ld>
: 다운받은 ld(로더)를 지정--replace-needed <libc>
: libc 교체
그럼 이제 문제 환경(remote)와 local이 똑같아졌다! 가젯 찾을 일 있으면 가져온 libc나 ld에서 가젯찾으면 된다.
보호 기법
1 | Arch: amd64-64-little |
전부 다 걸려있다. :(
취약점
우리의 친구 IDA와 함께 차근차근 분석하면서 취약점을 찾아보자.
vuln 함수
1 | unsigned __int64 __fastcall vuln(unsigned __int64 a1) |
vuln
함수에서 _IO_2_1_stdout_
을 출력해 준다. (gdb로 바로 찍어봤다.)
그리고 어떠한 수를 입력받고 그 입력값을 인자로 read_numbers
함수와 get_gnomed
함수를 호출한다.
read_numbers 함수
1 | __int64 __fastcall read_numbers(_QWORD *buf, __int64 cnt) |
두 번째 인자 cnt
는 나의 입력 값이다. 결론적으로 스택에 내가 원하는 데이터를 넣을 수 있다.
이때 oob write가 가능하다. 인자로 넘어온 buf
는 크기가 48이다. 이때 cnt
가 48이라면 &buf[48]
에 접근하여 쓰기가 가능하다.
1 | &buf[48] ~ &buf[1] |
oob write가 발생하지 않으려면(원래대로라면) 아래처럼 0부터 cnt-1
까지만 접근해야겠지.
1 | &buf[48-1] ~ &buf[0] |
그럼 이제 우리가 oob를 통해 접근할 수 있는 &buf[48]
에 저장된 값이 뭔지 궁금하다.
1 | __int64 stack_buf[48]; // [rsp+20h] [rbp-1A0h] BYREF |
buf
바로 밑에 cnt
가 존재한다. 그렇다. &buf[48]
에는 우리의 입력값인 cnt
가 저장되어 있다. 이 말 즉슨.. read_numbers
함수가 끝나고 다음 호출될 get_gnomed
함수의 인자를 조작 가능하다는 의미이다.
get_gnomed 함수
1 | __int64 __fastcall get_gnomed(_QWORD *buf, __int64 cnt) |
그냥 크기 순으로 정렬하는 코드이다. 대소비교에 마우스를 올려보면 *curr >= *(curr-1)
에서 signed로 비교를 하고 있는 것을 확인할 수 있다. 참고로 이 사실은 익스하는 데에 있어서 아주 중요하다..
스택에 원하는 값들을 적으면 결국 여기에서 다시 정렬하게 되는데, 크기 순으로 정렬이 되니까 값을 어떻게 넣을 지 생각을 잘해야 한다.
음 취약점을 설명하는 해당 부분에서는 살짝 맞지 않는 것 같지만 이어서 설명하는 것이 이해하기에 쉬운 것 같아 바로 스택을 보여주겠다.
1 | 0x7ffe3265c040: 0x00007f29d7a406c0 0x8000000000006873 |
이 값들을 get_gnomed
함수를 거치면 아래와 같다.
1 | 0x7ffe3265c040: 0x8000000000006873 0x8000000000006873 |
현재 카나리는 0x7ffe3265c1c8
주소에 위치한다.
익스를 작성할 때 가장 까다로운 점은 카나리의 값이 랜덤이란 점이다. 이 위치가 바뀌면 당연하게도 *** stack smashing detected ***: terminated
이 발생하는데, 카나리의 값은 랜덤이기 때문에 적당히 작은 수(음수?)이길 기대해야 한다. 이 때문에 익스 코드는 확률이 조금 있다.
그리고 0x8000000000006873
가 보이는데 이는 -9223372036854749069
이다. 사실은 sh
을 의도한 문자열이다. 왜 이렇게 줬을까?
1 | ► 0x56310fe0d428 <vuln+294> call get_gnomed <get_gnomed> |
get_gnomed
함수를 호출할 떄 rdi의 값은 0x7ffe3265c040
이다. 이 함수에서 rdi
를 쓰지 않기 때문에 함수가 종료될 때도 rdi가 동일하다.
그렇단 뜻은 system
함수를 실행할 때 내가 원하는 매우 작은 값으로 rdi
로 지정할 수 있단 뜻
Exploit
시나리오
- OOB write 트리거 위해
cnt
입력 시 48 입력 _IO_2_1_stdout_
출력해주는 것을 이용하여 libc base 구하기- 정렬할 값들을 적을 때(스택에 값을 적을 때)
cnt
를 더 큰 수로 하기 위해 맨 처음 57 입력 <- OOB write 트리거 됨 - 스택에 페이로드 입력: ret 가젯 필요 (
movaps xmmword ptr [rsp + 0x50], xmm0
에서 멈추기 때문) - 카나리의 위치가 제 위치에 온전히 있을 수 있도록 기도
여담이지만 ret
주소가 system
주소보다 작게 구해졌기 때문에 이거에 대한 크기 고민은 하지 않았다.
가젯 찾기
ret
1 | jir4vvit@22:~/ctf/lake/porcosort2$ ROPgadget --binary libc.so.6 | grep " : ret" |
Exploit Code
1 | from pwn import * |
Flag
1 | [*] leak :: 0x7fb8ce4466c0 |