본문 바로가기

Wargame/pwnable.kr

[Grotesque] asg

쓸까말까 고민하다가.. exploit 코드 전체는 공개안하더라도, 일부분 공개하면서 설명하는건 괜찮지 않을까?? 하고 글을 쓴다.

Write up 작성 목적은 pwnable.kr 점수 올리라는게 아닌, 순수 방문자의 공부에 도움이 되도록 방향을 잡아주기 위함이다.

 

이 글을 시작으로 write up 작성 기준, 틀 등을 마련하여 향후 관련 게시글에도 이를 적용할 수 있도록 잘 써야겠다 싶다.

 

 

==========================================================================================

 

글을 시작하기에 앞서 당부 드린다. 이 글은 수많은 힌트, 풀이 방법이 포함되어 있으므로, 본인의 실력 증진을 원한다면 삽질을 충분히 한 뒤에 글을 읽어주길 바란다.

문제 점수가 높으니 PoC 코드는 올리지 않도록 하겠다.

 

 

asg 문제는 Toddler의 asm 과 틀이 같다.

asm 문제 풀이는 https://shayete.tistory.com/75에 작성하였다. 이 문제를 풀기 전에 가볍게 asm을 푸는 것도 나쁘지 않겠다.

 

1. 코드흐름

====================================================

 

 

main() 위의 stub[] 배열은 rsp 레지스터를 제외한 모든 레지스터를 0으로 초기화하는 쉘코드이다. 이는 asm의 그것과 동일하다.

main에서는 처음에 urandom을 이용해 filter 배열(filter[256])을 셔플한다. shuffle함수가 실행되면 filter 배열에는 0x00 부터 0xff까지 256개의 문자열이 뒤죽박죽이 되어있다.

 

 

 

이후 write를 이용해 filter[256]중 0~127까지만 출력해서 화면에 보여준다. 128개의 문자열을 필터링하겠다는 의미다.

이후 ./genflag를 실행하여 flag가 담긴 파일이름을 출력해준다.

 

"flag is inside this file: [flagbox/블라블라 63개의 문자열]"

 - flag는 flagbox 폴더 내 63개의 문자로 구성된 이름의 파일에 들어있다.

 

이후 mmap을 통해 0x1000 사이즈만큼 임의 주소에 메모리 매핑을 하고 해당하는 주소에 nop(0x90)를 가득 채운다.

그리고 stub, offset을 채우는데.. 구조는 아래 그림과 같다.

 

 

read(0, sh+offset, 1000)을 이용해 사용자로부터 1000바이트만큼 문자를 입력받는다. 입력값과 filter[0~127]가 같은지 비교를 한 후 필터링한 값이 존재한다면 "caught by filter!"를 외치며 프로그램이 종료된다.

 

그러나 입력값에 filter값이 존재하지 않는다면

 

 

10초정도 대기후 root를 /home/asg_pwn로 설정한 후 "buena suerte!"를 출력한다. 쉘코드 입력하고 "buena suerte!"가 출력됬다는 건 필터링(filter[0~127]을 뚫었다는 의미이다.

 

이후 alloca(rand()*12345) % 1024);를 호출하여 스택에 0~1023만큼 임의 공간을 할당한다. 이 함수는 스택에 존재하는 값들을 쓰지 말라고 사용되었다. 그림으로 보자면..

 

 

이렇다.

마지막으로 asm("jmp *%0", :: "r"(sh)); 를 이용해 sh를 실행한다. 그럼 rsp를 제외한 모든 레지스터가 초기화되고 우리가 작성한 쉘코드가 실행된다.

 

2. 고려사항

====================================================

이 문제는 다음과 같은 점을 주의해야 할 것이다.

 

  1. filter[0] ~ filter[127]의 값을 피해서 쉘코드를 생성해야 한다.

  2. mmap으로 할당되는 주소는 random이며(사실 서버는 항상 열려있으니 주소 자체가 랜덤은 아니겠지만, 우리가 알 수 있는 방법이 없다) rsp를 제외한 모든 레지스터를 초기화하는 쉘코드(stub)의 뒤에 붙는 offset도 랜덤이다.

  3. pwnable.kr 서버의 /tmp에서 뭐 못한다. 허튼짓 x

  4. alloca()로 스택 공간을 랜덤하게 할당한다. 기존 스택에 쌓여있는 값들 사용 못함

  5. flagbox 뒤에 붙는 문자열도 당연히 filter[0] ~ filter[127]에 걸린다. 그걸 고려해서 쉘코드를 작성해야 한다.

  6. stub 쉘코드는 rsp 레지스터를 초기화하지 않는다. 왜?? 사용해서 문제풀라고.

  7. 페이로드는 1000바이트를 넘어서는 안된다.

3. 필요 지식

====================================================

단순하지만 단순하지 않은 문제이다. filter만 없었어도 단순했을텐데. 이 문제를 풀기 위해서는 다음과 같은 지식이 필요하다.

 

1. 쉘코드를 이용해 open, read, write, exit 함수를 실행시키기 위해서는 system call table이 뭔지 알아야 한다.

관련 주소: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/

x64 환경의 read(), write(), open()함수의 system call 번호는 각각 0, 1, 2 이다.

 

2. x64 환경의 함수가 어떻게 동작하는지 정확하게 알아야 한다.

예를 들면, read(fd, buf, sizeof(buf))라는 함수가 있을 때, 첫번째 인자는 fd, 두번째 인자는 buf, 세번째 인자는 sizeof(buf)이다.

첫번째 인자값은 rdi 레지스터에 들어가고, 두번째는 rsi, 세번째는 rdx에 들어간다. 함수의 반환값은 rax에 들어간다.

관련 주소: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64

근데, 쉘코드에서 시스템콜을 위해서는 rax에 해당 함수 번호가 들어간다.

 

3. 쉘코드 만드는 법을 알아야 한다.

원리만 알면 뭐하나, 이제 쉘코드 만드는 법을 알아야 한다.

관련 주소: https://www.geeksforgeeks.org/as-command-in-linux-with-examples/

이 문제에서는 asm과는 다르게 as 어셈블러를 이용해야한다. nasm에서는 rip 레지스터를 지원하지 않기 때문이다. (rip 사용못함)

그래서 nasm 대신 as를 이용해 바이너리를 만들어야한다.

 

4. 어셈블리어 구조를 알아야 한다.

관련 주소: https://www.lri.fr/~filliatr/ens/compil/x86-64.pdf

문제를 풀 때 구조를 깊이있게 몰라도 상관은 없겠으나, 시스템해킹을 잘 하고 싶다면 반드시 알아두는 게 좋다.

 

5. 코딩을 잘해야 한다.

 ..... 

... 파이썬 공부 화이팅

 

4. 문제 풀이

====================================================

문제는 다양한 방법으로 풀 수 있겠지만, 나는 이렇게 풀었다.

..

 

1. 필터링을 우회해야 하니 우회할 수 있는 방법을 먼저 찾았다. 그 방법은 "subl rip, 0x...."

rip를 직접 제어하여 쉘코드를 원래 코드로 복원한다. 어떻게?? 2번에서 설명하겠다. 2번의 변환과정을 보면 rip로 쉘코드를 변환하는 방법도 눈치챌 것이다.

 

2. 72바이트로 구성된 flagname은 스택에 저장하고 해당 주소를 호출하면 되는데 subl로 바꾸기엔 방법이 너무 복잡하다. 그래서 이건

 

"""

mov r9, replace flagname 8바이트

mov r8, replace count 8바이트

sub r9, r8

push r9

"""

이렇게 우회하여 flagname을 스택에 박아줬다. 무슨 말인지 감이 안올거라 생각하여 그림을 그려주겠다.

 

 

먼저 필터를 우회한 replace Flagname을 생성한다. flagbox/가 플래그 이름일 때 필터에 f, g, x가 존재한다면 replace flagname은 hlahboy/와 같이 변할 것이다. 이 때 replace한 카운트를 센다.

f는 h로 변할 때 총 2번 카운트되고 (f 다음 g, h이기 때문에 총 2번 변할 것이다.) g는 1번, x는 y로 될 때 1번 카운트된다. replace 안 된 부분은 0을 넣으면 된다.

 

 

replace flag와 replace count를 빼면 원상태의 flagname으로 복원되는데, 이를 스택에 푸쉬하면 원상태의 flagname이 스택에 들어갈 것이다.

 

이것만 알면 문제풀이는 끝난다. 쉘코드를 작성해주면 된다. 문제풀이에 사용한 쉘코드는 아래와 같다.

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
print ("[*] init shellcode..")
open_shellcode = "\x4c\x89\xd7\x90"
# mov rdi, r10 # nop
open_shellcode += "\xb0\x02\x0f\x05"
# mov al, 0x2 # syscall
open_shellcode = list(open_shellcode)
## push a string into the list by 1 byte.
 
read_shellcode = "\x50\x5f\x90\x90"
# push rax # pop rdi # nop # nop
read_shellcode += "\x66\xba\x10\x01"
# mov dx, 0x110
read_shellcode += "\x49\x83\xc4\x7f"
# add r12, 127(0x7f)
read_shellcode += "\x4d\x01\xd4\x90"
# add r12, r10  [r12 = rsp + 100(0x64)]
read_shellcode += "\x4c\x89\xe6\x90"
# mov rsi, r12 # nop
read_shellcode += "\x48\x31\xc0\x90"
# xor rax, rax # nop
read_shellcode += "\x0f\x05\x90\x90"
# syscall # nop # nop
read_shellcode = list(read_shellcode)
## push a string into the list by 1 byte.
 
write_shellcode = "\x48\x31\xc0\x90"
# xor rax, rax # nop
write_shellcode += "\x50\x5f\xb0\x01"
# push rax # pop rdi # mov al, 1
write_shellcode += "\x48\x83\xc7\x01"
# add rdi, 1
write_shellcode += "\x66\xba\x10\x01"
# mov dx, 0x110
write_shellcode += "\x4c\x89\xe6\x90"
# mov rsi, r12
write_shellcode += "\x0f\x05\x90\x90"
# syscall # nop # nop
write_shellcode = list(write_shellcode)
## push a string into the list by 1 byte.
cs

 

open shellcode 가 실행되기 전 mov r10, rsp를 써줬다. (위의 쉘코드에서는 안보이지만..)

mov r10, rsp 쓸 필요 없이 open shellcode 첫문장을 mov rdi, rsp로 변경해도 상관없다.

 

중간중간에 nop을 써준 이유는 "subl rip, 쉘코드"를 이용해 필터링 우회한 쉘코드를 원상태로 복원해야 하는데, subl은 2번째 operand를 4바이트 입력받기 때문이다. 4바이트를 맞춰주기 위해 부족한 사이즈는 nop으로 채웠다. 그리고 r11 레지스터를 사용하지 않은게 보일텐데, syscall이 호출되면 r11에 특정한 값이 담긴다. 이게 뭔지는 잘 모르겠지만 건들어서 좋을 게 없을 것 같으니 r12를 이용하였다.

(syscall 호출 후 r11에 뭐가 담기는지 아시는분 댓글좀..)

 

이제 코드를 잘 짜서 문제를 풀면 끝!

'Wargame > pwnable.kr' 카테고리의 다른 글

[Grotesque] aeg  (2) 2020.10.03
[Rookies] crypto1  (0) 2020.09.14
[Rookies] note  (0) 2020.09.10
[Toddler] fd  (0) 2020.09.06
[Toddler] asm  (2) 2020.07.05