Lazy Binding

리눅스 ELF 바이너리에서 라이브러리 함수의 시작주소를 구하지 않다가, 함수를 처음 호출할 때 해당 주소를 구하는 것

이후 호출에는 함수의 실제 주소를 구하지 않고 바로바로 이동한다.

* plt, got 에 대한 개념을 알아야 함

 

ex) puts() 함수를 호출하는 경우

_dl_runtime_resolve () 를 호출하기 전

맨 처음에 puts() 를 호출하면, puts@plt 영역으로 이동한다. 그리고 got 영역 (0x804a00c) 으로 이동(1번)하나, 첫 호출이기 때문에 plt+6 의 주소가 저장되어 있다. (2번) 따라서 plt + 6 에서 값을 스택에 넣고 점프(3번)하는데, 두 영역의 주소를 보면 가깝게 위치한다. 그 이유는 Lazy Binding 을 위해 push ; jmp 명령어를 놓기 위한 여분의 자리가 있는 상태에서 그 자리에 여분의 코드가 들어갔기 때문이다. 결국 컴파일 방식에 따라 없을 수도 있다.

아무튼 중요한건 노란색 박스에 보여진 값인데, 0x0은 reloc_offset 이고 0x804a004 는 link_map 구조체의 포인터로 자세한 설명은 아래를 참고하고, 두 값은 모두 _dl_runtime_resolve() 함수의 인자이다.

 

실제 GOT 영역에 주소가 저장되는 과정

>> 함수 호출 순서

// 깊이 들어갈수록 나중에 호출되는 것

_dl_runtime_resolve(int reloc_offset, struct link_map *l) ;

	_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) ;

		_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
                     const ElfW(Sym) **ref,
                     struct r_scope_elem *symbol_scope[],
                     const struct r_found_version *version,
                     int type_class, int flags, struct link_map *skip_map) ;

			do_lookup_x (const char *undef_name, uint_fast32_t new_hash,
                            unsigned long int *old_hash, const ElfW(Sym) *ref,
                            struct sym_val *result, struct r_scope_elem *scope, size_t i,
                            const struct r_found_version *const version, int flags,
                            struct link_map *skip, int type_class, struct link_map *undef_map)


>> reloc_offset

: 아래와 같은 구조체의 시작주소를 얻기 위해 이용된다.

 (32-bit) JMPREL(.rel.plt) 영역에서 해당 함수의 Elf32_Rel 구조체의 오프셋 (주소 오프셋)

 (64-bit) RELA(.rela.plt) 영역에서 해당 함수의 Elf32_Rela 구조체의 오프셋 (구조체 배열의 인덱스)

구조체의 크기가 다르기 때문에, 32비트인 경우 주소의 오프셋을 사용하나, 64비트인 경우 배열의 인덱스를 사용한다.

 

>> link_map 구조체

 : 링커가 런타임에서 라이브러리 함수들을 메모리에 매핑시킬 때 사용하는 구조체

struct link_map
{
	ElfW(Addr) l_addr;		  /* Difference between the address in the ELF
    						file and the addresses in memory.  */
	char *l_name;			  /* Absolute file name object was found in.  */
	ElfW(Dyn) *l_ld;		  /* Dynamic section of the shared object.  */
	struct link_map *l_next, *l_prev; /* Chain of loaded objects. link_map 앞,뒤 노드 */
};

- link_map 은 이중 연결 리스트 구조이며, GOT 영역에서 2번째 원소에 첫번째 노드의 주소가 있다.
- 의미는 각 주석 참조
- l_name 은 문자열 포인터인데 STRTAB(.dynstr) 에 있는 라이브러리의 전체 경로 문자열을 참조한다.
- 찾고자 하는 함수의 메모리 상의 실제 주소를 구할 때 사용 (라이브러리의 시작주소부터 알아야 하니, 필요하다.)

 

GOT 영역과 link_map 구조체

0x804a000 은 GOT 영역이며, 다음과 같은 정보를 가진다.

GOT[0] = .dynsym 시작주소

GOT[1] = link_map 연결 리스트의 첫번째 노드의 시작주소

GOT[2] = _dl_runtime_resolve() 의 시작주소

GOT[3] = PLT 에서 참조하는 GOT 영역 (0x804a000 + 0x10 이후)

아래에 있는 link_map 이 이중 연결 리스트 구조인 것을 알 수 있다. 그림에서 마지막 노드의 경우 l_name은 "/lib32/libc.so.6"이다.

 

>> Elf32_Rel 그리고 Elf64_Rela 구조체

 : 함수의 재배치 정보를 가지며, _dl_runtime_resolve() 에서 참조된다.

typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
 
/* Relocation table entry without addend (in section of type SHT_REL).  */
typedef struct
{
	Elf32_Addr        r_offset;	/* Address */
	Elf32_Word        r_info;	/* Relocation type and symbol index */
} Elf32_Rel;

- r_offset : 재배치된 후 (함수 바인딩이 끝나고) 실제 함수 주소가 저장될 GOT 영역의 주소
- r_info : 첫 1바이트는 재배치 타입(ELF32_R_TYPE), 나머지 3바이트는 심볼 테이블 정보(ELF32_R_SYM)

#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)

 

- 32비트일 때 구조체는 크기가 8바이트이며, 64비트인 경우 크기는 24바이트이다.

typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;
typedef int64_t  Elf64_Sxword;
 
typedef struct
{
	Elf64_Addr	r_offset;	/* Address */
	Elf64_Xword	r_info;		/* Relocation type and symbol index */
	Elf64_Sxword	r_addend;	/* Addend */
} Elf64_Rela;

 

>> Elf32_Sym 그리고 Elf64_Sym 구조체

 : 함수의 심볼테이블 엔트리에 해당된다. _dl_fixup() 에서 참조된다.
  (아래 방식으로 주소를 구함, Elf32_Sym 구조체는 크기가 16바이트)

SYMTAB(.dynsym) 시작주소 + r_info->ELF32_R_SYM * 16 = 해당 함수의 Elf32_Sym 구조체 시작주소

typedef uint16_t Elf32_Section;
 
/* Symbol table entry.  */
typedef struct
{
	Elf32_Word	st_name;	/* Symbol name (string tbl index) */
	Elf32_Addr	st_value;	/* Symbol value */
	Elf32_Word	st_size;	/* Symbol size */
	unsigned char	st_info;	/* Symbol type and binding */
	unsigned char	st_other;	/* Symbol visibility */
	Elf32_Section	st_shndx;	/* Section index */
} Elf32_Sym;

STRTAB(.dynstr) 시작주소 + Elf32_Sym->st_name = 함수 이름이 저장된 주소

 

- 64비트인 경우 

typedef uint32_t Elf64_Word;
typedef uint16_t Elf64_Section;
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Xword;
 
 
typedef struct
{
	Elf64_Word	st_name;	/* Symbol name (string tbl index), 4 Byte */
	unsigned char	st_info;	/* Symbol type and binding, 1 Byte */
	unsigned char	st_other;	/* Symbol visibility, 1 Byte */
	Elf64_Section	st_shndx;	/* Section index, 2 Byte */
	Elf64_Addr	st_value;	/* Symbol value, 8 Byte */
	Elf64_Xword	st_size;	/* Symbol size, 8 Byte */
} Elf64_Sym;

 

>> 이후 라이브러리 주소를 구하는 과정

  1. 함수이름의 시작 주소를 인자로 _dl_lookup_symbol_x() 함수를 호출
  2. 함수 이름을 해쉬값으로 바꿔서 do_lookup_x() 함수에서 검사
  3. 실제 라이브러리 영역에서의 심볼 테이블 인덱스 구함 (do_lookup_x 함수에서 반환됨)
  4. 인덱스를 가지고 라이브러리 상에서의 오프셋을 구함 (do_lookup_x 함수에서 반환됨)
  5. 로드된 라이브러리 파일에서 해당 함수의 Elf32_Sym 구조체 주소에 접근

    "libc의 .dynsym 영역" + "symidx offset" = libc에서 찾고자 하는 함수의 Elf32_Sym 구조체 영역
  6. Elf32_Sym->st_value 는 실제 함수의 상대주소를 가지고 있음

    즉, libc base address + Elf32_Sym->st_value = 실제 주소


    끝으로, Elf32_Rel->r_offset 에 구한 실제 주소를 저장

 

Reference

https://www.lazenca.net/display/TEC/01.Return-to-dl-resolve+-+x86

 

01.Return-to-dl-resolve - x86 - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List Return-to-dl-resolve - x86 Return-to-dl-resolve란 프로그램에서 동적라이브러리 함수의 주소를 찾기 위해 Lazy binding 을 사용할 경우 활용이 가능한 기법입니다.Return-to-dl-resolve는 Lazy binding 을 악용해 필요한 함수를 호출합니다. Lazy binding Flow Lazy bin

www.lazenca.net

https://www.lazenca.net/pages/viewpage.action?pageId=19300744

 

02.Return-to-dl-resolve - x64(feat.Return-to-csu) - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List Return-to-dl-resolve - x64 Return-to-dl-resolve란 프로그램에서 동적라이브러리 함수의 주소를 찾기 위해 Lazy binding 을 사용할 경우 활용이 가능한 기법입니다.Return-to-dl-resolve는 Lazy binding 을 악용해 필요한 함수를 호출합니다. Lazy binding Source code -

www.lazenca.net

 

'Security > System' 카테고리의 다른 글

CTF Summary  (0) 2020.02.07
[Heap Overflow] House Of Orange  (0) 2019.07.06
linux system call table  (0) 2019.07.03
ROP Gadget Dictionary  (0) 2019.06.30
BROP (Blind Return Oriented Programming)  (0) 2019.06.26

 

출처

 

'Security > System' 카테고리의 다른 글

CTF Summary  (0) 2020.02.07
[Heap Overflow] House Of Orange  (0) 2019.07.06
linux system call table  (0) 2019.07.03
[Linux] Lazy Binding  (0) 2019.06.30
BROP (Blind Return Oriented Programming)  (0) 2019.06.26

 

 

1. 개념

BROP: 소스코드 및 바이너리가 주어지지 않은 상태에서 프로세스의 상태 또는 출력내용으로 공격을 수행하는 기법

= ROP Chain + Brute Force

무작위 대입 과정이 있어서 시간이 오래걸림..

이 기법을 이해하기 앞서 다음 두 가지 가젯에 대해 이해해야 함

STOP Gadget : main() 함수 또는 취약한 함수를 다시 실행하는 주소 (_start 주소일 수도 있고 정말 함수의 시작주소 일 수도 있으나 결과적으로 복귀주소를 이 가젯으로 변경하면 프로세스는 처음 실행했을 때의 출력값을 보인다.)

BROP Gadget : 보통 libc_csu_init 함수에 있는 pop 6개 + ret 가젯을 의미하는데, 이처럼 레지스터를 많이 쓸 수 있는 희소한 가젯을 일컫는다. 이 가젯을 찾는 이유는 주변에 0x9 그리고 0x7 오프셋으로 pop rdi ; ret 가젯과 pop rsi ; ret 가젯을 찾을 수 있기 때문이다. 두 가젯은 모두 system() 함수에서 첫번째 인자로 /bin/sh 를 넣기 위해 사용된다.

[ 도식화 ]

 

2. 공격 순서

정말 아~~무것도 주어지지 않았기 때문에 메모리 릭부터 가젯 구성에 필요한 주소까지 전부 알아내야 한다.

단계를 나름 나눠봤는데 9단계나 된다;; (무엇?)

 

(1) 스택 오버플로우 크기 확인

복귀주소를 변경하기 때문에 당연히 스택에서 오버플로가 날 것을 예상할 수 있다. 간혹 CTF 에서 이 크기를 알려준다고 한다.

파이썬 스크립트로 brute forcing 해서 프로세스가 죽거나 기존의 출력결과와 다른 출력값을 낸다면 스택 오버플로를 의심할 수 있다.

 

(2) STOP Gadget 찾기

오버플로우 크기를 알고 있기 때문에 "A" * SIZE + CodeAddress 를 페이로드로 공격했을 때, 처음 출력값이 나오면 CodeAddress 가 STOP Gadget 이 된다.

** Code 영역을 알아야 하니 checksec 같은 툴을 써서 No PIE 인지 확인하면 될 것

** 코드 세그먼트 크기가 0x1000 이므로, 보통 오프셋은 0 ~ 0x1000 이다. 

당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림

 

(3) BROP Gadget 찾기

pop 6개 + ret 가젯을 찾는 과정이기 때문에 페이로드는 "A" * SIZE + CodeAddress + PARAM * 6 + STOP_GADGET 이 된다. PARAM * 6 은 더미로 채우는데 32비트면 4바이트 * 6 = 24 바이트 일 것이고 64비트면 8바이트 * 6 = 48바이트 가 된다.

공격했을 때 처음 출력내용이 그대로 나오면 CodeAddress 는 BROP Gadget 이 된다.

(pop 6개 + ret 이 성공했으니 처음 출력내용이 나오는거니까)

** pwntools 에 p32() 랑 p64() 쓰면 편하게 계산가능하다.

이 가젯을 찾는 이유는 근처에 pop rdi ; ret (offset: 0x9) 과 pop rsi ; ret (0x7) 가젯이 있어서라 했다.

따라서 여기서 얻은 CodeAddress + 0x9 또는 CodeAddress + 0x7 주소를 실제로 사용한다.

당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림

 

(4) Printable Function 주소 찾기

출력함수는 C 언어에서 printf 와 puts 함수가 있는데 둘의 차이는 출력했을 때 개행 문자 "\n"가 출력되는지이다.

printf 일 경우 위에서 설명한 가젯 2개를 모두 사용 --> pop rdi ; ret (BROP Gadget +0x9) 과 pop rsi ; ret (+0x7)

puts 일 경우 하나만 사용 --> pop rdi ; ret (BROP Gadget +0x9)

보통 puts 함수를 쓸거라 생각하고 brute forcing 을 한다.

페이로드는 "A" * SIZE + POP_RDI_GADGET + CodeBase + CodeAddress 이다.

CodeBase 는 코드 영역의 시작주소이고, CodeAddress는 Base 부터 오프셋 범위(0 ~ 0x1000) 내로 1씩 증가시킨다. 위 페이로드로 공격했을 때 출력결과가 "\x7fELF" 문자열을 포함한다면 CodeAddress 는 puts@plt 가 된다.

** 리눅스 실행파일은 ELF 구조라 무조건 코드영역의 첫 4바이트는 "\x7fELF"이다. 그래서 CodeBase를 인자로 주고 puts 인지 아닌지 알아낸다.

당연히 Code Base Address 부터 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림

 

(5) 메모리 덤프

자 이제 puts@plt 를 알아냈으니 출력함수를 사용할 수 있다. 인자로 메모리 영역만 넣어주면 바이너리 정보를 제대로 얻어낼 수 있는 거다.

코드 영역만 필요하니 CodeBase ~ CodeBase + 0x1000 범위를 출력해서 덤프 파일을 만든다.

페이로드는 "A" * SIZE + POP_RDI_GADGET + CodeAddress + PUTS_PLT 가 된다.

** 계속 출력하다보면 프로세스가 처음 출력하는 문자열이 있는데 그 부분 전까지만 포함해야 한다.

보통 파이썬 코드로 data = response[:response.index(처음출력결과)] 라고 작성한다. 문자열의 index 함수는 발견못하면 -1 이라 끝까지 넣기 때문이다. 아무튼 저런식으로 data 를 계속 쌓아서 한번에 파일 출력을 해준다.

** 덤프 파일을 생성하면 리버싱 툴써서 분석한다. radare 를 많이 쓴다.

당연히 CodeAddress 는 오프셋 1씩 늘려주면서 찾아야 한다. 역시 brute forcing 이므로 시간이 걸림

 

(6) Printable Function 의 GOT 주소 찾기

radare 로 덤프한 메모리를 다음과 같은 명령어로 분석을 진행한다.

$ r2 -B <CodeBase> <DumpFileName>

>> pd 10 @ <puts@plt>

검색하면 plt 영역의 코드가 나오는데, 세그먼트 내에 오프셋으로 jmp 할 주소를 계산하기 때문에 실제 GOT 주소를 찾을 수 있다.

 

(7) 메모리 릭 (leak libc address)

puts@got.plt 주소를 인자로 puts@plt 를 호출한다. 이는 libc 영역의 주소를 릭해서 오프셋 계산을 하기 위함이다.

페이로드는 "A" * SIZE + POP_RDI_GADGET + PUTS_GOT + PUTS_PLT 이다.

출력값은 당연히 puts 의 시작주소일 것이다. 이 주소의 끝 3자리 숫자를 가지고 libc 를 탐색한다.

ASLR 걸려있으면 여기서부터는 아래 단계들이 원자적으로 수행되어야 한다.

 

(8) libc 탐색 후 오프셋 찾기

필요한 도구는 libc-databaseLibcSearcher 이다. 사용하기전에 어떤 라이브러리인지 파악해야 한다.

둘다 설치해서 사용법대로 system() 함수와 /bin/sh 문자열의 오프셋을 알아낼 수 있다.

릭한 주소 그대로 써도 되긴 하는데, puts 와 system 그리고 /bin/sh 의 오프셋을 알아낸 다음 익스를 짜는게 덜 귀찮다. (적어도 나는 그랬다.)

보통 LibcSearcher 모듈을 쓸 때 다음과 같이 쓴다.

from LibcSearcher import *

addr_puts_libc = 0x6f690	# 라이브러리에 따라 오프셋이 바뀔 수 있음.

lib = LibcSearcher('puts', addr_puts_libc)
libcBase = addr_puts_libc - lib.dump('puts')
system_addr = libcBase + lib.dump('system')
binsh_addr = libcBase + lib.dump('str_bin_sh')

print 'libc base : ' + hex(libcBase)
print 'system : ' + hex(system_addr)
print 'binsh : ' + hex(binsh_addr)

 

(9) 최종 익스플로잇

여기는 그냥 ROP 기법대로 하면 된다. 위 과정에서 알아낸 오프셋을 기반으로 libc base 에 더해서 실제 주소를 계산해서 페이로드를 짜면 된다.

페이로드는 "A" * SIZE + POP_RDI_GADGET + STR_BINSH_ADDR + SYSTEM_ADDR 이다.

 

 

Reference

https://www.lazenca.net/pages/viewpage.action?pageId=16810286

 

08.BROP(Blind Return Oriented Programming) - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List BROP(Blind Return Oriented Programming) 소프트웨어를 해킹할 때 대략 3가지의 형태의 공격대상이 있습니다.Open-source (예 :  Apache)Open-binary (예 : Internet Explorer)Closed-binary and source (예 :일부 독점 네트워크 서비스)BROP는 Closed-bin

www.lazenca.net

https://blog.h3x0r.kr/2018-12-30-brop/

 

BROP (Blind ROP)

BROP라는 주제로 글을 쓰게된 ipwn(안건희)입니다. 아직 완벽하게 연구했다고는 생각하지 않지만 지금까지의 연구과정을 예제를 통해서 포스팅하려 합니다. 목차 들어가면서 … 필요한 것 ? stop_gadget ? brop_gadget ? exploit 절차 Example binary Server setting Check overflow Get stop_gadget Get brop_gadget Get printable function Dump memory

blog.h3x0r.kr

 

위 자료는 Write-Up 이 있어서 참고하면 된다. 사용한 문제는 HCTF 2018 과 CodeGate 2018 예선문제이다.

(다른 문제 찾으면 추가예정)

 

 

'Security > System' 카테고리의 다른 글

CTF Summary  (0) 2020.02.07
[Heap Overflow] House Of Orange  (0) 2019.07.06
linux system call table  (0) 2019.07.03
[Linux] Lazy Binding  (0) 2019.06.30
ROP Gadget Dictionary  (0) 2019.06.30

+ Recent posts