printf와 fprintf의 소스코드를 비교해 보자.
printf
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stdout, format, arg, 0);
va_end (arg);
return done;
}
fprintf
int
__fprintf (FILE *stream, const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stream, format, arg, 0);
va_end (arg);
return done;
}
보면 알 수 있지만, 사실상 둘은 똑같다고 볼 수 있다.
그냥 stream 인자로 stdout을 주는 것인지, user-defined 값으로 주는 것인지의 차이일 뿐이다.
앞으로 fprintf를 기준으로 분석할 것이다.
먼저 인자들이 메모리에 어떻게 쌓이는지 확인해 보자.
Dump of assembler code for function __fprintf:
0x00007ed91ede0bd0 <+0>: endbr64
0x00007ed91ede0bd4 <+4>: sub rsp,0xd8
0x00007ed91ede0bdb <+11>: mov QWORD PTR [rsp+0x30],rdx
0x00007ed91ede0be0 <+16>: mov QWORD PTR [rsp+0x38],rcx
0x00007ed91ede0be5 <+21>: mov QWORD PTR [rsp+0x40],r8
0x00007ed91ede0bea <+26>: mov QWORD PTR [rsp+0x48],r9
0x00007ed91ede0bef <+31>: test al,al
0x00007ed91ede0bf1 <+33>: je 0x7ed91ede0c2a <__fprintf+90>
0x00007ed91ede0bf3 <+35>: movaps XMMWORD PTR [rsp+0x50],xmm0
0x00007ed91ede0bf8 <+40>: movaps XMMWORD PTR [rsp+0x60],xmm1
0x00007ed91ede0bfd <+45>: movaps XMMWORD PTR [rsp+0x70],xmm2
0x00007ed91ede0c02 <+50>: movaps XMMWORD PTR [rsp+0x80],xmm3
0x00007ed91ede0c0a <+58>: movaps XMMWORD PTR [rsp+0x90],xmm4
0x00007ed91ede0c12 <+66>: movaps XMMWORD PTR [rsp+0xa0],xmm5
0x00007ed91ede0c1a <+74>: movaps XMMWORD PTR [rsp+0xb0],xmm6
0x00007ed91ede0c22 <+82>: movaps XMMWORD PTR [rsp+0xc0],xmm7
0x00007ed91ede0c2a <+90>: mov rax,QWORD PTR fs:0x28
=> 0x00007ed91ede0c33 <+99>: mov QWORD PTR [rsp+0x18],rax
0x00007ed91ede0c38 <+104>: xor eax,eax
0x00007ed91ede0c3a <+106>: lea rax,[rsp+0xe0]
0x00007ed91ede0c42 <+114>: xor ecx,ecx
0x00007ed91ede0c44 <+116>: mov rdx,rsp
0x00007ed91ede0c47 <+119>: mov QWORD PTR [rsp+0x8],rax
0x00007ed91ede0c4c <+124>: lea rax,[rsp+0x20]
0x00007ed91ede0c51 <+129>: mov DWORD PTR [rsp],0x10
0x00007ed91ede0c58 <+136>: mov DWORD PTR [rsp+0x4],0x30
0x00007ed91ede0c60 <+144>: mov QWORD PTR [rsp+0x10],rax
0x00007ed91ede0c65 <+149>: call 0x7ed91edf5860 <__vfprintf_internal>
0x00007ed91ede0c6a <+154>: mov rcx,QWORD PTR [rsp+0x18]
0x00007ed91ede0c6f <+159>: xor rcx,QWORD PTR fs:0x28
0x00007ed91ede0c78 <+168>: jne 0x7ed91ede0c82 <__fprintf+178>
0x00007ed91ede0c7a <+170>: add rsp,0xd8
0x00007ed91ede0c81 <+177>: ret
0x00007ed91ede0c82 <+178>: call 0x7ed91eeaec90 <__stack_chk_fail>
End of assembler dump.
앞으로 format string의 인자들을 format string의 표기를 따라 1$
, 2$
, ... 이런 식으로 표기할 것이다.
<__fprintf + 11>
~ <__fprintf + 26>
1$
, 2$
, 3$
, 4$
를 rsp + 0x30
부터 순서대로 메모리에 저장해 준다.<__fprintf + 31>
~ <__fprintf + 82>
__fprintf + 31
~ __fprintf + 82
까지는 jmp된다.<__fprintf + 90>
~ <__fprintf + 99>
rsp + 0x18
에 canary가 저장된다.<__fprintf + 104>
~ <__fprintf + 119>
rsp + 0x8
에 5$
부터가 저장된 주소가 저장된다.<__fprintf + 124>
~ <__fprintf + 144>
rsp
에 0x10이 저장된다.rsp + 0x4
에 0x30이 저장된다.rsp + 0x10
에 rsp + 0x20이 저장된다.typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list[1];
va_list
구조체는 위와 같은데, rsp
의 위치에 va_list
가 저장된다고 생각하면 된다.그 후에 __vfprintf_internal
함수가 호출된다.
__vfprintf_internal
함수가 호출되기 전의 메모리 구조를 확인해 보면 다음과 같다.
-------va_list-------
rsp + 0x0 0x10
rsp + 0x4 0x30
rsp + 0x8 rsp + 0xe0 (5$부터의 주소)
rsp + 0x10 rsp + 0x20
---------------------
rsp + 0x18 canary
rsp + 0x20 ??
rsp + 0x28 ??
rsp + 0x30 1$
rsp + 0x38 2$
rsp + 0x40 3$
rsp + 0x48 4$
rsp + 0x50
...
rsp + 0xd8 ret_addr
rsp + 0xe0 5$
rsp + 0xe8 6$
...
여기까지만..