내용 정리
익스플로잇의 기본 흐름
Preparing Heap Layout 과정

검증 1
분석 블로그를 보면 아래 글이 있다.
The ArrayBuffer size is selected so the underlying allocation
is of size 0x2608 (including the chunk metadata), which corresponds to
an LFH bucket not used by the application.
왜 0x2608바이트 크기의 ArrayBuffer를 선택하였는가?
리더기에서 사용되지 않는 할당 요청값이기 때문이다. 만약 리더기에서 자주 할당 및 해제하는 크기의 값을 ArrayBuffer로 선택한다면, 우리가 하려고 하는 작업인 UserBlock을 꽉 채우는 작업에 방해될 수 있기 때문이다.
그럼 공격자는 0x2608 크기의 할당이 리더기에서 사용되지 않음을 어떻게 확인했을까?
확인하려고 하는 것: 0x2608 크기의 할당이 리더기에서 사용되지 않음을 직접 확인하기
힙 할당시 호출되는 RtlAllocateHeap함수에 조건부 BP를 걸 것이다. RtlAllocateHeap의 인자로 넘어온 size값이 0x2600 이상이고 0x2610 이하일 때에만 BP가 걸리도록 조건부 BP를 거는 것이다.

이렇게 조건부 BP를 걸고 프로그램이 실행되도록 놔두면 Hits 값은 쭉쭉 올라가지만, 조건에 맞지 않아 실제로 BP가 걸리지는 않음을 알 수 있다. (Hits만 계속 올라감.)
즉, 0x2608 할당은 프로그램 안에서 수행되지 않으므로, 0x2608은 프로그램의 방해를 받지 않고 UserBlock을 꽉 채울 수 있는 할당 사이즈값임이 확인되었다.
검증 2
In order to set up such a layout, 0xd+0x11 ArrayBuffers of size 0x2608-0x10-0x8 are allocated.
만약 C에서 RtlAllocateHeap 함수에 size를 0x10을 넣어 호출하면, 실제 할당되는 메모리는 0x18 바이트다.
그 이유는, 청크의 정보가 들어갈 청크 헤더의 크기인 8바이트가 더 필요하기 때문이다.
우리는 현재, 할당된 청크의 최종 크기가 0x2608이었으면 좋겠다.
그래서 (0x2608 - 0x8)바이트를 할당 요청하면 최종적으로 0x2608바이트의 청크가 할당된다는 것도 알고있다.
근데 블로그 글에서는 0x8뿐만 아니라, 0x10을 추가로 빼는 것을 알 수 있는데,
이는 ArrayBuffer 객체를 만들 때 ArrayBuffer의 정보를 담을 공간을 JS가 추가로 0x10바이트만큼 더 할당하기 때문이다.
그럼 이것이 진실인지 직접 확인해봐야된다.
확인하려고 하는 것: ‘new ArrayBuffer(0x2608-0x10-0x8)’을 통해, 우리가 원하는 크기인 0x2608이 최종적으로 할당되는가?
app.alert("Click OK to start Spray!");
var arrayBuffers = new Array(0xd+0x11);
for (var i = 0; i < arrayBuffers.length; i++) {
arrayBuffers[i] = new ArrayBuffer(0x2608-0x10-0x8);
}
우선 테스트 PDF 안에 있는 JS를 위와 같이 바꾼다.

위와 같이 힙 할당에 사용되는 함수인 RtlAllocateHeap 함수에 조건부 BP를 건다.
이러면 RtlAllocateHeap 함수에 넘어온 size값이 2600이상이고 2610이하일 때에만 BP가 걸린다.
왜 RtlAllocateHeap에 BP를 거는가?: ArrayBuffer 객체를 생성할 때, 힙 할당을 위해 결국 calloc이 호출되며, calloc은 RtlAllocateHeap을 호출하기 때문이다.

이렇게 BP가 걸린 것을 확인할 수 있다.
이때 스택을 보면 할당 요청 size가 몇인지 알 수 있다.

0x2600바이트의 요청이 들어온 것을 확인할 수 있다.
0x2600바이트의 할당 요청이 들어왔으므로, RtlAllocateHeap 함수는 청크 헤더 크기인 8바이트까지 포함하여 총 0x2608 바이트를 할당할 것이다.
즉, new ArrayBuffer(0x2608 - 0x10 - 0x8)을 통해 우리가 원하는 크기인 0x2608이 최종적으로 할당된다는 것을 확인하였다.
검증 3
new ArrayBuffer(0x2608 - 0x10 - 0x8)을 통해 우리가 원하는 크기인 0x2608이 최종적으로 할당된다는 것을 확인하였고, 우리의 목표는 UserBlock을 다 덮는 것이다.
그것을 위해 UserBlock의 최대 크기가 몇인지를 알아야 한다.
확인하려고 하는 것: 몇 번째의 ArrayBuffer 할당부터 LFH가 관리하는 UserBlock에 할당되기 시작하는 것이며, 그 UserBlock의 최대 크기는 몇인가?

windbg에서도 역시 조건문 BP를 걸어서 ArrayBuffer를 생성하기 위한 크기의 RtlAllocateHeap이 호출되면 BP가 걸리게 만든다.

위 사진처럼 BP가 걸릴 때마다 pt; .echo [!] Allocated At:; r eax; !heap -x @eax; g 명령어를 통해, RtlAllocateHeap이 반환하는 청크 주소에 대한 정보를 출력하도록 한다.
그렇게 BP가 걸리고, 위 명령을 치고, BP가 걸리고, 위 명령을 치고를 반복하다 보면 아래와 같이 LFH가 활성화되는 지점을 만나게 된다.

위 사진은 18번째와 19번째 RtlAllocateHeap 호출의 BreakPoint Hit 이다.
18번째 호출을 보면 0x2608 사이즈가 아닌 0x20008(131.08KB)사이즈를 받아오는 것을 알 수 있는데, 이는 LFH가 활성화됨에 따라 0x20008 크기의 UserBlock을 할당받아온 것임을 알 수 있다.
LFH가 활성화되었기 때문에, 19번째 호출에서 청크 정보가 출력된 부분의 Flags를 보면, LFH 플래그가 활성화되었음을 알 수 있다.
그렇다면, LFH 플래그가 활성화돼있는 19번째 청크부터 UserBlock이 채워지기 시작한 것인가?
아니다. 여기에서 주의할 점은, 실제로 ‘UserBlock이라는 메모리 영토에 발을 들인 게 언제부터인가?’를 묻는다면 18번째 청크부터라고 답해야 한다. 18번째 호출에서 0x2608을 요청했는데, 운영체제가 따로 UserBlock을 받아오느라 0x20008의 할당을 받아왔기 때문에, 0x20008 안에 우리가 요청한 0x2608 사이즈의 청크가 들어있는 것이다.
우리는 UserBlock을 꽉 채워야 하는 상황이기 때문에, 이런 경계 조건을 잘 따져야 한다.
즉, LFH를 활성화시키기 위해 총 17번의 앞선 할당이 필요하고, 18번째 청크부터 UserBlock이 채워지는 것이다.
이때 할당받아온 UserBlock의 크기는 0x20008이므로, 아래 계산 결과와 같이 최대 0xd개의 청크가 UserBlock에 들어올 수 있게 된다. (0xe개부터는 0x20008을 넘어버리므로)

따라서 30(17 + 0xd)번째 할당이 UserBlock의 마지막 청크 할당이고, 이후의 31번째 할당부터는 새로운 UserBlock을 받아와 그곳에 할당한다는 것을 확인해보면 아래와 같다.

위 사진은 30번째 Hit과 31번째 Hit이다.
30(17 + 0xd)번째 청크를 할당받아오고, 그 다음인 31번째 청크를 할당할 때 0x42008 사이즈의 또 다른 UserBlock을 받아오는 것을 확인할 수 있다.
즉, 30번째 청크가 이 UserBlock의 마지막 청크였던 것을 알 수 있다.
결론: LFH을 활성화시키기 위해 총 17번의 앞선 ArrayBuffer 생성이 필요하고, 이후 0xd번 ArrayBuffer를 생성하면 LFH가 할당받아온 UserBlock이 꽉 채워진다는 것을 확인할 수 있었다.
'정보보안 > 시스템' 카테고리의 다른 글
| [Dreamhack] ROP(Return Oriented Programming) 문제 해설 & 추가 설명 (0) | 2024.08.09 |
|---|