ret2csu는 __libc_csu_init 함수에 존재하는 gadget을 이용하여 ROP 상황에서 레지스터를 세팅하고 원하는 함수를 호출하는 공격 기법이다.
일반적인 ROP 상황에서는 pop rdi와 같은 gadget을 사용하여 함수 인자를 세팅하지만 어떤 바이너리에서는 이런 gadget이 존재하지 않거나 사용하기 어려운 경우가 있다.
이때 __libc_csu_init내부 코드를 활용하면 여러 레지스터를 동시에 세팅하면서 함수 호출까지 수행할 수 있다.
프로그램은 main 함수부터 실행되는 것처럼 보이지만 실제로는 그 전에 여러 초기화 함수들이 먼저 실행된다.
대표적인 실행 흐름은 다음과 같다.
_start
__libc_start_main
__libc_csu_init
main
_fini
이 중 __libc_csu_init 함수는 프로그램 초기화 과정에서 호출되는 함수이며 대부분의 ELF 바이너리에 포함되어 있다.
이 함수 내부에는 ROP에서 활용 가능한 gadget들이 존재한다.
__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);
}
일반적인 rop 문제에서 gdb로 info function을 해보면 다음과 같다.

__libc_csu_init 함수가 있고 해당 함수를 디스어셈블하면 다음과 같다.

여기서 __libc_csu_init + 84를 기준으로 위쪽을 stage2, 아래쪽을 stage1이라고 한다.
이 부분은 스택의 값을 레지스터로 삽입한다.

예를 들어 스택이 다음과 같다.
padding
A
B
C
D
E
F
RET
이 코드가 실행되면 다음과 같이 레지스터가 채워진다.
rbx = A
rbp = B
r12 = C
r13 = D
r14 = E
r15 = F
그리고 ret을 통해 다음 gadget으로 이동하게 된다.
이 부분에서는 위에서 세팅된 레지스터를 이용해 실제 함수 호출을 수행한다.

mov rdx, r15
mov rsi, r14
mov edi, r13d
call *(r12 + rbx*8)
이 코드의 의미는 다음과 같다.
rdi = r13
rsi = r14
rdx = r15
call *(r12 + rbx*8)
즉 함수 인자 설정 -> 함수 호출을 동시에 수행한다.
예를 들어 다음과 같은 상황이 있다.
rbx = 0
r12 = puts@got
그러면 다음 코드가 실행된다.
call *(r12 + rbx*8)
즉
call *puts_got
이 되고 실제 puts 함수가 실행된다.
또한 함수 호출 규약에 따라
rdi
rsi
rdx
레지스터 값이 함수 인자로 사용된다.
예를 들어 다음과 같이 설정하면
r13 = read_got
r14 = buffer
r15 = size
다음과 같은 함수 호출이 가능하다.
puts(read_got)
이를 이용하면 read의 got leak이 가능하다.
stage2의 아래 부분을 보면 다음과 같은 코드가 있다.
add rbx, 1
cmp rbx, rbp
jne stage2
즉 rbx != rbp이면 다시 stage2를 반복한다.
따라서 ret2csu 공격에서는 일반적으로 다음과 같은 값을 사용한다.
rbx = 0
rbp = 1
이렇게하면 stage2가 한 번만 실행되고 루프를 빠져나오게 된다.
ret2csu에서 stage1에 넣어야 하는 값은 다음과 같다.
rbx = 0
rbp = 1
r12 = 호출할 함수 주소 (예: puts@got)
r13 = arg1
r14 = arg2
r15 = arg3
즉 ret2csu는 다음과 같은 함수 호출을 만들 수 있다.
func(arg1, arg2, arg3)
edi는 4byte만 사용한다.
mov edi, r13d
이기 때문에 rdi 전체가 아니라 하위 32bit만 사용된다.
따라서 큰 주소를 직접 전달하기 어려운 경우가 있다.
이 경우 보통 read -> bss 값을 쓰고 그 주소를 사용한다.
또한 csu 구조상 여러 에지스터 값을 스택에 넣어야 하기 때문에 payload 길이에 제약에 생기고 3개의 인자까지만 설정이 가능하다.
ret2csu는 ROP 공격에서 필요한 gadget이 부족할 때 __libc_csu_init 함수 내부 코드를 활용해 함수 인자를 세팅하고 임의의 함수를 호출할 수 있도록 하는 기법이다. 특히 pop rdi, pop rsi, pop rdx와 같은 gadget이 존재하지 않는 상황에서도 r13, r14, r15, 레지스터를 통해 각각 rdi, rsi, rdx, 값을 설정하고 call *(r12 + rbx*8) 구조를 이용해 원하는 함수를 실행할 수 있다는 점에서 유용하다. 다만 설정 가능한 인자가 세 개로 제한 되고 스택에 많은 값을 배치해야 하는 제약이 존재하므로 보통 libc 주소를 leak 하는 단계에서 활용한 뒤 이후 ret2libc나 onegadget으로 이어지는 방식으로 사용된다.