4회차 목표 : 정보보안 공부
4회차 결과 : Code Reuse Attack
Code Reuse Attack
쉘코드를 직접 삽입해서 공격하기 어려운 경우에 이걸 사용할 수 있다!
페이지2 malicious code는 악성 코드로 악성 행위를 실시한다. 다만, normal code라고 해
서 이게 항상 malicious하지 않게 수행하는 건 아니다.
페이지 3 정상적인 normal code를 악용해서 비정상적인 악성행위를 할 수 있고, 그걸 가
능하게 하는 방법이 code reuse attack이 있다.
페이지 4 해당 방식의 정의. 말리셔스 코드 없이 정상 코드를 이용해서 공격을 실시한다.
정상적인 코드를 묶어서 하는 공격이기 때문에, 프로그램 크기가 클수록 공격자가 활용
할 수 있는 코드 청크들이 많다. 공격을 성공률이 높아진다.
페이지 5 코드 리유즈 어택 이전에 그 방식의 배경을 알아보자. 스택이 있으면 리턴 어
드레스, 버퍼가 있다. 버퍼가 scanf같은 API를 잘못 사용해서 취약점이 있고, 그 취약점에
의해서 공격 대상이 되는 것이다. 버퍼로 오버라이트하고, 리턴 어드레스를 공격자가 덮
어 쓸 수 있다. 쉘코드를 넣어서 리턴 어드레스가 뛰도록하면 공격자가 공격을 실행할
수 있다. 스택 영역에는 실행권한이 있기 때문에 공격이 가능했다. 이런 것들의 방어기법
으로 데이터 영역을 실행할 수 없게 no execute로 페이지 테이블의 플래그를 세팅하든지데이터 영역을 실행하지 못 하는 방어기법이 등장했다. 스택에 쉘코드를 넣어도 그 영역
으로도 실행이 안 된다. 이런 데이터 익시큐션 프리벤션으로 새로운 공격기법을 찾게 되
었다.
페이지 6 DEP는 writeable page를 non-executable하게 만드는 것이다. 프로그램 스택이
익시큐터블이 아니다. 이런 간단한 공격기법이 안 된다. PaX라는 방어로 Linux는 DEP를
한다. 하드웨어 서폿으로 하기도 한다. ARM의 플래그로, 유저 영역의 DEP만 적용, WXN
은 모든 영역에서 적용. 그런데 모든 영역으로 하면, 자바스크립트에서 컴파일해서 실행
되는 부분같은 건 그러면 안 된다. 이건 드문 경우이다. Intel도 있고, 시스템 레지스터 플
래그들이 존재한다. 이름만 다를 뿐.
페이지 7 그림을 보면, 스택에서 말리셔스 부분을 쉘코드로 넣어서 실행했는데, 실행이
불가능하도록 하면, 실행 흐름을 변화시켜도 exception이 발생하게 된다.
페이지 8 DEP를 바이패스하려는 방법
페이지 9 DEP 환경에서 악성행위를 할 수 있을까? 공격자가 삽입한 코드보다, 이미 있는
걸로 수행하는 것이다. LIBC는 C 라이브러리이다. 여기서 대부분의 프로그램들이 사용하
는데, API로 점프해서 하는 게 Return to LIBC이다. 공격자가 제공하는 파라미터를 이용하
든지, 새로운 쉘을 실행하든지, 메모리 영역에 매핑하든지 그런 악성행위를 실행할 수 있
다.
페이지 10 공격시나리오를 보면, 정상적인 스택이 구현되어 있다. 이전 function에 대한
주소들 등등이 있다. 버퍼를 넘치게 해서 오버라이트를 한다. 공격자가 점프할 주소, 점
프할 주소에서 실행되는 함수의 파라미터를 쓴다. 시스템 함수로 뛰어가게 되는데 시스
템 인풋을 받아서 쉘을 띄우는 공격형태이다. 한 칸 4바이트 띄운 이유는 새 펑션이 실
행되면 항상 파라미터를 읽어오는 부분이 4바이트 띄어져 있는 형태이기 때문이다. 스택
포인터가 있고, 파라미터가 있다. 그렇기에 리턴 어드레스를 띄어야 한다. 시스템으로 점
프하면 파라미터는 4바이트 띄어진 메모리 부분을 읽게 된다. 그렇기에 세팅을 이와 같
이 했다.
페이지 11 그러면, 하나의 함수를 콜하는 공격을 통해서 함수를 콜하는 형태. 공격자가
실행하고 싶은 함수의 주소를 적어두고 4바이트 띄우고 파라미터를 넣어둔다. 함수를 실
행하며 파라미터들을 사용하게 된다.
페이지 12 두 개 이상의 함수를 실행해야하는 요구가 있을 때, 스택을 이와 같이 꾸미면
어떻게 되는가? 첫 펑션이 실행되며 파라미터를 사용하게 된다. 그 다음에 리턴을 두 번
째 함수가 호출하게 된다. 그런데 파라미터가 필요한 경우 4바이트 띄어진 부분을 읽어
가게 되는데 이미 첫 함수에서 사용할 파라미터로 세팅되어 있다. 이런 식이면 파라미터
세팅이 쉽지 않다. 이 부분이 어려우면 이전 수업의 메모리를 보면서 공부하기.페이지 13 두 번째 함수의 파라미터는 어떻게 하는가?
페이지 14 여기서 스택 한 번 다시 꾸며보자
페이지 15 공격자 입장에서는 이와같이 꾸밀 수 있다. 버퍼를 덮어 쓰고, 리턴어드레스
에 첫 번째 주소를 넣는다. 그 다음에 파라미터 두 개를 사용하고 첫 함수가 끝나면 어
딘가로 리턴하게 되는데 어디인지 참조하게 될 것이다.pop/pop/ret이라는 코드 조각으로
세팅 된 걸 보고, 실행흐름이 이렇게 된다. pop/pop 인스트럭션을 이용하게 된다. 이런
식으로 세팅한 이유는 팝을 하면 스택에서 4바이트 만큼 꺼낸다. 리턴이 실행된 시점에
서 팝이 실행되면 첫 번째 파라미터가 레지스터에 들어가고, 스택 포인터가 움직여지고,
또 팝 되면 움직여진다음에 리턴이 되면 해당 주소를 읽게 되고, 그 주소는 두 번 째 함
수의 주소이다. 앞서 읽은 것처럼 4바이트 띄우고 2번째 함수의 파라미터들을 읽게 된
다. 이렇게 해결이 가능하게 됐다. 해결에 기여한 게 "팝팝리턴" 코드 조각이다. 코드 청
크를 공격자가 메모리 어딘가에 찾아냈고, 이 코드 청크의 주소를 갖고와서 메모리에서
읽게 한 것이다.
페이지 16 리턴 어드레스가 덮여 쓰이고, 첫 함수로 점프하고, 파라미터들을 읽고, 끝난
다음에 1004의 주소를 와서 인스트럭션을 하게 된다. 거기는 팝팝리턴의 주소이고 그 메
모리 영역으로 점프해서 이동하게 된다. 스택 포인터를 옮기기 위한 역할을 수행한다. 첫
함수의 파라미터들을 지나쳐서 두 번째 함수를 실행하게 된다. 세컨드 함수로 점프하게
되어서 파라미터를 사용하게 되면 4바이트를 넘어서 그 다음에 파라미터들을 읽게 된다.
두 함수의 파라미터 세팅을 코드 조각 팝팝리턴으로 이용하게 됐다.
페이지 17 해당 코드들이 메모리에 존재하고 있다는 것이다.
페이지 18 리턴 투 립씨는 이미 있는 함수 덩어리를 사용한다면 지금부터 보는 회귀지
향 프로그래밍 방법에서는 이런 조그마한 코드 청크들을 이용해서 공격하는 기법을 말한
다. 가젯이라고도 하는 코드 청크들을 묶어서 공격하는 것이다.
페이지 19 펑션들을 지움으로써 공격자를 방어하는 것이다. 필요없는 함수, 사용될 API
만 올려두고 필요 없는 건 올려두지 않는다. 시스템함수들이 공격자들이 손쉽게 쓰는 것
들이니 그런 것들을 메모리에 두지 않는다. 이러면 리턴 투 립씨 공격이 안 통한다. 그러
니 조그마한 코드 청크들을 엮어서 프로그램 펑션을 만들어내는 것이다. 조금 수고스럽
지만, ROP 공격으로 하면 시간과 노력만큼 원래 함수가 수행할 오퍼레이션을 엮어서 만
들 수 있다. ROP를 설명할 때 이러한 그림들이 나온다. 신문 오려 붙여 만든 것. 코드 청
크를 묶어서 의미 있는 악성코드를 만들 수 있다는 것으로 ROP를 설명할 때 이렇게 표
현한다.
페이지 20 일반적인 프로그램이 실행되는 방식과 ROP의 방식을 비교. 일반적인 프로그
램의 실행은 프로그램 카운터 PC 혹은 EIP(x86에서)는 인스트럭션의 주소를 담는다. 쭉이동하면서 하나씩 인스트럭션이 실행되게 되는데, EIP로 컨트롤 플로우가 만들어지고
CPU는 EIP를 늘린다. 컨트롤 플로우를 제어한다.
페이지 21 ROP는 ESP가 프로그램 카운터 역할을 하게 된다. 리턴투립씨에서는 팝팝리턴
이다. 이건 스택에 저장되어 있었다. 해당 칸들을 스택으로 보면 코드 청크들이 들어가게
되고, 그 위치의 코드 청크들은 리턴으로 끝나게 된다. 리턴의 시멘틱을 생각하면 주소를
읽어와서 주소로 점프하는 것. ESP가 코드 청크를 가리키는 주소를 담고 있는 스택의 위
치를 담고 있는 것이고, 그렇기에 ESP가 프로그램의 실행 흐름을 제어하는 역할을 하게
되는 것이다.
페이지 22 ROP의 설명 부가. 튜링 컴플리트 오퍼레이션을 할 수 있다. 완벽한 펑션을 만
들어낼 수 있다는 것이다. 이미 올라와져 있는 코드 영역이 클수록 사용할 코드 청크들
이 많으니 더하기 빼기 메모리 로드 스토어 등등의 프로그램 실행에 필요한 기본적인 어
셈블리 인스트럭션들이 존재하기 때문에 이걸 엮어서 하나의 프로그램을 만들 수 있는
것이다. 어셈블리 변환 하고 쉘코드를 만들고 이렇게 하는 건 C나 C++로 프로그래밍하
는데 어셈블리로도 변경할 수 있고, 결국에는 어셈블리를 이용해서 같은 프로그램을 만
들 수 있는 것이다. 공격자는 기본적인 오퍼레이션들을 찾아서 엮어서 같은 역할을 수행
하는 함수를 스택을 조절해서 만들 수 있는 것이다. 방어방법에 의해서 제거되더라도 연
연하지 않고 작은 코드 청크들을 엮어서 공격할 수 있는 것이다. unintended instruction
을 찾아서 공격할 수 있는 것이다. 이런 걸 컴파일러나 어셈블러로 방어하기가 어렵다.
인스트럭션들을 제거할 수 없는 것이다. 연구 논문들을 보면, 리턴을 제거하고 공격의 활
용될 수 있는 청크들을 같은 의미에 다른 형태로 바꿔서 방어하기는 어렵다는 것이다.
페이지 23 가젯들의 예씨를 보자. No-ops. 아무것도 하지 않는다. 0x90으로 인텔에선 머
신 코드를 갖는다. 아무 행위도 안 하는 인스트럭션. EIP를 움직이기만 한다. 시스템 상태
를 변화하지 않고, 단순히 EIP를 다음 주소로 옮겨주는 역할만 한다. 이것과 비슷한 역할
을 하는 것은 리턴만을 이용하는 것이다. 스택의 주소를 읽어와서 그 주소로 점프하는
것이다. 리턴 어드레스 오브 리턴이다. 아무것도 안 하고, 리턴하고, 다시 리턴할 때는 주
소를 읽고 또 리턴하고 또 주소로 가서 보니 리턴이다. 리턴만 있는 코드청크를 이용하
면 NOP의 효과를 볼 수 있다.
페이지 24 move 같은 건 레지스터의 값을 옮기는 것이다. 상수를 옮기는 것을 mov eax,
0xDEADBEEF로, eax에 상수를 넣는데, ROP에서도 가능하다. 해당 코드 청크들을 이용하
면 된다. ESP 값이 있는데, pop eax ret라는 코드가 있는 주소가 있다. 리턴하게 되어 해
당 코드 청크를 실행하게 되고, 윗 부분에 있는 숫자를 읽게 된다. 공격자가 설정한 수로
스택에서 읽을 수 있는 것이다. 결국에는 ROP로 move constant 오퍼레이션을 수행할 수
있다.페이지 25 점프는 EIP 값에 새로운 메모리 주소로 넣는 것이다. 메모리 어딘가로 점프하
게 되는 오퍼레이션이다. 동일한 것을 하는 ROP는 ESP가 EIP 역할을 하게 되니, 점프를
시킬 수 있는 것이다. pop ESP ret 라는 가젯을 이용한다. 스택에 있는 값을 ESP에 넣고,
새 값으로 세팅이 되면, 스택 포인터가 쭉 이동하게 된다. 예제에서는 0x1016으로 되고,
점프와 동일한 효과를 본다.
페이지 26 그리고 데이터 스토어, 무브해서 메모리 주소에 값을 넣는 것. 비슷하게 할
수 있다. 여러 가지 가젯을 이용한다. pop EBX ret은 EBX에 숫자를 넣고, 리턴은 그걸 참
조하니 pop EAX로 가서 실행되어서 EAX에 값을 또 넣고 리턴되어 mov EAX, EBX ret가
있다. 이 명령이 이전에 세팅된 EAX, EBX를 이용해서 값을 넣는다. EAX에 있는 주소에
있는 메모리를 EBX로 써라.
페이지 27 conditional jump도 가능하다. 좀 더 많은 가젯을 이용해야한다. 조건에 따라
서 점프 혹은 그 다음 인스트럭션 실행. ROP로 구현이 가능하다. 전략이 있다. 다양한 방
법으로 가능하지만 여기선 EFLAG라는 상태값을 저장하는 것을 이용해서 공격하는 예제
이다.
페이지 28 EFLAG 를 이용해서, 특정한 컨디션을 세팅해서 세팅된 값을 이용해서 컨디셔
널 분기문을 작성하는 것이다. 플래그에서 carry, parity flag들이 있다. 그런 것들은 스택
에 넣어두고, 스택 값을 넣어두고 참조해서 이용한다. 플래그 값을 스택에 넣어주는
pushf라는 명령이 있다. 프로그램에서 잘 사용하지 않기에 활용하기는 어렵다. 이후 예제
에서는 다른 방법을 이용하도록 한다.
페이지 29 adc라는 명령어는 add with carry이다. 이전에 계산된 값에서 EFLAG 레지스터
의 캐리비트가 세팅이 되어 있으면 세팅된 값도 함께 더하기할 때, 캐리도 같이 더해준
다. ADD EDX, 0은 EDX + 0으로 값 변화가 없지만, adc EDX, 0이라면 EDX = EDX + 0 +
CF로 연산이 된다. 그냥 add와 adc의 차이가 있는데 플래그 값을 더하냐 안 하냐의 차
이다. 이런 명령을 사용하는 건 EFLAG라는 특수 레지스터를 추출해서 스택에 넣어두기
위한 전초작업이다.
페이지 30 adc를 어떻게 활용하는지. 이 예제에서는 캐리값은 이미 플래그에 세팅이 되
어있다고 가정. EFLAG에 있는 값을 스택에 저장하는 방법을 보자. 이 부분은 먼저 어떤
버퍼오버플로우로 컨트롤 플로우 하이재킹으로 넘겨지고, 주소에 있는 위치로 넘어가서
가젯이 실행된다. 그게 pop ecx pop edx ret이다. ecx에 0이 들어가고, 그 위에가 edx에
저장된다. 스택의 어떤 어드레스를 가리키고 있는 주소가 넣어진다. 그곳에는 캐리가 담
겨질 것이다. 예를 들어 그 주소가 0x1008로 edx에 0x1008이 저장될 것이다. 위는 adc
cl, cl, ret의 가젯이 있다. 여기서 adc 명령어로 캐리값을 함께 더해주는 명령어가 된다.
이 세팅에서 cl과 함께 캐리가 들어가는 것으로 ecx는 0으로 세팅되어 있으니 ecx 레지스터에 캐리값만을 집어넣는 오퍼레이션이 된다. 그리고, ecx에는 CF의 값이 들어가 있
다. 리턴이 되면 그 참조되는 리턴에는 movl ecx (edx) 이다. ATT 폼이니, ecx의 값을 edx
가 가리키는 주소에 넣게 되는 것이다. 1008이라는 주소에 ecx 값이 들어가게 된다. 즉
CF 값이 들어가게 된다. EFLAG라는 시스템 레지스터의 값을 스택에 넣는 방식이다. 3번
전략은 CF를 이용해서 컨디셔널 점프하는 것을 보일 것이다.
페이지 31 앞에서 CF값을 결정했고, 어떻게 ESP를 이동하는 오프셋을 보자. 앞서서 리턴
으로 끝내는 코드 가젯을 실행했고 esp는 해당 그림에 위치한다. 거기에는 pop ebx ret
의 가젯이 있다. pop ebx를 실행하면, 위에 있는 주소가 ebx가 들어간다. 그 주소의 값과
CF가 저장되어있는 주소(1094라고 가정)까지의 거리가 94라면 그 주소에 들어가 있는
수는 1000이다. 이제 저장된 이유는 뒤에서 설명할 것. 리턴이 실행되면 1000 위의 부분
이 실행되는데 거기에는 negl 94(ebx) 2의 보수를 구하는 negl이다. 2의 보수를 구하는
게 각 비트를 0이면 1, 1이면 0하고 1더해주면 되는 것이다. negl 1은 0xfffffff가 되는 것.
0의 보수는 오버플로우로 0x0이 된다. ebx에 있는 값을 주소로 해서 그 주소 플러스 94
에 위치에 있는 것의 2의 보수를 구하는 것이다. 결국에는 1000이었던 것은 공격자가 활
용가능한 옵션들을 이런 것만 찾은 것이었다. 94 더하는 명령어가 아니었으면 그냥 1094
로 직접 메모리에 넣어뒀을 것이다. 그래서 2의 보수를 구한 이유는 esp값이 얼마만큼
이동할 오프셋을 결정하기 위해서 이러한 인스트럭션을 사용한 것이다. 이런 코드 가젯
들이 필요 없지만 리턴을 찾다보니 pop edi pop edp mov esi esi 는 쓰레기 값만 넣어서
아무 의미 없도록 한다. 마지막 리턴이 일어나고 pop esi가 있는 가젯으로 이동하게 된
다. pop esi하면 esp_
delta라는 esp값을 움직일 오프셋 값이 esi에 들어가서 리턴으로 그
위에가 실행된다. pop ecx pop ebx로, ecx에는 그 위의 값이 저장되는데 그 값은 CF를 가
리키는 주소로 세팅이 되어 있어서 ECX에는 CF의 주소가 들어가게 된다. EBX는 쓸모 없
는 애로, 쓸모없는 주소를 가리키게 한다. 의미 없고, 리턴하게 되면 andl 부터 시작하게
된다. 이 네모친 부분이 esp가 얼마만큼 이동할 것인지 오프셋을 결정하는 것이다. 앤드
연산을 했는데 esi는 현재 델타, 즉 오프셋이 저장되어 있고, ecx는 0 또는 1이 저장되어
있다. 계산했던것으로 앤드 연산되어서 0 and ㅁㅁ 0xfffff and ㅁㅁ이다. 0이나 ㅁㅁ이냐.
이 네모가 오프셋이기 때문에 esp값을 피보팅할 것이다. 오프셋만큼 이동할 거냐 아니면
0을 해서 esp 값을 점프하지 않을 것이냐의 부분이다. 그 아래는 가젯을 찾다보니 필요
없는 것들이 있던 것이다. 의미 없는 것들. 리턴하고 어딘가로 갈 것이다.
페이지 32 마지막 컨디셔널 점프로 ESP 값을 조정하는 것이다. perturbation 값. 즉, 델타
값이 저장되어 있는 절대값을 이용해서 esp 값을 조절한다. 조절은 간단하다. pop eax가
되면 델타값이 저장되어 있는 위치를 가리키는 주소를 들어가게 되고, 리턴 되고 가젯이
실행된다. addl (eax) esp eax에 있는 값을 esp에 더해라 라는 명령이다. eax에는 오프셋
값이 있고, 0 또는 델타 값이 있다. 0을 더하면 그냥 esp 유지. 그렇지 않은 경우는 변화
가 생긴다. esp가 프로그램 카운터 역할을 하니 컨디셔널 점프 역할을 할 수 있게 되는것이다. 0, 오프셋에 따라서 컨디셔널 점프를 실행하게 된다. 그 아래는 이제 쓸모없는
코드 부분이다. 리턴 되고 계속 실시된다.
페이지 33 간단한 예제로 상기해보자. 어떻게 스택이 구현되며 코드 실행되고, 시스템
스테이터스가 변하는지 보자. 공격자가 스택을 꾸며놓고, 현재 부분에 esp로 첫 리턴이
되어 컨트롤 플로우 하이재킹이 된 거다. 그렇게 코드들이 실행된다. pop eax ret 가젯이
실행되고, eax에는 1이 들어간다. 마지막 코드는 ebx의 위치에 3을 넣게 되는 것이다.
페이지 34 가젯들을 이용한다? 이 가젯들을 찾는 툴들은? 여기다. 가젯 찾는 법은 비슷
하고, 시스템 아키텍쳐마다 다르긴 하다. 암에서는 4바이트 2바이트로 정해져 있고, 메모
리 읽을 때, 딱딱 정해진 위치대로 찾는다. 그렇게 명령어를 찾아서 한다. x86은 메모리
에 어떤 값이 있는데, 어디서부터 읽고 해석하는지에 따라 명령이 달라진다. 명령어 길이
가 가변적이기 때문에 다른 명령을 찾아낼 수 있는 것이다. 어디서부터 명령어를 찾느냐.
메모리 어디부터 찾느냐가 똑같은 부분이어도 가젯을 찾을 수 있고 못 찾을 수도 있다.
페이지 35 ROP 공격 활용 가젯 찾기. 그 중에서 유명한게 갈릴레오 알고리즘이다. 이 알
고리즘은 간단하다. 코드 영역이 있을 때, 어디서부터 해석하느냐에 따라서 리턴 어드레
스가 있는 부분을 찾을 수도 있고, 아니면 다른 명령어를 찾을 수도 있다. 여기서는 바이
너리 코드 영역이 있으면 리턴이 해석될 수 있는 c3 바이트 코드. 리턴이 있는 부분을
찾는다음에 1바이트, 2바이트 인스트럭션을 해석해보면서 필요한 가젯들을 추출하는 것
이다. 1바이트에 유효한 명령어가 있으면 다시 해석해보면서 유효한 명령어들이 나올때
까지 해석한다. 1바이트 해석, 2바이트 해석, 3바이트 해석, 이렇게 유효한 명령이 나올
때까지 backward로 바이너리를 분석하며 가젯을 찾는 알고리즘이다. LibC같은 이미 메모
리에 올라와 있는 코드영역. LibC 사이즈가 굉장히 크니 가젯을 찾을 수 있는 확률이 높
아진다. 리턴 인스트럭션을 먼저 찾고, 거기부터 뒤로 계속 해석해본다.
페이지 36 갈릴레오 알고리즘은 이와 같다. 앞에서 설명한대로 동작한다. 리턴인스트럭
션부터 시작해서 앞쪽으로 이동하면서 크기 1,2,3 바이트 차례대로 해석하면서 유효 명
령어를 찾는다. boring 인스트럭션은 유효하지 않으니 안 쓴다. 3번째 거는 점프하고, 리
턴이 따라오는 가젯은 점프에서 리턴이 실행되지 않으니 버린다.1번은 컴파일러에서 정
상적으로 넣어준 건 빼겠다는 것이다. 펑션 에필로그 같은 부분. 컴파일러에서 정상적으
로 넣어주는 데 방어기법에 의해서 제거될 수 있다. 이 알고리즘에서는 정상적으로 넣어
진 인스트럭션은 빼고, unintended 인스트럭션으로 구성된 것들만 찾기. 오픈 소스 툴을
이용해서 가젯 찾아서 공격하기.
'[활동 정리] - 비밀번호 : helloㅁㅁㅁ > [2024]동계 모각코 개인' 카테고리의 다른 글
[모각코] 동계 모각코 6회차 개인목표 및 결과 (0) | 2025.02.13 |
---|---|
[모각코] 동계 모각코 5회차 개인목표 및 결과 (0) | 2025.01.21 |
[모각코] 동계 모각코 3회차 개인목표 및 결과 (0) | 2025.01.21 |
[모각코] 동계 모각코 2회차 개인목표 및 결과 (0) | 2025.01.10 |
[모각코] 동계 모각코 1회차 개인목표 및 결과 (0) | 2025.01.10 |
4회차 목표 : 정보보안 공부
4회차 결과 : Code Reuse Attack
Code Reuse Attack
쉘코드를 직접 삽입해서 공격하기 어려운 경우에 이걸 사용할 수 있다!
페이지2 malicious code는 악성 코드로 악성 행위를 실시한다. 다만, normal code라고 해
서 이게 항상 malicious하지 않게 수행하는 건 아니다.
페이지 3 정상적인 normal code를 악용해서 비정상적인 악성행위를 할 수 있고, 그걸 가
능하게 하는 방법이 code reuse attack이 있다.
페이지 4 해당 방식의 정의. 말리셔스 코드 없이 정상 코드를 이용해서 공격을 실시한다.
정상적인 코드를 묶어서 하는 공격이기 때문에, 프로그램 크기가 클수록 공격자가 활용
할 수 있는 코드 청크들이 많다. 공격을 성공률이 높아진다.
페이지 5 코드 리유즈 어택 이전에 그 방식의 배경을 알아보자. 스택이 있으면 리턴 어
드레스, 버퍼가 있다. 버퍼가 scanf같은 API를 잘못 사용해서 취약점이 있고, 그 취약점에
의해서 공격 대상이 되는 것이다. 버퍼로 오버라이트하고, 리턴 어드레스를 공격자가 덮
어 쓸 수 있다. 쉘코드를 넣어서 리턴 어드레스가 뛰도록하면 공격자가 공격을 실행할
수 있다. 스택 영역에는 실행권한이 있기 때문에 공격이 가능했다. 이런 것들의 방어기법
으로 데이터 영역을 실행할 수 없게 no execute로 페이지 테이블의 플래그를 세팅하든지데이터 영역을 실행하지 못 하는 방어기법이 등장했다. 스택에 쉘코드를 넣어도 그 영역
으로도 실행이 안 된다. 이런 데이터 익시큐션 프리벤션으로 새로운 공격기법을 찾게 되
었다.
페이지 6 DEP는 writeable page를 non-executable하게 만드는 것이다. 프로그램 스택이
익시큐터블이 아니다. 이런 간단한 공격기법이 안 된다. PaX라는 방어로 Linux는 DEP를
한다. 하드웨어 서폿으로 하기도 한다. ARM의 플래그로, 유저 영역의 DEP만 적용, WXN
은 모든 영역에서 적용. 그런데 모든 영역으로 하면, 자바스크립트에서 컴파일해서 실행
되는 부분같은 건 그러면 안 된다. 이건 드문 경우이다. Intel도 있고, 시스템 레지스터 플
래그들이 존재한다. 이름만 다를 뿐.
페이지 7 그림을 보면, 스택에서 말리셔스 부분을 쉘코드로 넣어서 실행했는데, 실행이
불가능하도록 하면, 실행 흐름을 변화시켜도 exception이 발생하게 된다.
페이지 8 DEP를 바이패스하려는 방법
페이지 9 DEP 환경에서 악성행위를 할 수 있을까? 공격자가 삽입한 코드보다, 이미 있는
걸로 수행하는 것이다. LIBC는 C 라이브러리이다. 여기서 대부분의 프로그램들이 사용하
는데, API로 점프해서 하는 게 Return to LIBC이다. 공격자가 제공하는 파라미터를 이용하
든지, 새로운 쉘을 실행하든지, 메모리 영역에 매핑하든지 그런 악성행위를 실행할 수 있
다.
페이지 10 공격시나리오를 보면, 정상적인 스택이 구현되어 있다. 이전 function에 대한
주소들 등등이 있다. 버퍼를 넘치게 해서 오버라이트를 한다. 공격자가 점프할 주소, 점
프할 주소에서 실행되는 함수의 파라미터를 쓴다. 시스템 함수로 뛰어가게 되는데 시스
템 인풋을 받아서 쉘을 띄우는 공격형태이다. 한 칸 4바이트 띄운 이유는 새 펑션이 실
행되면 항상 파라미터를 읽어오는 부분이 4바이트 띄어져 있는 형태이기 때문이다. 스택
포인터가 있고, 파라미터가 있다. 그렇기에 리턴 어드레스를 띄어야 한다. 시스템으로 점
프하면 파라미터는 4바이트 띄어진 메모리 부분을 읽게 된다. 그렇기에 세팅을 이와 같
이 했다.
페이지 11 그러면, 하나의 함수를 콜하는 공격을 통해서 함수를 콜하는 형태. 공격자가
실행하고 싶은 함수의 주소를 적어두고 4바이트 띄우고 파라미터를 넣어둔다. 함수를 실
행하며 파라미터들을 사용하게 된다.
페이지 12 두 개 이상의 함수를 실행해야하는 요구가 있을 때, 스택을 이와 같이 꾸미면
어떻게 되는가? 첫 펑션이 실행되며 파라미터를 사용하게 된다. 그 다음에 리턴을 두 번
째 함수가 호출하게 된다. 그런데 파라미터가 필요한 경우 4바이트 띄어진 부분을 읽어
가게 되는데 이미 첫 함수에서 사용할 파라미터로 세팅되어 있다. 이런 식이면 파라미터
세팅이 쉽지 않다. 이 부분이 어려우면 이전 수업의 메모리를 보면서 공부하기.페이지 13 두 번째 함수의 파라미터는 어떻게 하는가?
페이지 14 여기서 스택 한 번 다시 꾸며보자
페이지 15 공격자 입장에서는 이와같이 꾸밀 수 있다. 버퍼를 덮어 쓰고, 리턴어드레스
에 첫 번째 주소를 넣는다. 그 다음에 파라미터 두 개를 사용하고 첫 함수가 끝나면 어
딘가로 리턴하게 되는데 어디인지 참조하게 될 것이다.pop/pop/ret이라는 코드 조각으로
세팅 된 걸 보고, 실행흐름이 이렇게 된다. pop/pop 인스트럭션을 이용하게 된다. 이런
식으로 세팅한 이유는 팝을 하면 스택에서 4바이트 만큼 꺼낸다. 리턴이 실행된 시점에
서 팝이 실행되면 첫 번째 파라미터가 레지스터에 들어가고, 스택 포인터가 움직여지고,
또 팝 되면 움직여진다음에 리턴이 되면 해당 주소를 읽게 되고, 그 주소는 두 번 째 함
수의 주소이다. 앞서 읽은 것처럼 4바이트 띄우고 2번째 함수의 파라미터들을 읽게 된
다. 이렇게 해결이 가능하게 됐다. 해결에 기여한 게 "팝팝리턴" 코드 조각이다. 코드 청
크를 공격자가 메모리 어딘가에 찾아냈고, 이 코드 청크의 주소를 갖고와서 메모리에서
읽게 한 것이다.
페이지 16 리턴 어드레스가 덮여 쓰이고, 첫 함수로 점프하고, 파라미터들을 읽고, 끝난
다음에 1004의 주소를 와서 인스트럭션을 하게 된다. 거기는 팝팝리턴의 주소이고 그 메
모리 영역으로 점프해서 이동하게 된다. 스택 포인터를 옮기기 위한 역할을 수행한다. 첫
함수의 파라미터들을 지나쳐서 두 번째 함수를 실행하게 된다. 세컨드 함수로 점프하게
되어서 파라미터를 사용하게 되면 4바이트를 넘어서 그 다음에 파라미터들을 읽게 된다.
두 함수의 파라미터 세팅을 코드 조각 팝팝리턴으로 이용하게 됐다.
페이지 17 해당 코드들이 메모리에 존재하고 있다는 것이다.
페이지 18 리턴 투 립씨는 이미 있는 함수 덩어리를 사용한다면 지금부터 보는 회귀지
향 프로그래밍 방법에서는 이런 조그마한 코드 청크들을 이용해서 공격하는 기법을 말한
다. 가젯이라고도 하는 코드 청크들을 묶어서 공격하는 것이다.
페이지 19 펑션들을 지움으로써 공격자를 방어하는 것이다. 필요없는 함수, 사용될 API
만 올려두고 필요 없는 건 올려두지 않는다. 시스템함수들이 공격자들이 손쉽게 쓰는 것
들이니 그런 것들을 메모리에 두지 않는다. 이러면 리턴 투 립씨 공격이 안 통한다. 그러
니 조그마한 코드 청크들을 엮어서 프로그램 펑션을 만들어내는 것이다. 조금 수고스럽
지만, ROP 공격으로 하면 시간과 노력만큼 원래 함수가 수행할 오퍼레이션을 엮어서 만
들 수 있다. ROP를 설명할 때 이러한 그림들이 나온다. 신문 오려 붙여 만든 것. 코드 청
크를 묶어서 의미 있는 악성코드를 만들 수 있다는 것으로 ROP를 설명할 때 이렇게 표
현한다.
페이지 20 일반적인 프로그램이 실행되는 방식과 ROP의 방식을 비교. 일반적인 프로그
램의 실행은 프로그램 카운터 PC 혹은 EIP(x86에서)는 인스트럭션의 주소를 담는다. 쭉이동하면서 하나씩 인스트럭션이 실행되게 되는데, EIP로 컨트롤 플로우가 만들어지고
CPU는 EIP를 늘린다. 컨트롤 플로우를 제어한다.
페이지 21 ROP는 ESP가 프로그램 카운터 역할을 하게 된다. 리턴투립씨에서는 팝팝리턴
이다. 이건 스택에 저장되어 있었다. 해당 칸들을 스택으로 보면 코드 청크들이 들어가게
되고, 그 위치의 코드 청크들은 리턴으로 끝나게 된다. 리턴의 시멘틱을 생각하면 주소를
읽어와서 주소로 점프하는 것. ESP가 코드 청크를 가리키는 주소를 담고 있는 스택의 위
치를 담고 있는 것이고, 그렇기에 ESP가 프로그램의 실행 흐름을 제어하는 역할을 하게
되는 것이다.
페이지 22 ROP의 설명 부가. 튜링 컴플리트 오퍼레이션을 할 수 있다. 완벽한 펑션을 만
들어낼 수 있다는 것이다. 이미 올라와져 있는 코드 영역이 클수록 사용할 코드 청크들
이 많으니 더하기 빼기 메모리 로드 스토어 등등의 프로그램 실행에 필요한 기본적인 어
셈블리 인스트럭션들이 존재하기 때문에 이걸 엮어서 하나의 프로그램을 만들 수 있는
것이다. 어셈블리 변환 하고 쉘코드를 만들고 이렇게 하는 건 C나 C++로 프로그래밍하
는데 어셈블리로도 변경할 수 있고, 결국에는 어셈블리를 이용해서 같은 프로그램을 만
들 수 있는 것이다. 공격자는 기본적인 오퍼레이션들을 찾아서 엮어서 같은 역할을 수행
하는 함수를 스택을 조절해서 만들 수 있는 것이다. 방어방법에 의해서 제거되더라도 연
연하지 않고 작은 코드 청크들을 엮어서 공격할 수 있는 것이다. unintended instruction
을 찾아서 공격할 수 있는 것이다. 이런 걸 컴파일러나 어셈블러로 방어하기가 어렵다.
인스트럭션들을 제거할 수 없는 것이다. 연구 논문들을 보면, 리턴을 제거하고 공격의 활
용될 수 있는 청크들을 같은 의미에 다른 형태로 바꿔서 방어하기는 어렵다는 것이다.
페이지 23 가젯들의 예씨를 보자. No-ops. 아무것도 하지 않는다. 0x90으로 인텔에선 머
신 코드를 갖는다. 아무 행위도 안 하는 인스트럭션. EIP를 움직이기만 한다. 시스템 상태
를 변화하지 않고, 단순히 EIP를 다음 주소로 옮겨주는 역할만 한다. 이것과 비슷한 역할
을 하는 것은 리턴만을 이용하는 것이다. 스택의 주소를 읽어와서 그 주소로 점프하는
것이다. 리턴 어드레스 오브 리턴이다. 아무것도 안 하고, 리턴하고, 다시 리턴할 때는 주
소를 읽고 또 리턴하고 또 주소로 가서 보니 리턴이다. 리턴만 있는 코드청크를 이용하
면 NOP의 효과를 볼 수 있다.
페이지 24 move 같은 건 레지스터의 값을 옮기는 것이다. 상수를 옮기는 것을 mov eax,
0xDEADBEEF로, eax에 상수를 넣는데, ROP에서도 가능하다. 해당 코드 청크들을 이용하
면 된다. ESP 값이 있는데, pop eax ret라는 코드가 있는 주소가 있다. 리턴하게 되어 해
당 코드 청크를 실행하게 되고, 윗 부분에 있는 숫자를 읽게 된다. 공격자가 설정한 수로
스택에서 읽을 수 있는 것이다. 결국에는 ROP로 move constant 오퍼레이션을 수행할 수
있다.페이지 25 점프는 EIP 값에 새로운 메모리 주소로 넣는 것이다. 메모리 어딘가로 점프하
게 되는 오퍼레이션이다. 동일한 것을 하는 ROP는 ESP가 EIP 역할을 하게 되니, 점프를
시킬 수 있는 것이다. pop ESP ret 라는 가젯을 이용한다. 스택에 있는 값을 ESP에 넣고,
새 값으로 세팅이 되면, 스택 포인터가 쭉 이동하게 된다. 예제에서는 0x1016으로 되고,
점프와 동일한 효과를 본다.
페이지 26 그리고 데이터 스토어, 무브해서 메모리 주소에 값을 넣는 것. 비슷하게 할
수 있다. 여러 가지 가젯을 이용한다. pop EBX ret은 EBX에 숫자를 넣고, 리턴은 그걸 참
조하니 pop EAX로 가서 실행되어서 EAX에 값을 또 넣고 리턴되어 mov EAX, EBX ret가
있다. 이 명령이 이전에 세팅된 EAX, EBX를 이용해서 값을 넣는다. EAX에 있는 주소에
있는 메모리를 EBX로 써라.
페이지 27 conditional jump도 가능하다. 좀 더 많은 가젯을 이용해야한다. 조건에 따라
서 점프 혹은 그 다음 인스트럭션 실행. ROP로 구현이 가능하다. 전략이 있다. 다양한 방
법으로 가능하지만 여기선 EFLAG라는 상태값을 저장하는 것을 이용해서 공격하는 예제
이다.
페이지 28 EFLAG 를 이용해서, 특정한 컨디션을 세팅해서 세팅된 값을 이용해서 컨디셔
널 분기문을 작성하는 것이다. 플래그에서 carry, parity flag들이 있다. 그런 것들은 스택
에 넣어두고, 스택 값을 넣어두고 참조해서 이용한다. 플래그 값을 스택에 넣어주는
pushf라는 명령이 있다. 프로그램에서 잘 사용하지 않기에 활용하기는 어렵다. 이후 예제
에서는 다른 방법을 이용하도록 한다.
페이지 29 adc라는 명령어는 add with carry이다. 이전에 계산된 값에서 EFLAG 레지스터
의 캐리비트가 세팅이 되어 있으면 세팅된 값도 함께 더하기할 때, 캐리도 같이 더해준
다. ADD EDX, 0은 EDX + 0으로 값 변화가 없지만, adc EDX, 0이라면 EDX = EDX + 0 +
CF로 연산이 된다. 그냥 add와 adc의 차이가 있는데 플래그 값을 더하냐 안 하냐의 차
이다. 이런 명령을 사용하는 건 EFLAG라는 특수 레지스터를 추출해서 스택에 넣어두기
위한 전초작업이다.
페이지 30 adc를 어떻게 활용하는지. 이 예제에서는 캐리값은 이미 플래그에 세팅이 되
어있다고 가정. EFLAG에 있는 값을 스택에 저장하는 방법을 보자. 이 부분은 먼저 어떤
버퍼오버플로우로 컨트롤 플로우 하이재킹으로 넘겨지고, 주소에 있는 위치로 넘어가서
가젯이 실행된다. 그게 pop ecx pop edx ret이다. ecx에 0이 들어가고, 그 위에가 edx에
저장된다. 스택의 어떤 어드레스를 가리키고 있는 주소가 넣어진다. 그곳에는 캐리가 담
겨질 것이다. 예를 들어 그 주소가 0x1008로 edx에 0x1008이 저장될 것이다. 위는 adc
cl, cl, ret의 가젯이 있다. 여기서 adc 명령어로 캐리값을 함께 더해주는 명령어가 된다.
이 세팅에서 cl과 함께 캐리가 들어가는 것으로 ecx는 0으로 세팅되어 있으니 ecx 레지스터에 캐리값만을 집어넣는 오퍼레이션이 된다. 그리고, ecx에는 CF의 값이 들어가 있
다. 리턴이 되면 그 참조되는 리턴에는 movl ecx (edx) 이다. ATT 폼이니, ecx의 값을 edx
가 가리키는 주소에 넣게 되는 것이다. 1008이라는 주소에 ecx 값이 들어가게 된다. 즉
CF 값이 들어가게 된다. EFLAG라는 시스템 레지스터의 값을 스택에 넣는 방식이다. 3번
전략은 CF를 이용해서 컨디셔널 점프하는 것을 보일 것이다.
페이지 31 앞에서 CF값을 결정했고, 어떻게 ESP를 이동하는 오프셋을 보자. 앞서서 리턴
으로 끝내는 코드 가젯을 실행했고 esp는 해당 그림에 위치한다. 거기에는 pop ebx ret
의 가젯이 있다. pop ebx를 실행하면, 위에 있는 주소가 ebx가 들어간다. 그 주소의 값과
CF가 저장되어있는 주소(1094라고 가정)까지의 거리가 94라면 그 주소에 들어가 있는
수는 1000이다. 이제 저장된 이유는 뒤에서 설명할 것. 리턴이 실행되면 1000 위의 부분
이 실행되는데 거기에는 negl 94(ebx) 2의 보수를 구하는 negl이다. 2의 보수를 구하는
게 각 비트를 0이면 1, 1이면 0하고 1더해주면 되는 것이다. negl 1은 0xfffffff가 되는 것.
0의 보수는 오버플로우로 0x0이 된다. ebx에 있는 값을 주소로 해서 그 주소 플러스 94
에 위치에 있는 것의 2의 보수를 구하는 것이다. 결국에는 1000이었던 것은 공격자가 활
용가능한 옵션들을 이런 것만 찾은 것이었다. 94 더하는 명령어가 아니었으면 그냥 1094
로 직접 메모리에 넣어뒀을 것이다. 그래서 2의 보수를 구한 이유는 esp값이 얼마만큼
이동할 오프셋을 결정하기 위해서 이러한 인스트럭션을 사용한 것이다. 이런 코드 가젯
들이 필요 없지만 리턴을 찾다보니 pop edi pop edp mov esi esi 는 쓰레기 값만 넣어서
아무 의미 없도록 한다. 마지막 리턴이 일어나고 pop esi가 있는 가젯으로 이동하게 된
다. pop esi하면 esp_
delta라는 esp값을 움직일 오프셋 값이 esi에 들어가서 리턴으로 그
위에가 실행된다. pop ecx pop ebx로, ecx에는 그 위의 값이 저장되는데 그 값은 CF를 가
리키는 주소로 세팅이 되어 있어서 ECX에는 CF의 주소가 들어가게 된다. EBX는 쓸모 없
는 애로, 쓸모없는 주소를 가리키게 한다. 의미 없고, 리턴하게 되면 andl 부터 시작하게
된다. 이 네모친 부분이 esp가 얼마만큼 이동할 것인지 오프셋을 결정하는 것이다. 앤드
연산을 했는데 esi는 현재 델타, 즉 오프셋이 저장되어 있고, ecx는 0 또는 1이 저장되어
있다. 계산했던것으로 앤드 연산되어서 0 and ㅁㅁ 0xfffff and ㅁㅁ이다. 0이나 ㅁㅁ이냐.
이 네모가 오프셋이기 때문에 esp값을 피보팅할 것이다. 오프셋만큼 이동할 거냐 아니면
0을 해서 esp 값을 점프하지 않을 것이냐의 부분이다. 그 아래는 가젯을 찾다보니 필요
없는 것들이 있던 것이다. 의미 없는 것들. 리턴하고 어딘가로 갈 것이다.
페이지 32 마지막 컨디셔널 점프로 ESP 값을 조정하는 것이다. perturbation 값. 즉, 델타
값이 저장되어 있는 절대값을 이용해서 esp 값을 조절한다. 조절은 간단하다. pop eax가
되면 델타값이 저장되어 있는 위치를 가리키는 주소를 들어가게 되고, 리턴 되고 가젯이
실행된다. addl (eax) esp eax에 있는 값을 esp에 더해라 라는 명령이다. eax에는 오프셋
값이 있고, 0 또는 델타 값이 있다. 0을 더하면 그냥 esp 유지. 그렇지 않은 경우는 변화
가 생긴다. esp가 프로그램 카운터 역할을 하니 컨디셔널 점프 역할을 할 수 있게 되는것이다. 0, 오프셋에 따라서 컨디셔널 점프를 실행하게 된다. 그 아래는 이제 쓸모없는
코드 부분이다. 리턴 되고 계속 실시된다.
페이지 33 간단한 예제로 상기해보자. 어떻게 스택이 구현되며 코드 실행되고, 시스템
스테이터스가 변하는지 보자. 공격자가 스택을 꾸며놓고, 현재 부분에 esp로 첫 리턴이
되어 컨트롤 플로우 하이재킹이 된 거다. 그렇게 코드들이 실행된다. pop eax ret 가젯이
실행되고, eax에는 1이 들어간다. 마지막 코드는 ebx의 위치에 3을 넣게 되는 것이다.
페이지 34 가젯들을 이용한다? 이 가젯들을 찾는 툴들은? 여기다. 가젯 찾는 법은 비슷
하고, 시스템 아키텍쳐마다 다르긴 하다. 암에서는 4바이트 2바이트로 정해져 있고, 메모
리 읽을 때, 딱딱 정해진 위치대로 찾는다. 그렇게 명령어를 찾아서 한다. x86은 메모리
에 어떤 값이 있는데, 어디서부터 읽고 해석하는지에 따라 명령이 달라진다. 명령어 길이
가 가변적이기 때문에 다른 명령을 찾아낼 수 있는 것이다. 어디서부터 명령어를 찾느냐.
메모리 어디부터 찾느냐가 똑같은 부분이어도 가젯을 찾을 수 있고 못 찾을 수도 있다.
페이지 35 ROP 공격 활용 가젯 찾기. 그 중에서 유명한게 갈릴레오 알고리즘이다. 이 알
고리즘은 간단하다. 코드 영역이 있을 때, 어디서부터 해석하느냐에 따라서 리턴 어드레
스가 있는 부분을 찾을 수도 있고, 아니면 다른 명령어를 찾을 수도 있다. 여기서는 바이
너리 코드 영역이 있으면 리턴이 해석될 수 있는 c3 바이트 코드. 리턴이 있는 부분을
찾는다음에 1바이트, 2바이트 인스트럭션을 해석해보면서 필요한 가젯들을 추출하는 것
이다. 1바이트에 유효한 명령어가 있으면 다시 해석해보면서 유효한 명령어들이 나올때
까지 해석한다. 1바이트 해석, 2바이트 해석, 3바이트 해석, 이렇게 유효한 명령이 나올
때까지 backward로 바이너리를 분석하며 가젯을 찾는 알고리즘이다. LibC같은 이미 메모
리에 올라와 있는 코드영역. LibC 사이즈가 굉장히 크니 가젯을 찾을 수 있는 확률이 높
아진다. 리턴 인스트럭션을 먼저 찾고, 거기부터 뒤로 계속 해석해본다.
페이지 36 갈릴레오 알고리즘은 이와 같다. 앞에서 설명한대로 동작한다. 리턴인스트럭
션부터 시작해서 앞쪽으로 이동하면서 크기 1,2,3 바이트 차례대로 해석하면서 유효 명
령어를 찾는다. boring 인스트럭션은 유효하지 않으니 안 쓴다. 3번째 거는 점프하고, 리
턴이 따라오는 가젯은 점프에서 리턴이 실행되지 않으니 버린다.1번은 컴파일러에서 정
상적으로 넣어준 건 빼겠다는 것이다. 펑션 에필로그 같은 부분. 컴파일러에서 정상적으
로 넣어주는 데 방어기법에 의해서 제거될 수 있다. 이 알고리즘에서는 정상적으로 넣어
진 인스트럭션은 빼고, unintended 인스트럭션으로 구성된 것들만 찾기. 오픈 소스 툴을
이용해서 가젯 찾아서 공격하기.
'[활동 정리] - 비밀번호 : helloㅁㅁㅁ > [2024]동계 모각코 개인' 카테고리의 다른 글
[모각코] 동계 모각코 6회차 개인목표 및 결과 (0) | 2025.02.13 |
---|---|
[모각코] 동계 모각코 5회차 개인목표 및 결과 (0) | 2025.01.21 |
[모각코] 동계 모각코 3회차 개인목표 및 결과 (0) | 2025.01.21 |
[모각코] 동계 모각코 2회차 개인목표 및 결과 (0) | 2025.01.10 |
[모각코] 동계 모각코 1회차 개인목표 및 결과 (0) | 2025.01.10 |