Pwnable 문제를 풀다가 문득 32bit ROP를 살짝 잊었다는 기분이 들었다. (…) 예제 코드를 작성하고 그것을 익스하려고 했으나 생각대로 잘 되지 않아 분석을 진행하였다. 함수 프롤로그와 에필로그에 낯선 코드가 추가되어 있었고, 이 때문에 buf와 sfp 사이에 무언의 값이 들어있었다. 분석 결과, 원인은 gcc 옵션 중 하나인 -mpreferred-stack-boundary 때문임을 알게되었다.
Pwnable에서의 32bit 바이너리는 (거의) 대부분 -mpreferred-stack-boundary=2 옵션을 주고 컴파일을 한다.
gcc option: -mpreferred-stack-boundary=N
Attempt to keep the stack boundary aligned to a 2 raised to num byte boundary. If ‘-mpreferred-stack-boundary’ is not specified, the default is 4 (16 bytes or 128 bits).
/* Validate -mpreferred-stack-boundary= value or default it to PREFERRED_STACK_BOUNDARY_DEFAULT. */ ix86_preferred_stack_boundary = PREFERRED_STACK_BOUNDARY_DEFAULT; if (opts_set->x_ix86_preferred_stack_boundary_arg) { int min = TARGET_64BIT_P (opts->x_ix86_isa_flags)? 3 : 2; int max = TARGET_SEH ? 4 : 12;
if (opts->x_ix86_preferred_stack_boundary_arg < min || opts->x_ix86_preferred_stack_boundary_arg > max) { if (min == max) error ("%<-mpreferred-stack-boundary%> is not supported " "for this target"); else error ("%<-mpreferred-stack-boundary=%d%> is not between %d and %d", opts->x_ix86_preferred_stack_boundary_arg, min, max); } else ix86_preferred_stack_boundary = (1 << opts->x_ix86_preferred_stack_boundary_arg) * BITS_PER_UNIT; }
32bit 프로그램에선 block 단위가 4바이트이기 때문에 컴파일러(gcc)가 and 연산으로 esp를 16에 align시키는 모습을 확인할 수 있다.
ecx 레지스터를 왜 push 했을까?
ecx is a temporary register so it has to be saved. Also, depending on optimization level, some of the frame linkage ops that don’t seem strictly necessary to run the program might well be important in order to set up a trace-ready chain of frames.
에필로그를 진행해서 ebp와 esp를 애써 맞춰주었음에도 불구하고(?) ecx를 push했다. 32bit 프로그램이라면 무조건 buf, ebp(sfp) 순일 줄 알았는데 중간에 ecx가 끼어있다. 이것을 보지 못하고 무작정 익스를 진행하려고 하다가 평소와 스택이 쌓이는 것이 이상함을 감지하고 분석을 진행하게 되었다.
참고로 64bit 프로그램에서는 block 단위가 4바이트에서 8바이트로 늘어났기 때문에 -mpreferred-stack-boundary=2 옵션을 사용하려고 하면 에러를 내뿜는다.
궁금증
무려 6년 9개월 전 어떤 분이 하신 궁금증과 동일한 궁금증을 가졌다. 안타깝게도 해당 질문에 대한 답은 찾아보기 어려웠다.
I noticed that when not using -mpreferred-stack-boundary=2, gcc might compile “main” with an interesting prologue/epilogue: effectively “relying on $ecx (which relies on $ebp-4) for $esp value before ret”. Has anyone else come across this observation?
This means you can not overwrite normal ret address staying at $ebp+4, but instead you have to overwrite $ebp-4 (that is ecx) and reposition the stack pointer and your return address (effectively using a stack pivot) to further the exploitation.