imnyang's workspace

뒤로

Return Oriented Programming#

기본적인 스택 버퍼 오버플로 공격에서는 스택에 shellcode를 넣고 반환 주소를 shellcode가 저장된 위치로 변경했어요.

하지만 NX가 적용되면 스택에 저장한 shellcode를 직접 실행할 수 없어요. ASLR이 적용되면 스택과 공유 라이브러리의 주소도 실행할 때마다 달라져요.

이러한 환경에서는 새로운 코드를 입력해서 실행하는 대신 프로그램과 공유 라이브러리에 이미 존재하는 코드를 재사용할 수 있어요.

이때 사용하는 공격 기법이 **ROP (Return Oriented Programming)**예요.

ROP는 프로그램이나 공유 라이브러리의 코드 영역에 존재하는 짧은 명령어 조각을 연결하여 원하는 동작을 만드는 방식이에요.

Gadget#

ROP에서 사용하는 짧은 명령어 조각을 Gadget이라고 해요.

Gadget은 일반적으로 ret 명령어로 끝나요.

대표적인 Gadget은 다음과 같아요.

pop rdi
ret
asm

pop rdi는 스택에서 값을 하나 꺼내 RDI에 저장해요. 이후 ret은 스택에서 다음 주소를 꺼내 해당 주소로 실행 흐름을 이동시켜요.

스택의 첫 번째 값
→ pop rdi
→ RDI에 저장

스택의 다음 값
→ ret
→ 해당 주소로 이동
text

다른 레지스터를 설정하는 Gadget도 사용할 수 있어요.

pop rsi
ret
asm
pop rdx
ret
asm

바이너리에 따라 여러 레지스터를 한 번에 설정하는 Gadget이 존재하기도 해요.

pop rsi
pop r15
ret
asm

이러한 Gadget을 사용하려면 스택에 각 pop 명령어가 꺼낼 값을 모두 넣어야 해요.

pop rsi; pop r15; ret 주소
RSI에 넣을 값
R15에 넣을 값
다음 실행 주소
text

ROP Chain#

여러 Gadget의 주소와 데이터를 스택에 순서대로 배치한 구조를 ROP Chain이라고 해요.

함수가 return하면 첫 번째 Gadget으로 실행 흐름이 이동해요. Gadget의 마지막에 있는 ret은 스택에서 다음 주소를 꺼내 다른 Gadget이나 함수로 이동해요.

첫 번째 Gadget
→ ret
→ 두 번째 Gadget
→ ret
→ 세 번째 Gadget
→ ret
→ 원하는 함수
text

64비트 Linux의 함수 인자 전달 방식은 함수 호출 규약에서 정리했어요.

예를 들어 첫 번째 인자를 설정하고 함수를 호출하는 ROP Chain은 다음과 같이 구성할 수 있어요.

pop rdi; ret
첫 번째 인자
호출할 함수 주소
text

실행 흐름은 다음과 같아요.

pop rdi; ret 실행
→ 스택의 값을 RDI에 저장
→ 함수 주소로 return
→ 설정한 인자를 사용하여 함수 실행
text

ROP Chain을 이용하면 단순히 함수 하나를 호출하는 것뿐만 아니라 여러 동작을 순서대로 실행할 수 있어요.

메모리 주소 출력
→ 필요한 주소 계산
→ 메모리에 값 쓰기
→ 다른 함수 호출
text

libc 주소 유출#

ASLR이 적용되면 libc의 시작 주소는 프로그램을 실행할 때마다 달라져요.

하지만 libc 내부 함수들의 상대적인 위치는 변하지 않아요.

함수의 실제 주소
=
libc base
+
함수의 오프셋
text

따라서 libc 함수 하나의 실제 주소를 알아내면 libc의 시작 주소를 계산할 수 있어요.

libc_base = leaked_address - function_offset
py

libc의 시작 주소를 구한 뒤에는 다른 함수의 실제 주소도 계산할 수 있어요.

system_address = libc_base + system_offset
py

동적 링크된 함수의 주소가 저장되는 구조는 PLT와 GOT에서 정리했어요.

함수가 한 번 호출되고 나면 해당 함수의 GOT 엔트리에는 실제 libc 함수 주소가 저장돼요.

read@got
→ libc의 read 실제 주소
text

따라서 출력 함수를 ROP로 호출하여 GOT에 저장된 값을 출력하면 libc 함수 주소를 유출할 수 있어요.

예를 들어 다음 호출을 만들 수 있어요.

write(1, read_got, 8);
c

각 인자는 다음과 같은 의미예요.

1
→ 표준 출력

read_got
→ 출력할 데이터가 저장된 주소

8
→ 출력할 바이트 수
text

ROP Chain에서는 각 인자를 Gadget으로 설정한 뒤 write@plt를 호출해요.

RDI = 1
RSI = read@got
RDX = 8
→ write@plt 호출
text

출력된 read 함수의 실제 주소에서 libc 내부의 read 오프셋을 빼면 libc의 시작 주소를 계산할 수 있어요.

libc_base = read_address - libc.symbols["read"]
py

그다음 system 함수의 오프셋을 더하면 실제 system 주소를 계산할 수 있어요.

system_address = libc_base + libc.symbols["system"]
py

전체 흐름은 다음과 같아요.

read@got 출력
→ libc의 read 주소 유출
→ read 오프셋 빼기
→ libc base 계산
→ system 오프셋 더하기
→ system 주소 계산
text

RELRO#

RELRO는 ELF 바이너리의 재배치 정보와 GOT 영역을 보호하는 기법이에요.

일반적으로 다음 세 가지 상태로 구분해요.

No RELRO
Partial RELRO
Full RELRO
text

No RELRO에서는 GOT와 재배치 관련 영역에 대한 보호가 거의 적용되지 않아요.

Partial RELRO에서는 일부 재배치 관련 영역을 읽기 전용으로 변경하지만, Lazy Binding에 사용되는 함수 GOT 엔트리는 실행 중에도 쓰기 가능한 경우가 많아요.

Partial RELRO
→ 일부 영역 보호
→ 함수 GOT 엔트리는 쓰기 가능할 수 있음
text

Full RELRO에서는 프로그램이 시작될 때 함수 주소를 미리 연결하고 GOT 전체를 읽기 전용으로 변경해요.

프로그램 시작
→ 함수 주소 미리 연결
→ GOT를 읽기 전용으로 변경
text

따라서 일반적인 GOT Overwrite는 No RELRO나 Partial RELRO 환경에서 사용할 수 있고, Full RELRO가 적용된 환경에서는 사용하기 어려워요.

No RELRO
→ GOT Overwrite 가능할 수 있음

Partial RELRO
→ GOT Overwrite 가능할 수 있음

Full RELRO
→ GOT 쓰기 차단
text

GOT Overwrite#

GOT에는 동적으로 연결된 함수의 실제 주소가 저장돼요.

GOT 영역에 쓰기 권한이 있다면 저장된 함수 주소를 다른 함수의 주소로 변경할 수 있어요.

이러한 공격 기법을 GOT Overwrite라고 해요.

예를 들어 원래 read@got에는 libc의 read 함수 주소가 저장되어 있어요.

read@got
→ libc의 read 주소
text

이 값을 system 함수의 주소로 덮으면 다음과 같이 변경돼요.

read@got
→ libc의 system 주소
text

이후 read@plt를 호출하면 PLT는 평소처럼 read@got에 저장된 주소로 이동해요.

하지만 read@got에는 이미 system 주소가 저장되어 있으므로 실제로는 read가 아니라 system이 실행돼요.

read@plt 호출
→ read@got 확인
→ system 주소로 이동
→ system 실행
text

GOT 엔트리를 덮기 위해서는 먼저 쓰기 함수로 GOT 영역에 새로운 주소를 입력해야 해요.

다음과 같은 호출을 ROP Chain으로 구성할 수 있어요.

read(0, read_got, 8);
c

이 호출은 표준 입력에서 8바이트를 읽어 read@got에 저장해요.

RDI = 0
RSI = read@got
RDX = 8
→ read@plt 호출
text

입력값으로 system 함수의 주소를 전달하면 read@got의 값이 변경돼요.

변경 전
read@got → read

변경 후
read@got → system
text

system을 호출하려면 명령어로 사용할 문자열도 쓰기 가능한 메모리에 저장해야 해요.

예를 들어 다음과 같이 저장할 수 있어요.

read@got
→ system 주소

read@got + 8
→ "/bin/sh\x00"
text

이후 RDI를 문자열의 주소로 설정하고 read@plt를 호출해요.

RDI = read@got + 8
→ read@plt 호출
text

read@pltread@got에 저장된 system 주소로 이동하므로 실제 호출은 다음과 같아져요.

system("/bin/sh");
c

전체적인 흐름은 다음과 같아요.

GOT에서 libc 함수 주소 유출
→ libc base 계산
→ system 주소 계산
→ read@got을 system 주소로 변경
→ 쓰기 가능한 영역에 "/bin/sh" 저장
→ RDI에 "/bin/sh" 주소 설정
→ read@plt 호출
→ system("/bin/sh") 실행
text
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg4c/dn4BitGH1/xNjKoKEp97I2b
eU57QXvkDBEdNNrEMAAAATYmxvZy5pbW55YS5uZy9wb3N0cwAAAAAAAAAGc2hhNTEyAAAA
UwAAAAtzc2gtZWQyNTUxOQAAAEBIiqF4vmDN2kchgBMZrYZK9Ua9rVA5w2cb9fXBfKuiAT
9fAu89fMMP7cWBCS3AGcHYuXaZr374rnXML7WgaVUJ
-----END SSH SIGNATURE-----
[Layer7] 2026년 7월 29일 시스템 해킹 4차시 과제
http://blog.imnya.ng/layer7/16
저자 imnyang
게시일 2026년 07월 31일