[PWN] bytechanger Write-Up

Magnolia·5일 전

보호 기법


코드 분석

main()

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+7h] [rbp-19h] BYREF
  __int64 v5; // [rsp+8h] [rbp-18h] BYREF
  void *addr; // [rsp+10h] [rbp-10h]
  unsigned __int64 v7; // [rsp+18h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(_bss_start, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  addr = (void *)((unsigned __int64)main & 0xFFFFFFFFFFFFF000LL);
  mprotect((void *)((unsigned __int64)main & 0xFFFFFFFFFFFFF000LL), 0x1000u, 7);
  printf("change only 1 byte (idx): ");
  __isoc99_scanf("%lu", &v5);
  printf("change to (val): ");
  __isoc99_scanf("%hhu", &v4);
  *((_BYTE *)addr + v5) = v4;
  return 0;
}

프로그램은 main이 들어있는 페이지의 시작 주소를 구하고 그 페이지 하나를 RWX 권한으로 바꾸고 사용자가 준 idx와 val로 1바이트를 덮는다.

main의 마지막에서 카나리가 정상이라면 je 0x123a가 실행되고 그대로 leave; ret으로 종료된다.


취약점 분석

먼저 idx에 대한 범위 검사가 없다. 따라서 이 코드는

*(page(main) + idx) = val;

을 수행한다고 볼 수 있다.

이 문제에서 중요한 건 쓰기 대상이 main의 페이지라는 것이다. main의 오프셋이 0x1203이므로 main의 페이지는 항상 base + 0x1000이다. 따라서 덮을 수 있는 영역은 base + 0x1000 ~ base + 0x1fff이기 때문에 코드 페이지 내부이다. 따라서 코드페이지 임의의 1바이트 쓰기가 가능하다.

PIE도 알빠가 아니다. 우리가 필요한건 절대주소가 아니라 같은 PIE 내부에서의 상대 오프셋이기 때문이다.


익스플로잇

처음엔 main 끝의 je를 직접 win으로 돌리려고 했다.

하지만 je는 short jump라 displacement가 1바이트고 다음 RIP는 0x1325이다. 또한 필요한 값은 0x11e9 - 0x1325 = -0x13c인데 short jump 범위는 -0x80 ~ 0x7f이기 때문에 je에서 바로 win으로 가는 것은 거리 부족으로 불가능하다.

그래서 한번만 쓸 수 있는 구조를 여러 번 쓸 수 있는 구조로 바꿔서 문제를 풀기로 했다.

먼저 je를 뒤로 보내서 입력 루프를 만든다.
원래 바이트는 다음과 같다.

0x1323에서 바이트코드 74 05에서 두번째 바이트를 0x8e로 바꾸면

je 0x12b3가 된다.

0x12b3scanf를 준비하는 지점이기 때문에 카나리 체크를 통과하면 프로그램이 다시 입력을 받게 된다.

이때 바꿔야 하는 주소는 0x1324이고 main의 페이지는 base + 0x1000이기 때문에 주소 계산은 다음과 같다.

idx = 0x1324 - 0x1000 = 0x324 = 804
val = 0x8e = 142

첫 입력은

804
142

가 된다.

이후에는 call __stack_chk_failcall main으로 바꾼다.

원래 명령은 다음과 같다.

call ret32는 다음 RIP(0x132a) 기준 상대 오프셋을 가진다.

원래 값은

0x10a0 - 0x132a = -0x28a = 0xfffffd76

이지만

우리가 원하는 값은

0x11e9 - 0x132a = -0x141 = 0xfffffebf

가 되어 바이트코드 76 fd ff ff -> bf fe ff ff로 바꾸면 된다.

이미 뒤에 ff ff는 같기 때문에 앞 2바이트만 바꾸면 된다.

0x1326: 0x76 -> 0xbf
0x1327: 0xfd -> 0xfe

각각의 입력은 다음과 같다.

idx = 0x1326 - 0x1000 = 0x326 = 806, val = 191
idx = 0x1327 - 0x1000 = 0x327 = 807, val = 254
806
191

807
254

마지막으로 루프를 끝내고 win을 실행시킨다.

아직 je 0x12b3 상태이기 때문에 계속 입력 루프만 돈다. 이제 다시 0x13240x00으로 바꿔서 74 00으로 되게 한다.

이 의미는 je 0x1325가 된다. 카나리 체크가 통과하기 때문에 분기는 taken이 되고 곧바로 아래의 call이 실행된다. 하지만 아래 call은 win 함수로 바뀌어 있기 때문에 win 함수가 실행된다.

전체 입력은 다음과 같다.

804
142
806
191
807
254
804
0


정리

사용자가 입력한 idx와 val로 main이 포함된 메모리 페이지에 1바이트를 덮어쓸 수 있게 해주는 문제이다. 프로그램은 먼저 main의 페이지 시작 주소를 구하고 해당 페이지를 mprotect로 RWX 권한으로 바꾸고 경계 검사 없이 main의 페이지 + idx 위치에 val을 기록한다. 따라서 공격자는 PIE 여부와 관계 없이 같은 코드 페이지 내부의 명령어 바이트를 직접 수정할 수 있고 이를 이용해 실행 흐름을 win 함수로 유도하는 문제였다.

오랜만에 익스플로잇 코드를 작성하지 않고 문제를 풀어서 인상깊었고
항상 느끼는거지만 이렇게 원격으로 어셈 패치하는게 재밌는거같다.

0개의 댓글