how got filling in runtime

GLOBAL_OFFSET_TALBE 이하 got가 libc 상에 함수 주소를 가리키고 있는 것은 모두 알고 있을 것이다.

그렇다면 이 실제 함수 주소를 어떤 방식으로 가져 올까? 이 과정에 대해서 자세히 알아보자

https://rond-o.tistory.com/216

다음 사이트를 참조했다.

C
pwndbg> disass 0x400770
Dump of assembler code for function read@plt:
   0x0000000000400770 <+0>:	jmp    *0x2018ca(%rip)        # 0x602040 <read@got.plt>
   0x0000000000400776 <+6>:	push   $0x5
   0x000000000040077b <+11>:	jmp    0x400710

일단 시작은 plt함수로 부터 시작된다. 해당 함수는 명령어 3개로 되어 있는데 맨처음

jmp *0x2018ca(%rip) 에서 해당 함수의 got에 있는 값으로 jmp명령어를 수행한다. lazy binding을 하고 있으니깐 만약 이 함수가 이미 한번 실행 되었다면, got에는 libc 상에서 read 함수의 위치를 가리킬 것이다. 만약 이 함수가 전에 한번도 실행되지 않았자면, 이제 lazy binding이 시작할 차례다

lazy binding

그럼 이제 이 과정을 알아보자

C
pwndbg> disass 0x400770
Dump of assembler code for function read@plt:
   0x0000000000400770 <+0>:	jmp    *0x2018ca(%rip)        # 0x602040 <read@got.plt>
   0x0000000000400776 <+6>:	push   $0x5
   0x000000000040077b <+11>:	jmp    0x400710
End of assembler dump.
pwndbg> x/gx 0x602040
0x602040 <read@got.plt>:	0x0000000000400776
pwndbg> 

현재 read 함수가 한번도 실행된적이 없으니깐 read@got.plt 에는 0x0000000000400776 즉,

read@plt 상에서 다음 명령어를 가리키고 있다.

C
   0x0000000000400776 <+6>:	push   $0x5
   0x000000000040077b <+11>:	jmp    0x400710

그러면서 다음 명령어를 실행하는데

push $0x5 에서 0x5 값은 read 함수의 got 상에서 오프셋이다.

좀더 자세히 설명하자면,

C
(pwn) amemoyoi ~$ objdump -T ./Notepad

./Notepad:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) remove
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) puts
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) fclose
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) system
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) strchr
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) read
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) __libc_start_main
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) ftell
0000000000000000  w   D  *UND*	0000000000000000              __gmon_start__
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) fseek
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) setvbuf
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) fopen
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) sprintf
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) strstr
00000000006020a0 g    DO .bss	0000000000000008 (GLIBC_2.2.5) stdout
00000000006020b0 g    DO .bss	0000000000000008 (GLIBC_2.2.5) stdin
00000000006020c0 g    DO .bss	0000000000000008 (GLIBC_2.2.5) stderr

.got.plt 섹션 상에서 read가 여섯번째에 위치하기 때문에 table[5]와 같은 index 느낌으로 5가 push 된다.

(나중에 나오지만 이 index를 정식명칭으로 reloc_offset 라고 한다. )

그럼 이제 jmp 0x400710 를 해보면

C
pwndbg> x/5i 0x400710
   0x400710:	push   0x2018f2(%rip)        # 0x602008
   0x400716:	jmp    *0x2018f4(%rip)        # 0x602010
   0x40071c:	nopl   0x0(%rax)
   0x400720 <remove@plt>:	jmp    *0x2018f2(%rip)        # 0x602018 <remove@got.plt>
   0x400726 <remove@plt+6>:	push   $0x0

다음과 같은 명령어를 거치게 된다. 먼저 0x602008 값을 push 하게 되는데 이값은

C
pwndbg> info symbol 0x602008
_GLOBAL_OFFSET_TABLE_ + 8 in section .got.plt of /home/amemoyoi/1day_2pwn/Notepad

.got.plt 의 _GLOBAL_OFFSET_TABLE_ + 8 이라고 한다.

해당 글을 참조해서 가져오면

C
// 이하 0x10은 .got section
0x600ff0: 0x00007ffff7a03b10  // & __libc_start_main()
0x600ff8: 0x0000000000000000
// 이하 0x38은 .got.plt section
0x601000: 0x0000000000600e20  // & .dynamic section
0x601008: 0x00007ffff7ffe170  // & link_map
0x601010: 0x00007ffff7dea8f0  // & _dl_runtime_resolve_xsavec()
0x601018: ??????????????????  // 우리가 아는 got 시작
0x601020: ??????????????????
0x601028: ??????????????????
0x601030: ??????????????????

_GLOBAL_OFFSET_TABLE_ + 8 는 link_map 구조체의 주소를 의미한다.

해당 구조체는

C
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;   /* Chain of loaded objects.                  */
    struct link_map *l_prev;    
};

https://elixir.bootlin.com/glibc/glibc-2.27/source/elf/link.h#L84

다음과 같다. 다시 코드로 돌아가서

C
pwndbg> x/5i 0x400710
   0x400710:	push   0x2018f2(%rip)        # 0x602008
   0x400716:	jmp    *0x2018f4(%rip)        # 0x602010
   0x40071c:	nopl   0x0(%rax)
   0x400720 <remove@plt>:	jmp    *0x2018f2(%rip)        # 0x602018 <remove@got.plt>
   0x400726 <remove@plt+6>:	push   $0x0

link_map 구조체의 주소를 push 한 다음에 0x602010에 있는 값으로 점프한다.

0x602010는 GLOBAL_OFFSET_TABLE + 16 을 의미하며

_dl_runtime_resolve_xsavec 함수의 위치를 나타낸다.

따라서 _dl_runtime_resolve_xsavec 가 실행이 되며, 이 함수는 레지스터 세팅을 위한 stub 함수이다.

C
   0x00007ffff7fda361 <+113>:	mov    0x10(%rbx),%rsi
   0x00007ffff7fda365 <+117>:	mov    0x8(%rbx),%rdi
   0x00007ffff7fda369 <+121>:	call   0x7ffff7fd7af0 <_dl_fixup>

결론적으로는 다음과 같이 %rdi에 앞서 push 했던 link_map 구조체의 주소와

%rsi에 앞서 push 했던 read 함수의 got상의 index 값을 가지고 _dl_fixup 함수를 실행한다.

먼저 사진을 첨부하겠다.

_dl_fixup(&link_map, reloc_offset) 으로 함수를 실행하고

reloc_offset 은 위에서 내가 계속 말하던 index에 대한 정식 명칭이다.

_dl_fixup 함수에서는

C
007ffff7fd7bf7 <+263>:	call   0x7ffff7fd00d0 <_dl_lookup_symbol_x>

link_map 구조체에 존재하는 l_ld 필드를 참조해서 .dynamic 섹션을 알게된다. 이후

.dynamic 섹션에 있는 구조체 배열중

C
0x0000000000000005 (STRTAB)             0x400478

다음과 같은 STRTAB에 대한 정보를 가지고 있는 구조체를 참조한다.

따라서 .dynstr 섹션에 접근할 수 있게 된다.

지금 부터의 분석은 실제 binary 내에 setvbuf 함수에 대한 got 채우는 과정을 따라갈 것이다.

그리고 이제 어떤 위치에 있는 got를 수정해야 되는지 알기 위해서는 .rela.plt 에 대해서도 참조해야 된다.

해당 부분에 대한 코드는 다음과 같다.

C
0x00007ffff7fd7b42 <+82>:  lea    (%rdx,%rcx,8),%r13
0x00007ffff7fd7b46 <+86>:  add    %r9,%r13
0x00007ffff7fd7b49 <+89>:  mov    0x8(%r13),%rsi       ; r_addend
0x00007ffff7fd7b4d <+93>:  mov    0x0(%r13),%r14       ; r_offset

gdb 상황으로 보면

C
 RAX  0
 RBX  0x7ffff7ffe2e0 ◂— 0
 RCX  0x1b
 RDX  0x4005b8 —▸ 0x602018 (remove@got[plt]) —▸ 0x400726 (remove@plt+6) ◂— pushq $0 /* 'h' */
 RDI  0x400478 ◂— addb %ch, 0x62(%rcx, %rbp, 2)
*RSI  0xb00000007
 R8   0x4002c8 ◂— addb %al, (%rax)
 R9   0
 R10  0x7ffff7c140c8 ◂— 0x110012000007e6
 R11  0x7ffff7c2a200 (__libc_start_main) ◂— endbr64 
 R12  9
 R13  0x400690 —▸ 0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
 R14  0
 R15  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffd4f0 —▸ 0x7fffffffdf00 —▸ 0x7fffffffe0f0 —▸ 0x7fffffffe190 —▸ 0x7fffffffe1f0 ◂— ...
 RSP  0x7fffffffd4b0 ◂— 0x1d67a0
*RIP  0x7ffff7fd7b4d (_dl_fixup+93) ◂— movq (%r13), %r14
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────
   0x7ffff7fd7b49 <_dl_fixup+89>     movq   8(%r13), %rsi            RSI, [0x400698] => 0xb00000007
 ► 0x7ffff7fd7b4d <_dl_fixup+93>     movq   (%r13), %r14             R14, [0x400690] => 0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
   0x7ffff7fd7b51 <_dl_fixup+97>     movq   %rsi, %rdx               RDX => 0xb00000007
   0x7ffff7fd7b54 <_dl_fixup+100>    addq   %rax, %r14               R14 => 0x602060 (setvbuf@got[plt]) (0x602060 + 0x0)
   0x7ffff7fd7b57 <_dl_fixup+103>    shrq   $0x20, %rdx
   0x7ffff7fd7b5b <_dl_fixup+107>    leaq   (%rdx, %rdx), %rcx       RCX => 0x16

0x400690에 있는 값을 가져오는데 해당영역은 .rela.plt 영역이며 setvbuf 함수에 대한

Elf64_Rela 구조체의 r_offset 필드이다.

따라서 지금 어디 got에 값을 써야하는지도 알고 있고 이제 위에 .dynstr 위치도 알았으니 해당

.dynstr base로 부터 우리가 찾는 재배치 항목의 offset을 구하면 문자열을 얻을 수 있다.

.dynstr base로 부터 우리가 찾는 재배치 항목의 offset은 .dynsym 섹션에 위치해있다.

C
typedef struct
{
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf64_Section	st_shndx;		/* Section index */
  Elf64_Addr	st_value;		/* Symbol value */
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

setvbuf 함수에 대한 Elf64_Sym 구조체를 찾고 해당 구조체에 st_name 필드에 재배치 항목의 offset 값이 들어가 있다.

이 값을 찾는 과정은 다음과 같다.

C
0x00007ffff7fd7b49 <+89>:  mov    0x8(%r13),%rsi         ; r_info
...
0x00007ffff7fd7b54 <+100>: add    %rax,%r14              ; r14 = r_offset + reloc_base
0x00007ffff7fd7b57 <+103>: shr    $0x20,%rdx             ; r_info 상위 32비트 추출
...
0x00007ffff7fd7b62 <+114>: lea    (%r8,%rdx,8),%rdx      ; rdx = &.dynsym[symidx]

gdb 분석상으로 보면

C
 RAX  0
 RBX  0x7ffff7ffe2e0 ◂— 0
*RCX  0x16
*RDX  0x21
 RDI  0x400478 ◂— addb %ch, 0x62(%rcx, %rbp, 2)
 RSI  0xb00000007
 R8   0x4002c8 ◂— addb %al, (%rax)
 R9   0
 R10  0x7ffff7c140c8 ◂— 0x110012000007e6
 R11  0x7ffff7c2a200 (__libc_start_main) ◂— endbr64 
 R12  9
 R13  0x400690 —▸ 0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
*R14  0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
 R15  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffd4f0 —▸ 0x7fffffffdf00 —▸ 0x7fffffffe0f0 —▸ 0x7fffffffe190 —▸ 0x7fffffffe1f0 ◂— ...
 RSP  0x7fffffffd4b0 ◂— 0x1d67a0
*RIP  0x7ffff7fd7b62 (_dl_fixup+114) ◂— leaq (%r8, %rdx, 8), %rdx
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────
   0x7ffff7fd7b51 <_dl_fixup+97>     movq   %rsi, %rdx
   0x7ffff7fd7b54 <_dl_fixup+100>    addq   %rax, %r14
   0x7ffff7fd7b57 <_dl_fixup+103>    shrq   $0x20, %rdx
   0x7ffff7fd7b5b <_dl_fixup+107>    leaq   (%rdx, %rdx), %rcx
   0x7ffff7fd7b5f <_dl_fixup+111>    addq   %rcx, %rdx
 ► 0x7ffff7fd7b62 <_dl_fixup+114>    leaq   (%r8, %rdx, 8), %rdx     RDX => 0x4003d0 ◂— addb %al, (%rax) /* 'f' */
   0x7ffff7fd7b66 <_dl_fixup+118>    movq   %rdx, -0x40(%rbp)        [0x7fffffffd4b0] <= 0x4003d0 ◂— addb %al, (%rax) /* 'f' */
   0x7ffff7fd7b6a <_dl_fixup+122>    cmpl   $7, %esi                 7 - 7     EFLAGS => 0x246 [ cf PF af ZF sf IF df of ]
   0x7ffff7fd7b6d <_dl_fixup+125>    jne    _dl_fixup+651               <_dl_fixup+651>
 
   0x7ffff7fd7b73 <_dl_fixup+131>    testb  $3, 5(%rdx)              0 & 3     EFLAGS => 0x246 [ cf PF af ZF sf IF df of ]
   0x7ffff7fd7b77 <_dl_fixup+135>    jne    _dl_fixup+608               <_dl_fixup+608>

0x4003d0 값을 얻은 것을 알 수 있는데 이 위치가 이제 rdx = &.dynsym[symidx]

즉, 우리가 찾는 setvbuf 함수에 대한 Elf64_Sym 구조체 이다. 여기서 첫번째 필드인 st_name 값을 확인해 보면

C
pwndbg> x/4wx 0x4003d0
0x4003d0:	0x00000066	0x00000012	0x00000000	0x00000000

st_name 은 4바이트이므로 0x00000066 값을 가진다.

C
pwndbg> info files
Symbols from "/home/amemoyoi/1day_2pwn/Notepad".
Native process:
	Using the running image of child process 2262447.
	While running this, GDB does not access memory from...
Local exec file:
	`/home/amemoyoi/1day_2pwn/Notepad', file type elf64-x86-64.
	Entry point: 0x400800
	0x0000000000400238 - 0x0000000000400254 is .interp
	0x0000000000400254 - 0x0000000000400274 is .note.ABI-tag
	0x0000000000400274 - 0x0000000000400298 is .note.gnu.build-id
	0x0000000000400298 - 0x00000000004002c8 is .gnu.hash
	0x00000000004002c8 - 0x0000000000400478 is .dynsym
	0x0000000000400478 - 0x0000000000400513 is .dynstr
	0x0000000000400514 - 0x0000000000400538 is .gnu.version
	0x0000000000400538 - 0x0000000000400558 is .gnu.version_r
	0x0000000000400558 - 0x00000000004005b8 is .rela.dyn
	0x00000000004005b8 - 0x00000000004006f0 is .rela.plt

0x0000000000400478인 dynstr 에서 0x00000066 을 더한 위치에 값을 확인해보면

C
pwndbg> x/s 0x00000066 + 0x000000000400478
0x4004de:	"setvbuf"

setvbuf 문자열이 들어가 있는 것을 알 수 있다. 이제 이 문자열을 가지고 간다.

_dl_lookup_symbol_x

C
 0x00007ffff7fd01f7 <+295>:	call   0x7ffff7fcf530 <do_lookup_x>

do_lookup_x

C
   0x00007ffff7fcf777 <+583>:	call   0x7ffff7fcf390 <check_match>

이 문자열을 do_lookup_x _dl_lookup_symbol_x 이 두 함수에서 strcmp 함수로 비교하면서 libc 상에서 같은 이름의 심볼을 찾고 libc 상에서 주소를 받아온다.

check_match

C
0x00007ffff7fcf500 <+368>:	call   0x7ffff7fd1380 <_dl_name_match_p>

이 함수에 대한 자세한 정보는 모르나 do_lookup_x _dl_lookup_symbol_x 와 같은 용도로 사용된다.

위와 같은 과정을 거친다. 이후에 함수의 주소를 얻으면,

실제로 got에 저장이 되는 것은 _dl_fixup 함수 내부에서 _dl_lookup_symbol_x 함수 호출 이후에

C
 0x00007ffff7fd7c7d <+397>:	mov    %rax,(%r14)

이 부분이다. 이때 gdb를 보면

C
*RAX  0x7ffff7c88550 (setvbuf) ◂— endbr64 
*RBX  0x7ffff7ffe2e0 ◂— 0
*RCX  0x4003d0 ◂— addb %al, (%rax) /* 'f' */
*RDX  0
*RDI  0x7ffff7c0a0c0 ◂— 0x11002200003bac
*RSI  1
*R8   1
*R9   0
*R10  0x7ffff7c0a0c0 ◂— 0x11002200003bac
*R11  3
*R12  9
*R13  0x400690 —▸ 0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
*R14  0x602060 (setvbuf@got[plt]) —▸ 0x4007b6 (setvbuf@plt+6) ◂— pushq $9 /* 'h\t' */
*R15  0x7ffff7fbd160 —▸ 0x7ffff7c00000 ◂— 0x3010102464c457f
*RBP  0x7fffffffd4f0 —▸ 0x7fffffffdf00 —▸ 0x7fffffffe0f0 —▸ 0x7fffffffe190 —▸ 0x7fffffffe1f0 ◂— ...
*RSP  0x7fffffffd4b0 —▸ 0x7ffff7c0a0c0 ◂— 0x11002200003bac
*RIP  0x7ffff7fd7c7d (_dl_fixup+397) ◂— movq %rax, (%r14)
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x7ffff7fd7c7d <_dl_fixup+397>                     movq   %rax, (%r14)          [setvbuf@got[plt]] <= 0x7ffff7c88550 (setvbuf) ◂— endbr64 
   0x7ffff7fd7c80 <_dl_fixup+400>                     leaq   -0x28(%rbp), %rsp     RSP => 0x7fffffffd4c8 —▸ 0x7fffffffdee0 —▸ 0x7fffffffe218 ◂— ...
   0x7ffff7fd7c84 <_dl_fixup+404>                     popq   %rbx                  RBX => 0x7fffffffdee0

setbuf got에 실제 함수 주소를 넣는 것을 알 수 있다.

그리고 _dl_runtime_resolve_xsavec 함수 내에서 _dl_fixup 실행후 반환값이 함수의 주소이고 이를

C
 0x00007ffff7fda3aa <+186>:	jmp    *%r11
C
*RAX  0x7ffff7e038e0 (_IO_2_1_stdin_) ◂— 0xfbad2088
*RBX  0x7fffffffe218 —▸ 0x7fffffffe4c8 ◂— '/home/amemoyoi/1day_2pwn/Notepad'
*RCX  0
*RDX  2
*RDI  0x7ffff7e038e0 (_IO_2_1_stdin_) ◂— 0xfbad2088
*RSI  0
*R8   0x400c80 (__libc_csu_fini) ◂— repz retq 
*R9   0x7ffff7fca380 (_dl_fini) ◂— endbr64 
 R10  0x7ffff7c0a0c0 ◂— 0x11002200003bac
*R11  0x7ffff7c88550 (setvbuf) ◂— endbr64 
 R12  1
 R13  0
 R14  0
 R15  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffdf00 —▸ 0x7fffffffe0f0 —▸ 0x7fffffffe190 —▸ 0x7fffffffe1f0 ◂— 0
*RSP  0x7fffffffdef8 —▸ 0x400918 (Initalize+34) ◂— movq 0x201781(%rip), %rax
*RIP  0x7ffff7fda3aa (_dl_runtime_resolve_xsavec+186) ◂— jmpq *%r11
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x7ffff7fda3aa <_dl_runtime_resolve_xsavec+186>    jmpq   *%r11                       <setvbuf>
    ↓

R11 레지스터에 넣은뒤 jmp로 그냥 바로 실행시키는 것을 볼 수 있다.