Post

(SecuInside 2017) OHCE - x64 SROP

get_userinput이 제일 소스가 길어서 거기에 취약점이 있을 것 같아 집중적으로 봤는데 정작 다른 곳에 있었음… 시간을 무지 낭비함.
풀고 나서 보니까 NX가 비활성화 되어 있었다 ㅡㅡ; 어쩐지 checksec으로 봤을 때는 disable로 뜨는데 stack에 x권한이 왜 있지? 싶었더라니… 반드시 nxtest를 사용해야겠다.

SROP같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

  

$ ldd ohce
not a dynamic executable

  

start addr : 0x4000B0

Note ) 이런 바이너리는 HexLay가 이상하게 나올 수 있으니 주의! 완전히 이상하게 나오는건 아닌데, 어떤 부분을 빼먹거나 변수를 잘못 추적해서 HexLay만 보면 놓치는 부분이 생겨 시간이 굉장히 오래걸린다! HexLay와 gdb를 동시에 보면서 gdb의 instruction을 하나하나 따라가며 대조해보는게 훨씬 빠름.

Note ) sys_read에 bp 거는 경우 원래는 sys\_readsize(rdx)를 넘는 문자열을 입력해도 반복문 내에서 syscall할 때 마다 버퍼에 남아있는 문자열을 조금 씩 가져오기 때문에 문제가 발생하지 않지만, sys_read에 bp를 거는 경우 size를 초과하는 부분은 애초에 버퍼로 들어가지 않아 버퍼가 비어있게 된다. 따라서 다음 syscall 시 처음 입력 받는 것 처럼 입력을 대기하게 되므로 주의. syscall 바로 다음 instruction에 bp 걸고 디버깅하면 입력하는 데이터가 stack/register context에 보이기 때문에 편하다.

이상한 부분 #1 get_userinput

rsp(top) + rbx(idx)가 buf의 주소(rbp-0x20~rbp 지점)이며 sys_read시 여기에 받는다. buf가 가득 찰 때 까지(0x20) 입력 받다가, 가득 차면 stack을 0x20 확장하고 buf 위의 공간 전체를 0x20만큼 위로 옮겨쓴다(이동시킨다.)

여기가 핵심일 것 같아서 자세히 보느라 시간을 엄청 많이 소모함.
실제로 rbx(idx)를 LOWORD()로 계산하기 때문에 0xffff가 넘어가면 0x0이 되어버린다.

leak #2 echo

\n포함 0x20 byte만큼 입력하면 그 뒤에 있는 sfp가 leak된다. 근데 #1 때문에 0x20을 초과해서 입력해봐야 소용없고, 0x20 배수로 입력해도 동일하게 동작한다.

rbp 조작 #3 echo_reverse

#2와 동일하게 0x20만큼 입력했더니 leak되면서 fault가 발생한다. 이어져있는 sfp까지 포함해서 reverse해버리기 때문에, rbp가 변경되면서 fault가 발생.

VULN) 따라서 rbp를 조작할 수 있으며, 바로 이어지는 다음 gadget을 고려하면 rsp -> ret도 조작할 수 있다.

1
2
3
0x40018b:    mov    rsp,rbp
0x40018e:    pop    rbp
=> 0x40018f:    ret

"/bin/sh" 등의 마땅한 string이 바이너리에 없기 때문에, stack에 입력하여 사용해야 한다.

#3을 이용해 leak하려면 leak을 수행하고 나서 바이너리가 종료되지 않도록 하기 위해 rbp가 적당한 곳에 위치하도록 해야한다. 만만한 곳이 .data section이나, get_userinput에서 \x00을 받으면 buf에 저장되기는 하지만 length\x00 전까지만 계산된다. 그러면 rbp에 stack을 지정할 수 밖에 없으므로 바이너리가 종료되는 것을 막을 수 없다. 따라서 그냥 #2를 이용해 leak하는게 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
leaked sfp : 0x7fffffffe2e0

  

------Layout------
buf (e2b0)  : e2c0
rbp-0x18 :
rbp-0x10 : sfp
rbp-0x08 : ret
rbp (e2d0)  : e2e0
rbp+0x08 : ret
e2e0 : 0x0000000000000000
-----reverse------
buf      : e2e0
rbp-0x18 :
e2c0 : ===sfp=== 어딘가로...
rbp-0x08 : ===ret===
e2d0 (rbp)  : e2c0
rbp+0x08 : ret
  1. 0x20 배수로 parameter를 입력해두고,
  2. 어딘가로 ret해서 RAX : 0xf로 만들고
  3. 다시 syscall해서 sigreturn 실행.
set rax

바이너리에서 제공하는 sys\_read는 get_userinput 뿐인데 rdx(size) == 1로 고정이다. 그래서 이를 이용해 rax를 설정할 수는 없다. 그렇다고 rdx를 조작할 수 있는 gadget이 있는 것도 아니라서, sys_read를 사용하는 방법으로는 어렵다.

대신 echo 메뉴의 sys_write 를 사용하면 가능할 것 같다. 15개 입력하고 15개 출력하도록 하면 되니까. reverse 되면 arg 넘기기가 귀찮아지기도 하고, reverse는 0x00에서 에러가 발생하기 때문에 arg넘기는건 1. echo를 사용해서 넘기고, rbp 조작만 2. echo(reverse)를 사용하도록 한다.

  
data topfake_sfp2
 ret (echo_addr)
 ret (syscall_addr)
sigreturn 직전 rsp/bin/sh\00
0x20 dummy 
….. sigcontext ( 256, 0x100 ) ….. 
  1. echo를 이용해 leak.
  2. echo를 이용해 payload 입력해두기.
  3. reverse에 0x20만큼 입력하며 rbp가 입력해둔 payload(data top)를 가리키도록 설정.
  4. 다시 echo로 ret해서 RAX : 0xf로 만들고
  5. syscall해서 sigreturn 실행.

https://github.com/umbum/pwn/blob/master/exploit/secu_ohce.py

This post is licensed under CC BY 4.0 by the author.