일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- cryptography
- webhacking
- got overwrite
- pwnable
- 2022 Fall GoN Open Qual CTF
- Wreckctf
- hack
- webhacking.kr
- h4cking game
- hacking game
- reversing
- typhooncon
- 워게임
- KAIST
- christmas ctf
- TeamH4C
- python
- CTF
- hacking
- writeup
- Buffer Overflow
- dreamhack
- ctf player
- crypto
- WEB
- Gon
- 해킹
- 웹해킹
- Wargame
- System Hacking
- Today
- Total
deayzl's blog
[2022 Fall GoN Open Qual CTF] Bomblab - Hard writeup 본문
[2022 Fall GoN Open Qual CTF] Bomblab - Hard writeup
deayzl 2022. 8. 31. 21:00
undefined8 main(int param_1,undefined8 *param_2)
{
undefined8 uVar1;
long in_FS_OFFSET;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
if (param_1 == 1) {
stdinaddr = stdin;
}
else {
if (param_1 != 2) {
printf("Usage: %s [<input_file>]\n",*param_2);
/* WARNING: Subroutine does not return */
exit(8);
}
stdinaddr = fopen((char *)param_2[1],"r");
if (stdinaddr == (FILE *)0x0) {
printf("%s: Error: Couldn\'t open %s\n",*param_2,param_2[1]);
/* WARNING: Subroutine does not return */
exit(8);
}
}
init();
puts("Welcome to my fiendish little bomb. You have 6 phases with");
puts("which to blow yourself up. Have a nice day!");
uVar1 = InputString();
phase1(uVar1);
DefuseBomb();
puts("Phase 1 defused. How about the next one?");
uVar1 = InputString();
phase2(uVar1);
DefuseBomb();
puts("That\'s number 2. Keep going!");
uVar1 = InputString();
phase3(uVar1);
DefuseBomb();
puts("Halfway there!");
uVar1 = InputString();
phase4(uVar1);
DefuseBomb();
puts("So you got that one. Try this one.");
uVar1 = InputString();
phase5(uVar1);
DefuseBomb();
puts("Good work! On to the next...");
uVar1 = InputString();
phase6(uVar1);
DefuseBomb();
puts("Umm... Good job bb :)");
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
ghidra 로 디컴파일한 모습이다.
총 6번의 phase 가 있으며, 이 모든 phase 를 통과해야 한다.
phase1 :
void phase1(char *string)
{
int iVar1;
byte *i;
for (i = (byte *)string; *i != 0; i = i + 1) {
*i = *i ^ 0xcc;
}
iVar1 = memcmp(string,&DAT_00102020,0xb);
if (iVar1 != 0) {
bomb();
}
return;
}
입력받은 string 에 0xcc 를 xor 값이 DAT_00102020 에 있는 값과 같아야 한다.
#phase1
data102020 = [0x83, 0x9c, 0x89, 0x82, 0x93, 0x9f, 0x89, 0x9f, 0x8d, 0x81, 0x89, 0x00]
st = b''
for i in range(0xb):
st += chr(data102020[i] ^ 0xcc).encode()
print(p.recvline().decode())
print(p.recvline().decode())
p.send_raw(st + b'\n')
이렇게 하면 phase1 을 통과할 수 있다.
phase2 :
void phase2(char *string)
{
long in_FS_OFFSET;
int i;
int intarray [6];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
Get6Integer(string,intarray);
if (intarray[0] < 0) {
bomb();
}
for (i = 1; i < 6; i = i + 1) {
if (intarray[i] != intarray[i + -1] + ((i + 1) * i >> 1)) {
bomb();
}
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
입력한 string 으로부터 6개의 integer 를 받고, validate 하는 것을 볼 수 있다.
#phase2
data2 = [0, 0, 0, 0, 0, 0]
st = b'0 '
for i in range(1, 6):
data2[i] = data2[i - 1] + ((i + 1) * i >> 1)
st += str(data2[i]).encode() + b' '
st = st[:-1]
print(p.recvline().decode())
p.send_raw(st + b'\n')
(phase4 까지는 거기서 거기라, 딱히 설명을 하지는 않겠다)
phase3 :
void phase3(undefined8 param_1)
{
long in_FS_OFFSET;
uint uinteger1;
int uinteger2;
int local_1c;
ulong ulong1;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
uinteger2 = 0;
local_1c = 0;
local_1c = __isoc99_sscanf(param_1,"%u %u",&uinteger1,&uinteger2);
if (local_1c < 2) {
bomb();
}
ulong1 = (ulong)uinteger1 * 0x1ad7e715;
if (uinteger1 < 0xdeadbeef) {
bomb();
}
if ((ulong)(uinteger1 - uinteger2) / 0x3d != (ulong1 >> 0x35) * 0x500bf) {
bomb();
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
#phase3
uint_1 = 0xdeadbeef
uint_3 = uint_1 * 0x1ad7e715
uint_2 = uint_1 - ((uint_3 >> 0x35) * 0x500bf) * 0x3d
if((uint_1 - uint_2) / 0x3d != (uint_3 >> 0x35) * 0x500bf):
exit(1)
print(p.recvline().decode())
p.send_raw(str(uint_1).encode() + b' ' + str(uint_2).encode() + b'\n')
phase4 :
int recursive(int param_1,int param_2,int param_3)
{
int iVar1;
int iVar2;
if (param_1 == 0) {
iVar1 = 0;
}
else if (param_1 == 1) {
iVar1 = 1;
}
else {
iVar1 = recursive(param_1 + -1,param_2,param_3);
iVar2 = recursive(param_1 + -2,param_2,param_3);
iVar1 = iVar1 * param_2 - iVar2 * param_3;
}
return iVar1;
}
void phase4(undefined8 param_1)
{
long in_FS_OFFSET;
int integer1;
int integer2;
int local_18;
int result;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
local_18 = __isoc99_sscanf(param_1,"%d %d",&integer1,&integer2);
if (((local_18 != 2) || (integer1 < 0)) || (integer2 < 0)) {
bomb();
}
result = recursive(5,integer1,integer2);
if (result != 0xc6be719) {
bomb();
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
#phase4 + DefuseBomb
"""def func(n, d1, d2):
result = 0
tmp = 0
if (n == 0):
result = 0
elif (n == 1):
result = 1
else:
result = func(n + -1,d1,d2)
tmp = func(n + -2,d1,d2)
result = result * d1 - tmp * d2
return result
num1 = 0
num2 = 0
for i in range(0, 1000):
for j in range(0, 1000):
value = func(5, i, j)
if(value == 0xc6be719):
num1 = i
num2 = j
break
if(num1 != 0 and num2 != 0):
break
print(num1)
print(num2)"""
print(p.recvline().decode())
p.send_raw(str(123).encode() + b' ' + str(456).encode() + b' c0m0r1bb\n')
마지막에 문자열 'c0m0r1bb' 을 넣어준 이유는 DefuseBomb 함수의 일부분
if (idx == 6) {
local_6c = __isoc99_sscanf(&DAT_00303e70,"%d %d %s",local_74,local_70,local_68);
if (local_6c == 3) {
iVar1 = isSameString(local_68,"c0m0r1bb");
if (iVar1 == 0) {
puts("Curses, you\'ve found the secret phase!");
puts("But finding it and solving it are quite different...");
finalphase();
}
}
}
phase6 이 끝나고 나서 finalphase 에 들어가기 위함이다.
phase 5 :
void phase5(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
double input;
double f;
double a;
double compare;
double b;
double c;
double d;
double e;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
compare = 1e-07;
f = 0.0;
a = 0.0;
iVar1 = __isoc99_sscanf(0,param_1,&%lf,&input);
if (((iVar1 != 1) || (input < 0.0)) || (1.0 < input)) {
bomb();
}
while (a < input) {
b = (f + 1.0) * (a + 1.0);
a = compare / 2.0 + a;
c = ((b * compare) / 2.0 + f + 1.0) * (a + 1.0);
d = ((c * compare) / 2.0 + f + 1.0) * (a + 1.0);
a = compare / 2.0 + a;
e = (compare * d + f + 1.0) * (a + 1.0);
f = ((d + d + c + c + b + e) * compare) / 6.0 + f;
}
if (compare < (double)((ulong)(f - 1.604151184400547) & 0x7fffffffffffffff)) {
bomb();
}
if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
문제를 풀 당시, 여기에서 시간을 좀 썼다.
이 코드를 그대로 복붙해서 돌려봐도 결과가 같게 나오진 않는다.
(기드라가 정확하게 디컴파일 하지 못하는 경우가 있다)
그래서 그냥 ida 를 이용해서, 이 bomb 파일의 어셈블리어를 조작해 elf 파일을 따로 만들었고
어떤 input 이 들어갈 때, 어떤 f 값이 나오는 지 리스트를 뽑았으며
0.7 근처에서 값이 작아져서, 그 근처를 조사했다.
대충
a = 0.68
while a <= 1.0:
p = process('bomb.patched')
p.recvline()
...
p.send_raw(str(a).encode() + b'\n')
f = hex_to_double(p.recvline()[2:-1])
print('f : ' + str(f))
if(p.recvline() == b'0x1\n'): #if sucess print 0x1 else print 0x0
print('bingo! a : ' + str(a))
break
a = compare / 2.0 + a
a = compare / 2.0 + a
p.close()
이런식으로 스크립트를 짜서 적절한 input 값을 찾았다.
#phase5
print(p.recvline().decode())
p.send_raw(str(0.7071067428588867).encode() + b'\n')
phase6 :
ulong recursive(uint param_1,int param_2,uint param_3,uint *param_4)
{
ulong uVar1;
if (param_4 == (uint *)0x0) {
uVar1 = 0;
}
else if (param_3 == *param_4) {
if (param_1 == param_2 * 2) {
uVar1 = (ulong)param_3;
}
else {
uVar1 = 0;
}
}
else if (param_3 == param_4[1]) {
if (param_1 == (param_2 * 2 | 1U)) {
uVar1 = (ulong)param_3;
}
else {
uVar1 = 0;
}
}
else if ((int)param_3 < (int)*param_4) {
uVar1 = recursive(param_1,param_2 * 3 + -1,param_3,*(undefined8 *)(param_4 + 2));
}
else if ((int)param_3 < (int)param_4[1]) {
uVar1 = recursive(param_1,param_2 * 3,param_3,*(undefined8 *)(param_4 + 4));
}
else {
uVar1 = recursive(param_1,param_2 * 3 + 1,param_3,*(undefined8 *)(param_4 + 6));
}
return uVar1;
}
void phase6(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
int i;
int local_4c;
int intarray [14];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_4c = 1;
Get12Integer(param_1,intarray);
for (i = 0; i < 6; i = i + 1) {
if ((intarray[i] < 1) || (0x1a < intarray[i])) {
bomb();
}
}
for (i = 0; i < 6; i = i + 1) {
iVar1 = recursive(intarray[i],1,intarray[i + 6],&DAT_00303d20);
local_4c = local_4c * iVar1;
}
if (local_4c != 0xdac3bf7) {
bomb();
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
여기 부분도 어려워보이지만 사실 별거 아니다.
함수를 잘 살펴보면 건네준 intarray 값 두 개에만 dependent 한 것을 볼 수 있다.
즉, 범위를 만족하는 int 값 두 개에 대한 recursive 함수 결과 값은 루프 몇번째에서 계산이 되는지와 무관하다.
그럼 int 두 개를 짝지어서 다 recursive 함수 돌려서 값을 다 구하고
(ida 를 이용해 계산된 값을 출력하도록 코드패치 하여, 각각의 값을 다 구했다)
그 중 0xdac3bf7 % result == 0 인 결과 값을 뽑아내면 된다.
(범위 제한은 intarray 처음 6개에만 적용되는 것을 유의해야한다)
다 뽑아낸 뒤, 그 중에서도 곱하면 0xdac3bf7 이 되는 6개의 값을 찾으면
그 6개의 값이 되기 위한 int 페어들이 payload 이다.
#phase6
print(p.recvline().decode())
p.send_raw(b'12 15 2 17 18 21 7 19 23 31 41 59\n')
이제 마지막이다.
DefuseBomb :
void DefuseBomb(void)
{
int iVar1;
long lVar2;
long in_FS_OFFSET;
undefined local_74 [4];
undefined local_70 [4];
int local_6c;
undefined local_68 [88];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
if (0 < idx) {
if (idx == 1) {
lVar2 = ptrace(PTRACE_TRACEME,0,0,0);
if (lVar2 == -1) {
bomb();
}
}
if (idx == 6) {
local_6c = __isoc99_sscanf(&phase4_string,"%d %d %s",local_74,local_70,local_68);
if (local_6c == 3) {
iVar1 = isSameString(local_68,"c0m0r1bb");
if (iVar1 == 0) {
puts("Curses, you\'ve found the secret phase!");
puts("But finding it and solving it are quite different...");
finalphase();
}
}
}
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
idx 변수는 InputString 함수로 string 을 입력 받을때마다 하나씩 늘어난다.
즉, phase6 이 끝나면 idx 는 6이 되고 finalphase 에 들어갈 기회를 얻는다.
phase4 에서 언급했듯이, finalphase 에 들어갈려면 phase4 때 입력받은 string 끝에 c0m0r1bb 라는 문자열을 넣어줘야한다.
finalphase :
void finalphase(void)
{
int int;
char *string;
long in_FS_OFFSET;
uint i;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
string = (char *)InputString();
bomb();
int = atoi(string);
int = abs(int);
if ((double)int == 3.14) {
puts("Wow! You\'ve defused the secret stage!");
puts("Congratulations! You\'ve defused the bomb!");
getflag();
/* WARNING: Subroutine does not return */
exit(0);
}
for (i = 0; i < 0x16f; i = i + 1) {
*(long *)(&stack0x00000000 + (long)(int)i * 8) =
*(long *)(&DAT_00303020 + (long)(int)i * 8) + 0x2bf240;
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail(string);
}
return;
}
이 코드를 처음 볼 때, 좀 당황했다.
입력받은 int 가 3.14 가 될 수 없기에, getflag 함수를 호출하는 것이 불가능해보였기 때문이다.
그래서 그냥 assembly 를 천천히 살펴보았다.
처음 부분에서 abs 함수의 주소와 atoi 함수의 주소 하위 1.5 바이트를 비교하는 것을 볼 수 있다.
그리고 나중엔 갑자기 rbp + 0x8 를 가지고 오기도 하고,
0x45dd0 을 빼는 명령어도 있다.
뭔가 평범하지는 않아 보여서, 더 분석하자면
ret 하기 전에 r9 에 finalphase + 0x90 주소값을 넣고, r10 에 bomb 함수 주소값을 넣는 것을 볼 수 있다.
getflag 함수 호출을 할 방법이, 앞의 3.14 비교 구문 밖에 없었다면 이런 이상한 어셈블리어는 없었을 것이다.
무엇보다 이상했던 점은 지금까지의 payload 를 다 보냈을 때, 원래대로라면 'Umm... ~' 의 문자열이 출력되어야 했을 것이다.
그러나 bomb 함수가 실행된 모습을 볼 수 있었다.
그래서 patchelf 로 라이브러리를 끼워 넣어준 다음, 직접 gdb 로 디버깅해보았다.
알고보니 그 이후로 rop chain 이 쭉 이어져 있는 것을 발견했다.
(입력한 값은 rdi 에 있고 r9 에는 flag 를 얻는 주소, r10 에는 bomb 함수의 주소가 있다)
쭉 가다보면 call r10 이 호출되게 되니, bomb 함수가 호출되는 것이었다.
입력한 값은 rdi 에 있으니 rdi 를 기준으로 노트에 적어가며 rop chain 을 분석해보았고 rdi 의 값이 어떤 문자열이 되어야 한다는 것을 알아내었다.
(자세한 분석 사진을 올리고 싶으나, 중간에 칼리 리눅스가 죽어버려서 다 날라갔다...)
이 내용을 코드로 대충 적자면
target_addr = 0x7ffff7facd80
while True:
if([rdi] == 0x0):
break
#asm
movzx eax, [rdi]
sub eax, ecx ; ecx : 0x60041
for i in range(2):
shl rax, 5
lea rax, [rax + rdx + 4] ; rdx : 0x0
sub rax, 1
sub rax, 1
sub rax, 1
sub rax, 1
sar eax, 6
mov rcx, rax
mov BYTE PTR [target_addr], cl
rdi += 1
movzx eax, [rdi]
sub eax, ecx ; ecx : 0x60041
mov r8b, al
add BYTE PTR [target_addr], r8b
target_addr += 1
if([target_addr] == "[R0P_M4DN3SS!!]"):
getflag()
else:
bomb()
대충 이런식이다.
(정확하지는 않다)
첫번째 값으로 'a' 를 전달하면 결국 target_addr 에 0x0 이 전달되고,
두번째 값으로 0x41 + 원하는 값 을 전달하면
0x7ffff7facd80 주소에 원하는 문자열을 넣을 수 있다.
#DefuseBomb
p.send_raw(b'a\x9ca\x93a\x71a\x91a\xa0a\x8ea\x75a\x85a\x8fa\x74a\x94a\x94a\x62a\x62a\x9ea\x41' + b'\n')
이렇게 하면 flag 를 얻을 수 있다.
최종 exploit 스크립트는 이렇다.
from pwn import *
p = remote('host3.dreamhack.games',17442)
#phase1
data102020 = [0x83, 0x9c, 0x89, 0x82, 0x93, 0x9f, 0x89, 0x9f, 0x8d, 0x81, 0x89, 0x00]
st = b''
for i in range(0xb):
st += chr(data102020[i] ^ 0xcc).encode()
print(p.recvline().decode())
print(p.recvline().decode())
p.send_raw(st + b'\n')
#phase2
data2 = [0, 0, 0, 0, 0, 0]
st = b'0 '
for i in range(1, 6):
data2[i] = data2[i - 1] + ((i + 1) * i >> 1)
st += str(data2[i]).encode() + b' '
st = st[:-1]
print(p.recvline().decode())
p.send_raw(st + b'\n')
#phase3
uint_1 = 0xdeadbeef
uint_3 = uint_1 * 0x1ad7e715
uint_2 = uint_1 - ((uint_3 >> 0x35) * 0x500bf) * 0x3d
if((uint_1 - uint_2) / 0x3d != (uint_3 >> 0x35) * 0x500bf):
exit(1)
print(p.recvline().decode())
p.send_raw(str(uint_1).encode() + b' ' + str(uint_2).encode() + b'\n')
#phase4 + DefuseBomb
"""def func(n, d1, d2):
result = 0
tmp = 0
if (n == 0):
result = 0
elif (n == 1):
result = 1
else:
result = func(n + -1,d1,d2)
tmp = func(n + -2,d1,d2)
result = result * d1 - tmp * d2
return result
num1 = 0
num2 = 0
for i in range(0, 1000):
for j in range(0, 1000):
value = func(5, i, j)
if(value == 0xc6be719):
num1 = i
num2 = j
break
if(num1 != 0 and num2 != 0):
break
print(num1)
print(num2)"""
print(p.recvline().decode())
p.send_raw(str(123).encode() + b' ' + str(456).encode() + b' c0m0r1bb\n')
#phase5
print(p.recvline().decode())
p.send_raw(str(0.7071067428588867).encode() + b'\n')
#phase6
print(p.recvline().decode())
p.send_raw(b'12 15 2 17 18 21 7 19 23 31 41 59\n')
print(p.recvline().decode())
print(p.recvline().decode())
#DefuseBomb
p.send_raw(b'a\x9ca\x93a\x71a\x91a\xa0a\x8ea\x75a\x85a\x8fa\x74a\x94a\x94a\x62a\x62a\x9ea\x41' + b'\n')
p.interactive()
'CTF writeup > GoN Open Qual CTF' 카테고리의 다른 글
[2022 Fall GoN Open Qual CTF] Private Storage writeup (0) | 2022.08.31 |
---|---|
[2022 Fall GoN Open Qual CTF] SleepingShark writeup (0) | 2022.08.31 |
[2022 Fall GoN Open Qual CTF] Zero Gravity writeup (0) | 2022.08.31 |
[2022 Fall GoN Open Qual CTF] Api Portal writeup (0) | 2022.08.31 |
[2022 Fall GoN Open Qual CTF] Checkers writeup (0) | 2022.08.31 |