최근에 꽤나 재미있는 기법을 알게되어 그 기록을 남긴다.
느낀점은 결국 ret address 변조가 가능하면 니 컴퓨터 = 내 컴퓨터
1. 서론
만일 rop 시 적절한 gadget이 구해지지 않는 경우에는 어떻게 하면 좋을까?
예를 들면 read 함수에서 rsi 값은 입력 받는 크기를 가지고 있는데, 이 값이 항상 0이고 pop rsi gadget이 없다면 어떻게 해야할까.
이런 경우에 return to csu 기법을 사용할 수 있다.
2. 원리
2.1. 기본 원리
사실 프로그램을 실행하면 main 함수부터 실행되는 것 같지만, 그 전에 실행되는 함수들이 있다.
이는 프로그램 실행 전, 실행을 위한 일련의 동작인데 예를 들자면 코드를 메모리에 넘겨주거나 인자를 넘겨주거나 하는 역할을 한다.
이에 대부분의 프로그램 (전부 다는 아니다)은 기본적으로
- _start
- __libc_start_main
- __libc_csu_init
- __ main
- _fini
의 흐름으로 실행된다.
즉, __libc_csu_init 함수는 거의 무조건 포함된다는 것이다.
문제는 이 함수 내에서 레지스터를 변조할 수 있는 어셈블러 코드가 있고, 이를 활용해 특정 함수 실행이 가능해진다.
2.2. 상세 구조
해당 함수의 코드는 아래와 같다.
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
#ifndef NO_INITFINI
_init ();
#endif
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
사실 이건 그냥 보여주기 위한 것이고, 실질적으로 중요한 부분은 아래이다.
아무 파일이나 gdb로 열어 function을 보았다.
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000400548 _init
0x0000000000400570 puts@plt
0x0000000000400580 __stack_chk_fail@plt
0x0000000000400590 printf@plt
0x00000000004005a0 read@plt
0x00000000004005b0 setvbuf@plt
0x00000000004005c0 _start
0x00000000004005f0 _dl_relocate_static_pie
0x0000000000400600 deregister_tm_clones
0x0000000000400630 register_tm_clones
0x0000000000400670 __do_global_dtors_aux
0x00000000004006a0 frame_dummy
0x00000000004006a7 main
0x0000000000400790 __libc_csu_init
0x0000000000400800 __libc_csu_fini
0x0000000000400804 _fini
이와 같이 __libc_csu_init 함수가 포함된 것을 볼 수 있다.
해당 위치의 어셈블러 코드를 보면 아래와 같다.
gef➤ x/40i 0x0000000000400790
0x400790 <__libc_csu_init>: push %r15
0x400792 <__libc_csu_init+2>: push %r14
0x400794 <__libc_csu_init+4>: mov %rdx,%r15
0x400797 <__libc_csu_init+7>: push %r13
0x400799 <__libc_csu_init+9>: push %r12
0x40079b <__libc_csu_init+11>: lea 0x20066e(%rip),%r12 # 0x600e10
0x4007a2 <__libc_csu_init+18>: push %rbp
0x4007a3 <__libc_csu_init+19>: lea 0x20066e(%rip),%rbp # 0x600e18
0x4007aa <__libc_csu_init+26>: push %rbx
0x4007ab <__libc_csu_init+27>: mov %edi,%r13d
0x4007ae <__libc_csu_init+30>: mov %rsi,%r14
0x4007b1 <__libc_csu_init+33>: sub %r12,%rbp
0x4007b4 <__libc_csu_init+36>: sub $0x8,%rsp
0x4007b8 <__libc_csu_init+40>: sar $0x3,%rbp
0x4007bc <__libc_csu_init+44>: call 0x400548 <_init>
0x4007c1 <__libc_csu_init+49>: test %rbp,%rbp
0x4007c4 <__libc_csu_init+52>: je 0x4007e6 <__libc_csu_init+86>
0x4007c6 <__libc_csu_init+54>: xor %ebx,%ebx
0x4007c8 <__libc_csu_init+56>: nopl 0x0(%rax,%rax,1)
0x4007d0 <__libc_csu_init+64>: mov %r15,%rdx
0x4007d3 <__libc_csu_init+67>: mov %r14,%rsi
0x4007d6 <__libc_csu_init+70>: mov %r13d,%edi
0x4007d9 <__libc_csu_init+73>: call *(%r12,%rbx,8)
0x4007dd <__libc_csu_init+77>: add $0x1,%rbx
0x4007e1 <__libc_csu_init+81>: cmp %rbx,%rbp
0x4007e4 <__libc_csu_init+84>: jne 0x4007d0 <__libc_csu_init+64>
0x4007e6 <__libc_csu_init+86>: add $0x8,%rsp
0x4007ea <__libc_csu_init+90>: pop %rbx
0x4007eb <__libc_csu_init+91>: pop %rbp
0x4007ec <__libc_csu_init+92>: pop %r12
0x4007ee <__libc_csu_init+94>: pop %r13
0x4007f0 <__libc_csu_init+96>: pop %r14
0x4007f2 <__libc_csu_init+98>: pop %r15
0x4007f4 <__libc_csu_init+100>: ret
2.3. 분석
우리가 관심을 가져야 하는 부분은 아래 부분이고, 조금 더 세밀하게 나누면 두개의 파트로 나눌 수 있다.
...
0x4007d0 <__libc_csu_init+64>: mov %r15,%rdx
0x4007d3 <__libc_csu_init+67>: mov %r14,%rsi
0x4007d6 <__libc_csu_init+70>: mov %r13d,%edi
0x4007d9 <__libc_csu_init+73>: call *(%r12,%rbx,8)
0x4007dd <__libc_csu_init+77>: add $0x1,%rbx
0x4007e1 <__libc_csu_init+81>: cmp %rbx,%rbp
0x4007e4 <__libc_csu_init+84>: jne 0x4007d0 <__libc_csu_init+64>
-------------------------------------------------------------------------
0x4007e6 <__libc_csu_init+86>: add $0x8,%rsp
0x4007ea <__libc_csu_init+90>: pop %rbx
0x4007eb <__libc_csu_init+91>: pop %rbp
0x4007ec <__libc_csu_init+92>: pop %r12
0x4007ee <__libc_csu_init+94>: pop %r13
0x4007f0 <__libc_csu_init+96>: pop %r14
0x4007f2 <__libc_csu_init+98>: pop %r15
0x4007f4 <__libc_csu_init+100>: ret
...
위쪽 파트를 보통 stage 2, 아래쪽 파트를 stage 1이라 말한다.
2.3.1. stage 1
이 부분은 스택의 값을 레지스터로 삽입하는 역할을 한다.
0x4007e6 <__libc_csu_init+86>: add $0x8,%rsp
0x4007ea <__libc_csu_init+90>: pop %rbx
0x4007eb <__libc_csu_init+91>: pop %rbp
0x4007ec <__libc_csu_init+92>: pop %r12
0x4007ee <__libc_csu_init+94>: pop %r13
0x4007f0 <__libc_csu_init+96>: pop %r14
0x4007f2 <__libc_csu_init+98>: pop %r15
0x4007f4 <__libc_csu_init+100>: ret
만일 스택에 아래와 같이 값이 삽입되어있다고 가정하자.
낮은 주소 |
AAAAAAAA |
BBBBBBBB |
CCCCCCCC |
DDDDDDDD |
EEEEEEEE |
FFFFFFFF |
GGGGGGGG |
높은 주소 |
이는 위의 코드가 실행되면서 아래와 같이 레지스터에 값이 삽입되게 된다.
낮은 주소 | stage 1 |
XXXXXXXX | add $0x8, %rsp로 인해 무시됨. |
AAAAAAAA | rbx |
BBBBBBBB | rbp |
CCCCCCCC | r12 |
DDDDDDDD | r13 |
EEEEEEEE | r14 |
FFFFFFFF | r15 |
GGGGGGGG | ret |
높은 주소 | - |
이후 GGGGGGGG의 위치로 return 하게 되는데, 만일 GGGGGGGG의 값이 0x4007d0라면 stage 2로 진입하게 될 것이다.
2.3.2. stage 2
이 부분에서는 각 레지스터의 값을 참조로 함수를 실행하는 역할을 한다.
0x4007d0 <__libc_csu_init+64>: mov %r15,%rdx
0x4007d3 <__libc_csu_init+67>: mov %r14,%rsi
0x4007d6 <__libc_csu_init+70>: mov %r13d,%edi
0x4007d9 <__libc_csu_init+73>: call *(%r12,%rbx,8)
0x4007dd <__libc_csu_init+77>: add $0x1,%rbx
0x4007e1 <__libc_csu_init+81>: cmp %rbx,%rbp
0x4007e4 <__libc_csu_init+84>: jne 0x4007d0 <__libc_csu_init+64>
위의 값을 다시 가져오고, 0x4007d6 위치까지 실행되면 아래와 같은 모양이 될 것이다.
낮은 주소 | stage 1 | stage 2 |
XXXXXXXX | add $0x8, %rsp로 인해 무시됨. | |
AAAAAAAA | rbx | |
BBBBBBBB | rbp | |
CCCCCCCC | r12 | |
DDDDDDDD | r13 | edi (4 byte만 삽입됨) |
EEEEEEEE | r14 | rsi |
FFFFFFFF | r15 | rdx |
0x4007d0 | ret | |
높은 주소 | - |
이후 0x4007dd에서 r12 + rbx x 8에 담긴 주소를 call 하게된다.
이 때 만일 rbx인 AAAAAAAA가 0이고 r12인 CCCCCCCC가 put 함수의 got 이라면 *put_got 위치의 값을 call하게 될 것이고 put 함수가 실행될 것이다.
낮은 주소 | stage 1 | stage 2 |
XXXXXXXX | add $0x8, %rsp로 인해 무시됨. | |
0 | rbx | |
BBBBBBBB | rbp | |
puts_got | r12 | |
DDDDDDDD | r13 | edi (4 byte만 삽입됨) |
EEEEEEEE | r14 | rsi |
FFFFFFFF | r15 | rdx |
0x4007d0 | ret | |
높은 주소 | - |
더불어 함수 호출 규약에 따라 rdi, rsi, rdx 레지스터의 값은 puts 함수의 인자로 사용되며,
함수 호출 규약. — think storage (tistory.com)
이 때 만일 rdi가 read_got 이라면 이를 출력하려 할 것이다.
낮은 주소 | stage 1 | stage 2 |
XXXXXXXX | add $0x8, %rsp로 인해 무시됨. | |
0 | rbx | call *(%r12, %rbx, 8)의 연산에 사용됨. |
BBBBBBBB | rbp | |
puts_got | r12 | call *(%r12, %rbx, 8)에 의해 호출됨. |
read_got | r13 | edi (4 byte만 삽입됨) |
EEEEEEEE | r14 | rsi |
FFFFFFFF | r15 | rdx |
0x4007d0 | ret | |
높은 주소 | - |
다시 남은 어셈블러 코드를 보면 아래와 같다.
0x4007dd <__libc_csu_init+77>: add $0x1,%rbx
0x4007e1 <__libc_csu_init+81>: cmp %rbx,%rbp
0x4007e4 <__libc_csu_init+84>: jne 0x4007d0 <__libc_csu_init+64>
이제 rbx에 1을 넣고, rbp와 비교해서 다르면 0x4007d0 즉 stage2로 돌아가고 그게 아니라면 이후에 이어지는 stag1을 계속 실행할 것이다.
즉 최종적으로 아래와 같은 모양이 된다.
낮은 주소 | stage 1 | stage 2 |
XXXXXXXX | add $0x8, %rsp로 인해 무시됨. | |
0 | rbx | call *(%r12, %rbx, 8)의 연산에 사용됨. |
1 | rbp | cmp %rbx, %bp 연산에 사용됨. |
puts_got | r12 | call *(%r12, %rbx, 8)에 의해 호출됨. |
read_got | r13 | edi (4 byte만 삽입됨) |
EEEEEEEE | r14 | rsi |
FFFFFFFF | r15 | rdx |
0x4007d0 | ret | |
높은 주소 | - |
2.3.3. 전체 작동 원리
앞서 stage 1에서는 각 레지스터에 값을, stage 2에서는 그 레지스터의 값을 참조로 함수를 실행할 수 있음을 확인하였다.
즉, stage 1에서 삽입된 값을 테이블화 하면 아래와 같이 사용됨을 알 수 있다.
stage 1 | stage 2 | 삽입 되어야할 값 | 비고 |
add $0x8, %rsp | - | padding 8 byte. | dummy 값임. |
rbx | rbx | 0 | 항상 0이어야 r12 주소의 값을 실행할 수 있음. |
rbp | rbp | 1 | 항상 1이어야 chainning이 가능해짐. |
r12 | call | 실행시킬 함수 주소 | |
r13 | edi | argv0 | 4 byte만 사용할 수 있음. 유의할 것. |
r14 | rsi | argv1 | |
r15 | rdx | argv2 | |
ret | ret | return address |
2.4. 유의할 점 및 단점.
2.4.1. 유의할 점
edi 값은 4 byte만 사용 가능한 경우가 대부분이다.
그러므로 인자로 사용 가능한 값이 제한된다.
이를 우회하기 위해 첫 rtc 시 read 함수를 통해 bss 영역에 값을 쓰고, 해당 값을 edi로 읽어들이는 방법이 있다.
2.4.2. 단점
csu의 작동 구조 상 ret address overwrite 후 8 x 8 byte 값을 넣어야 다시 한번 csu 함수 내의 ret address에 도달할 수 있기에 main 함수에서 할당된 buffer의 크기가 커야 한다는 것이다.
하지만 한번이라도 도달할 수 있다면
- csu 에서 return 시 main 으로 다시 돌아가던지,
- stack address leak 후 main 함수의 입력 받는 값의 크기를 키워버리거나
- bss 영역에 shellcode를 삽입하여 해당 위치로 돌아가거나
하는 방법으로 사용 가능하다.
또한 그 구조 상 3개 이상의 인자를 삽입할 수 없다.
(사실 3개 이상 인자를 사용할 함수가 없기에 단점 아닌 단점이다.)
가장 좋은 방법은 csu로 libc base만 구하고, main 함수로 돌아가 return 시 onegadget 주소로 return 해버리면 된다.
3. 결론
여러 보호 기법으로 인해 rop을 해야할 것 같은데 적절한 gadget이 찾아지지 않는 경우 차선책으로 rtc를 고려해볼 수 있다.
다만 프로그램마다 __libc_csu_init 함수의 어셈블러 코드는 조금씩 다를 수 있기에 직접 확인해봐야한다.
더불어 제약 조건도 꽤나 까다롭다.
그래도 __libc_csu_init 함수는 프로그램의 코드 영역에 존재하기에 언제든 사용할 수 있다.
'Tips & theory' 카테고리의 다른 글
docker <-> host 파일 전송 (0) | 2022.09.06 |
---|---|
system hacking을 위한 docker 설치 및 사용법 (0) | 2022.09.05 |
one gadget 사용법 (0) | 2022.09.05 |
함수 호출 규약. (0) | 2022.09.05 |
system hacking을 register 기초 (0) | 2022.09.05 |